mirror of
https://github.com/LamGC/ScalaBot.git
synced 2025-07-01 04:47:24 +00:00
Compare commits
101 Commits
Author | SHA1 | Date | |
---|---|---|---|
3932db11a1
|
|||
d18c059498
|
|||
e6b581b8cd
|
|||
26d7443c87
|
|||
2bf4eb684e
|
|||
5251b62733
|
|||
3ec5a8e9c3
|
|||
62f6c08cd2
|
|||
5e6f64f056
|
|||
a036695330
|
|||
e9c975f5c5
|
|||
9aac42d414
|
|||
5add6d9909
|
|||
eee6b7d2c9
|
|||
ff396425a7
|
|||
580d9122e5
|
|||
2a607f1129
|
|||
2d6da7c1ae
|
|||
6235c5f51a
|
|||
255a02c93c
|
|||
dce28be9c7
|
|||
673c6d8392
|
|||
d586ca378e
|
|||
3ba4364a07
|
|||
eda0e522cd
|
|||
c7fedf3882
|
|||
a7de85eacb
|
|||
b6013e2fbe
|
|||
f79a4e4ff3
|
|||
93b9c6b727
|
|||
a8a0a9576f
|
|||
e8711e9974
|
|||
93685e9440
|
|||
92b7e84b3a
|
|||
8c4e48e3eb
|
|||
7f7b2b8895
|
|||
441991b705
|
|||
51d036c4c6
|
|||
3c54c33364
|
|||
43dd0e7bea
|
|||
c144755913
|
|||
9ed55204c0
|
|||
9b7fc30512
|
|||
27dc26160d
|
|||
ae411ce829
|
|||
1afe0f07a8
|
|||
cf8e746bd4
|
|||
fc66cd16f4
|
|||
1340f0aa32
|
|||
099c452fe7 | |||
aa31bcd3a8
|
|||
5843e37196
|
|||
06acc78180
|
|||
db010d6c86
|
|||
61b611b22e
|
|||
c7c24fa454
|
|||
045b3e5d54
|
|||
d6b25c4560
|
|||
581eeba20b
|
|||
896305f4a3
|
|||
df484d6bd7
|
|||
b8a99a4491
|
|||
128e33e545
|
|||
85e59f4a64
|
|||
8f8d763566
|
|||
084280564a
|
|||
4a160ad42b
|
|||
a1790a0716
|
|||
b12758bd18
|
|||
a2667438f2
|
|||
d5e66156b9
|
|||
8a33448b19
|
|||
48a5c27cf7
|
|||
0c252f69fb
|
|||
a55f00edf0
|
|||
45244c1fb1
|
|||
cfdfa21619
|
|||
8e0bf3c22b | |||
c64f5e739b
|
|||
289b9678f2
|
|||
dbc4232dd6
|
|||
c662b970f0
|
|||
f148c21390
|
|||
c41aac735c
|
|||
ae64de00e7
|
|||
215a4670db
|
|||
c5fe96c02d
|
|||
508f14f271
|
|||
35c77f6093
|
|||
7e48f4bf0b
|
|||
9c05726849
|
|||
ac0a398afc
|
|||
145e5a2141
|
|||
b5c85e213b
|
|||
746221a085
|
|||
24f34aa27f
|
|||
31366575a9
|
|||
37c3275bb6
|
|||
72e26bd677
|
|||
9aab3c2a24
|
|||
cac055bb08
|
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@ -0,0 +1,2 @@
|
||||
**/
|
||||
!scalabot-app/build/install/
|
35
.github/workflows/build-and-test.yml
vendored
Normal file
35
.github/workflows/build-and-test.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
|
||||
|
||||
name: Build and test project
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
timeout-minutes: 8
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt-hotspot'
|
||||
cache: 'gradle'
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Build and test
|
||||
uses: gradle/gradle-build-action@v2.2.1
|
||||
with:
|
||||
gradle-version: 'wrapper'
|
||||
arguments: test
|
65
.github/workflows/create-release.yml
vendored
Normal file
65
.github/workflows/create-release.yml
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
name: Create release draft
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+*'
|
||||
|
||||
# 该 Action 有以下步骤:
|
||||
# 1. 拉取并构建代码, 然后生成 Application 发行包;
|
||||
# 2. 创建 Release, 并标记为 Draft(草稿);
|
||||
# 3. 上传 Application 发行包;
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# 创建更新日志.
|
||||
- name: 'Get Previous tag'
|
||||
id: previous-tag
|
||||
uses: younited/get-previous-tag-action@v1.0.0
|
||||
with:
|
||||
match: "v*.*.*"
|
||||
- name: Set up Python 3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install Commitizen
|
||||
run: pip install -U commitizen
|
||||
- name: Create Change log
|
||||
run: cz ch --start-rev ${{ steps.previous-tag.outputs.previous-tag }} --file-name ${{ github.workspace }}/CURRENT_CHANGELOG.md
|
||||
|
||||
# 开始构建项目.
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt-hotspot'
|
||||
cache: 'gradle'
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Build and test
|
||||
uses: gradle/gradle-build-action@v2.2.1
|
||||
with:
|
||||
gradle-version: 'wrapper'
|
||||
arguments: clean test assembleDist
|
||||
|
||||
# 创建新的发行版本
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
draft: true
|
||||
body_path: ${{ github.workspace }}/CURRENT_CHANGELOG.md
|
||||
files: |
|
||||
*/build/distributions/*
|
||||
*/build/libs/*
|
49
.github/workflows/release-container-image.yml
vendored
Normal file
49
.github/workflows/release-container-image.yml
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
name: Release container image
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
env:
|
||||
IMAGE_NAME: lamgc/scalabot
|
||||
|
||||
jobs:
|
||||
release-image:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt-hotspot'
|
||||
cache: 'gradle'
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Build and test
|
||||
uses: gradle/gradle-build-action@v2.2.1
|
||||
with:
|
||||
gradle-version: 'wrapper'
|
||||
arguments: clean test installDist
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker BuildX
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push container image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ env.IMAGE_NAME }}:latest, ${{ env.IMAGE_NAME }}:${{ github.ref_name }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
49
.github/workflows/release-dev-container-image.yml
vendored
Normal file
49
.github/workflows/release-dev-container-image.yml
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
name: Build development version container image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt-hotspot'
|
||||
cache: 'gradle'
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Build project and install Distribution package
|
||||
uses: gradle/gradle-build-action@v2.2.1
|
||||
with:
|
||||
gradle-version: 'wrapper'
|
||||
arguments: installDist
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker BuildX
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push container image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: lamgc/scalabot:dev
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@ -0,0 +1,8 @@
|
||||
FROM openjdk:18
|
||||
|
||||
ENV BOT_DATA_PATH /scalabot/data/
|
||||
WORKDIR /scalabot/run/
|
||||
|
||||
CMD ["/scalabot/app/bin/scalabot-app"]
|
||||
|
||||
COPY scalabot-app/build/install/scalabot-app/ /scalabot/app/
|
@ -1,7 +1,11 @@
|
||||
<div style="text-align: center;">
|
||||
|
||||
# ScalaBot
|
||||
|
||||
基于 [rubenlagus/TelegramBots](https://github.com/rubenlagus/TelegramBots) 的可扩展机器人服务器。 Extensible robot server based
|
||||
on [rubenlagus/TelegramBots](https://github.com/rubenlagus/TelegramBots).
|
||||
基于 [rubenlagus/TelegramBots](https://github.com/rubenlagus/TelegramBots) 的可扩展机器人服务器。
|
||||
Extensible robot server based on [rubenlagus/TelegramBots](https://github.com/rubenlagus/TelegramBots).
|
||||
|
||||
</div>
|
||||
|
||||
## 背景
|
||||
|
||||
|
@ -1,3 +1,8 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "1.7.10" apply false
|
||||
id("org.jetbrains.kotlinx.kover") version "0.5.1" apply false
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@ -7,5 +12,5 @@ allprojects {
|
||||
|
||||
}
|
||||
group = "net.lamgc"
|
||||
version = "0.3.0"
|
||||
version = "0.5.1"
|
||||
}
|
0
gradlew.bat
vendored
Normal file → Executable file
0
gradlew.bat
vendored
Normal file → Executable file
@ -1,16 +1,17 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") version "1.6.10"
|
||||
kotlin("jvm")
|
||||
application
|
||||
// id("org.jetbrains.kotlin") version "1.6.10"
|
||||
id("org.jetbrains.kotlinx.kover")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":scalabot-meta"))
|
||||
implementation(project(":scalabot-extension"))
|
||||
|
||||
implementation("org.slf4j:slf4j-api:1.7.36")
|
||||
implementation("io.github.microutils:kotlin-logging:2.1.21")
|
||||
implementation("io.github.microutils:kotlin-logging:2.1.23")
|
||||
implementation("ch.qos.logback:logback-classic:1.2.11")
|
||||
|
||||
val aetherVersion = "1.1.0"
|
||||
@ -22,20 +23,21 @@ dependencies {
|
||||
implementation("org.eclipse.aether:aether-connector-basic:$aetherVersion")
|
||||
implementation("org.apache.maven:maven-aether-provider:3.3.9")
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect:1.6.20")
|
||||
implementation("com.google.code.gson:gson:2.9.0")
|
||||
|
||||
implementation("org.jdom:jdom2:2.0.6.1")
|
||||
|
||||
implementation("org.telegram:telegrambots-abilities:6.0.1")
|
||||
implementation("org.telegram:telegrambots:6.0.1")
|
||||
implementation("org.telegram:telegrambots-abilities:6.1.0")
|
||||
implementation("org.telegram:telegrambots:6.1.0")
|
||||
|
||||
implementation("io.prometheus:simpleclient:0.15.0")
|
||||
implementation("io.prometheus:simpleclient_httpserver:0.15.0")
|
||||
|
||||
testImplementation(kotlin("test"))
|
||||
testImplementation("io.mockk:mockk:1.12.3")
|
||||
testImplementation("io.mockk:mockk:1.12.4")
|
||||
testImplementation("com.github.stefanbirkner:system-lambda:1.2.1")
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
@ -54,3 +56,8 @@ application {
|
||||
tasks.jar.configure {
|
||||
exclude("**/logback-test.xml")
|
||||
}
|
||||
|
||||
tasks.withType<AbstractArchiveTask>().configureEach {
|
||||
isPreserveFileTimestamps = false
|
||||
isReproducibleFileOrder = true
|
||||
}
|
||||
|
@ -3,122 +3,67 @@ package net.lamgc.scalabot
|
||||
import ch.qos.logback.core.PropertyDefinerBase
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.google.gson.JsonArray
|
||||
import mu.KotlinLogging
|
||||
import net.lamgc.scalabot.util.ArtifactSerializer
|
||||
import net.lamgc.scalabot.util.AuthenticationSerializer
|
||||
import net.lamgc.scalabot.util.MavenRepositoryConfigSerializer
|
||||
import net.lamgc.scalabot.util.ProxyTypeSerializer
|
||||
import net.lamgc.scalabot.config.*
|
||||
import net.lamgc.scalabot.config.serializer.*
|
||||
import org.eclipse.aether.artifact.Artifact
|
||||
import org.eclipse.aether.repository.Authentication
|
||||
import org.eclipse.aether.repository.Proxy
|
||||
import org.eclipse.aether.repository.RemoteRepository
|
||||
import org.eclipse.aether.repository.RepositoryPolicy
|
||||
import org.telegram.telegrambots.bots.DefaultBotOptions
|
||||
import org.telegram.telegrambots.meta.ApiConstants
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.system.exitProcess
|
||||
import java.util.function.Supplier
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
private val log = KotlinLogging.logger { }
|
||||
|
||||
/**
|
||||
* 机器人帐号信息.
|
||||
* @property name 机器人名称, 建议与实际设定的名称相同.
|
||||
* @property token 机器人 API Token.
|
||||
* @property creatorId 机器人创建者, 管理机器人需要使用该信息.
|
||||
*/
|
||||
internal data class BotAccount(
|
||||
val name: String,
|
||||
val token: String,
|
||||
val creatorId: Long = -1
|
||||
) {
|
||||
|
||||
val id
|
||||
// 不要想着每次获取都要从 token 里取出有性能损耗.
|
||||
// 由于 Gson 解析方式, 如果不这么做, 会出现 token 设置前 id 初始化完成, 就只有"0"了,
|
||||
// 虽然能过单元测试, 但实际使用过程是不能正常用的.
|
||||
get() = token.substringBefore(":").toLong()
|
||||
internal fun ProxyType.toTelegramBotsType(): DefaultBotOptions.ProxyType {
|
||||
return when (this) {
|
||||
ProxyType.NO_PROXY -> DefaultBotOptions.ProxyType.NO_PROXY
|
||||
ProxyType.HTTP -> DefaultBotOptions.ProxyType.HTTP
|
||||
ProxyType.HTTPS -> DefaultBotOptions.ProxyType.HTTP
|
||||
ProxyType.SOCKS4 -> DefaultBotOptions.ProxyType.SOCKS4
|
||||
ProxyType.SOCKS5 -> DefaultBotOptions.ProxyType.SOCKS5
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 机器人配置.
|
||||
* @property account 机器人帐号信息, 用于访问 API.
|
||||
* @property disableBuiltInAbility 是否禁用 AbilityBot 自带命令.
|
||||
* @property extensions 该机器人启用的扩展.
|
||||
* @property proxy 为该机器人单独设置的代理配置, 如无设置, 则使用 AppConfig 中的代理配置.
|
||||
*/
|
||||
internal data class BotConfig(
|
||||
val enabled: Boolean = true,
|
||||
val account: BotAccount,
|
||||
val disableBuiltInAbility: Boolean = false,
|
||||
val autoUpdateCommandList: Boolean = false,
|
||||
/*
|
||||
* 使用构件坐标来选择机器人所使用的扩展包.
|
||||
* 这么做的原因是我暂时没找到一个合适的方法来让开发者方便地设定自己的扩展 Id,
|
||||
* 而构件坐标(POM Reference 或者叫 GAV 坐标)是开发者创建 Maven/Gradle 项目时一定会设置的,
|
||||
* 所以就直接用了. :P
|
||||
*/
|
||||
val extensions: Set<Artifact>,
|
||||
val proxy: ProxyConfig? = ProxyConfig(),
|
||||
val baseApiUrl: String? = ApiConstants.BASE_URL
|
||||
)
|
||||
internal fun ProxyConfig.toAetherProxy(): Proxy? {
|
||||
val typeStr = when (type) {
|
||||
ProxyType.HTTP -> Proxy.TYPE_HTTP
|
||||
ProxyType.HTTPS -> Proxy.TYPE_HTTPS
|
||||
else -> return null
|
||||
}
|
||||
return Proxy(typeStr, host, port)
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理配置.
|
||||
* @property type 代理类型.
|
||||
* @property host 代理服务端地址.
|
||||
* @property port 代理服务端端口.
|
||||
*/
|
||||
internal data class ProxyConfig(
|
||||
val type: DefaultBotOptions.ProxyType = DefaultBotOptions.ProxyType.NO_PROXY,
|
||||
val host: String = "127.0.0.1",
|
||||
val port: Int = 1080
|
||||
) {
|
||||
|
||||
fun toAetherProxy(): Proxy? {
|
||||
return if (type == DefaultBotOptions.ProxyType.HTTP) {
|
||||
Proxy(Proxy.TYPE_HTTP, host, port)
|
||||
internal fun MavenRepositoryConfig.toRemoteRepository(proxyConfig: ProxyConfig? = null): RemoteRepository {
|
||||
val repositoryId = if (id == null) {
|
||||
val generatedRepoId = createDefaultRepositoryId()
|
||||
log.debug { "仓库 Url `$url` 未设置仓库 Id, 已分配缺省 Id: $generatedRepoId" }
|
||||
generatedRepoId
|
||||
} else {
|
||||
null
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal data class MetricsConfig(
|
||||
val enable: Boolean = false,
|
||||
val port: Int = 9386,
|
||||
val bindAddress: String? = "0.0.0.0"
|
||||
)
|
||||
|
||||
/**
|
||||
* Maven 远端仓库配置.
|
||||
* @property url 仓库地址.
|
||||
* @property proxy 访问仓库所使用的代理, 仅支持 http/https 代理.
|
||||
* @property layout 仓库布局版本, Maven 2 及以上使用 `default`, Maven 1 使用 `legacy`.
|
||||
*/
|
||||
internal data class MavenRepositoryConfig(
|
||||
val id: String? = null,
|
||||
val url: URL,
|
||||
val proxy: Proxy? = Proxy("http", "127.0.0.1", 1080),
|
||||
val layout: String = "default",
|
||||
val enableReleases: Boolean = true,
|
||||
val enableSnapshots: Boolean = true,
|
||||
// 可能要设计个 type 来判断解析成什么类型的 Authentication.
|
||||
val authentication: Authentication? = null
|
||||
) {
|
||||
|
||||
fun toRemoteRepository(proxyConfig: ProxyConfig): RemoteRepository {
|
||||
val builder =
|
||||
RemoteRepository.Builder(id ?: createDefaultRepositoryId(), checkRepositoryLayout(layout), url.toString())
|
||||
val builder = RemoteRepository.Builder(repositoryId, checkRepositoryLayout(layout), url.toString())
|
||||
if (proxy != null) {
|
||||
builder.setProxy(proxy)
|
||||
} else if (proxyConfig.type == DefaultBotOptions.ProxyType.HTTP) {
|
||||
val selfProxy = proxy!!
|
||||
builder.setProxy(selfProxy)
|
||||
log.debug { "仓库 $repositoryId 已使用独立的代理配置: ${selfProxy.type}://${selfProxy.host}:${selfProxy.port}" }
|
||||
} else if (proxyConfig != null) {
|
||||
if (proxyConfig.type in (ProxyType.HTTP..ProxyType.HTTPS)) {
|
||||
builder.setProxy(proxyConfig.toAetherProxy())
|
||||
log.debug { "仓库 $repositoryId 已使用 全局/Bot 代理配置: $proxyConfig" }
|
||||
} else {
|
||||
log.debug { "仓库 $repositoryId 不支持 全局/Bot 的代理配置: `$proxyConfig` (仅支持 HTTP 和 HTTPS)" }
|
||||
}
|
||||
} else {
|
||||
log.debug { "仓库 $repositoryId 不使用代理." }
|
||||
}
|
||||
|
||||
builder.setReleasePolicy(
|
||||
@ -139,8 +84,7 @@ internal data class MavenRepositoryConfig(
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
fun checkRepositoryLayout(layoutType: String): String {
|
||||
private fun checkRepositoryLayout(layoutType: String): String {
|
||||
val type = layoutType.trim().lowercase()
|
||||
if (type != "default" && type != "legacy") {
|
||||
throw IllegalArgumentException("Invalid layout type (expecting 'default' or 'legacy')")
|
||||
@ -148,40 +92,21 @@ internal data class MavenRepositoryConfig(
|
||||
return type
|
||||
}
|
||||
|
||||
private val repoNumber = AtomicInteger(1)
|
||||
private val repoNumberGenerator = AtomicInteger(1)
|
||||
|
||||
fun createDefaultRepositoryId(): String {
|
||||
return "Repository-${repoNumber.getAndIncrement()}"
|
||||
private fun createDefaultRepositoryId(): String {
|
||||
return "Repository-${repoNumberGenerator.getAndIncrement()}"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ScalaBot App 配置.
|
||||
*
|
||||
* App 配置信息与 BotConfig 分开, 分别存储在各自单独的文件中.
|
||||
* @property proxy Telegram API 代理配置.
|
||||
* @property metrics 运行指标数据配置. 可通过时序数据库记录运行数据.
|
||||
* @property mavenRepositories Maven 远端仓库配置.
|
||||
* @property mavenLocalRepository Maven 本地仓库路径. 相对于运行目录 (而不是 DATA_ROOT 目录)
|
||||
*/
|
||||
internal data class AppConfig(
|
||||
val proxy: ProxyConfig = ProxyConfig(),
|
||||
val metrics: MetricsConfig = MetricsConfig(),
|
||||
val mavenRepositories: List<MavenRepositoryConfig> = emptyList(),
|
||||
val mavenLocalRepository: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* 需要用到的路径.
|
||||
*
|
||||
* 必须提供 `pathSupplier` 或 `fileSupplier` 其中一个, 才能正常提供路径.
|
||||
*/
|
||||
internal enum class AppPaths(
|
||||
private val pathSupplier: () -> String = { fileSupplier.invoke().canonicalPath },
|
||||
private val pathSupplier: PathSupplier,
|
||||
private val initializer: AppPaths.() -> Unit = AppPaths::defaultInitializer,
|
||||
private val fileSupplier: () -> File = { File(pathSupplier()) }
|
||||
private val fileSupplier: FileSupplier,
|
||||
) {
|
||||
/**
|
||||
* 数据根目录.
|
||||
@ -190,7 +115,7 @@ internal enum class AppPaths(
|
||||
*
|
||||
* 提示: 结尾不带 `/`.
|
||||
*/
|
||||
DATA_ROOT(fileSupplier = {
|
||||
DATA_ROOT(fileSupplier = FileSupplier {
|
||||
File(
|
||||
System.getProperty(PathConst.PROP_DATA_PATH) ?: System.getenv(PathConst.ENV_DATA_PATH)
|
||||
?: System.getProperty("user.dir") ?: "."
|
||||
@ -202,10 +127,10 @@ internal enum class AppPaths(
|
||||
}
|
||||
}),
|
||||
|
||||
DEFAULT_CONFIG_APPLICATION({ "$DATA_ROOT/config.json" }, {
|
||||
CONFIG_APPLICATION(PathSupplier { "$DATA_ROOT/config.json" }, {
|
||||
if (!file.exists()) {
|
||||
file.bufferedWriter(StandardCharsets.UTF_8).use {
|
||||
GsonConst.botConfigGson.toJson(
|
||||
GsonConst.appConfigGson.toJson(
|
||||
AppConfig(
|
||||
mavenRepositories = listOf(
|
||||
MavenRepositoryConfig(
|
||||
@ -218,13 +143,13 @@ internal enum class AppPaths(
|
||||
}
|
||||
}
|
||||
}),
|
||||
DEFAULT_CONFIG_BOT({ "$DATA_ROOT/bot.json" }, {
|
||||
CONFIG_BOT(PathSupplier { "$DATA_ROOT/bot.json" }, {
|
||||
if (!file.exists()) {
|
||||
file.bufferedWriter(StandardCharsets.UTF_8).use {
|
||||
GsonConst.botConfigGson.toJson(
|
||||
setOf(
|
||||
BotConfig(
|
||||
enabled = false,
|
||||
enabled = true,
|
||||
proxy = ProxyConfig(),
|
||||
account = BotAccount(
|
||||
"Bot Username",
|
||||
@ -244,10 +169,25 @@ internal enum class AppPaths(
|
||||
TEMP({ "$DATA_ROOT/tmp/" })
|
||||
;
|
||||
|
||||
val file: File
|
||||
get() = fileSupplier.invoke()
|
||||
val path: String
|
||||
get() = pathSupplier.invoke()
|
||||
constructor(pathSupplier: PathSupplier, initializer: AppPaths.() -> Unit = AppPaths::defaultInitializer) : this(
|
||||
fileSupplier = FileSupplier { File(pathSupplier.path).canonicalFile },
|
||||
pathSupplier = pathSupplier,
|
||||
initializer = initializer
|
||||
)
|
||||
|
||||
constructor(fileSupplier: FileSupplier, initializer: AppPaths.() -> Unit = AppPaths::defaultInitializer) : this(
|
||||
fileSupplier = fileSupplier,
|
||||
pathSupplier = PathSupplier { fileSupplier.file.canonicalPath },
|
||||
initializer = initializer
|
||||
)
|
||||
|
||||
constructor(pathSupplier: () -> String) : this(
|
||||
fileSupplier = FileSupplier { File(pathSupplier.invoke()).canonicalFile },
|
||||
pathSupplier = PathSupplier { pathSupplier.invoke() }
|
||||
)
|
||||
|
||||
val file: File by fileSupplier
|
||||
val path: String by pathSupplier
|
||||
|
||||
private val initialized = AtomicBoolean(false)
|
||||
|
||||
@ -259,15 +199,44 @@ internal enum class AppPaths(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 一个内部方法, 用于将 [initialized] 状态重置.
|
||||
*
|
||||
* 如果不重置该状态, 将使得单元测试无法让 AppPath 重新初始化文件.
|
||||
*
|
||||
* 警告: 该方法不应该被非测试代码调用.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
private fun reset() {
|
||||
log.warn {
|
||||
"初始化状态已重置: `${this.name}`, 如果在非测试环境中重置状态, 请报告该问题."
|
||||
}
|
||||
initialized.set(false)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return path
|
||||
}
|
||||
|
||||
private object PathConst {
|
||||
object PathConst {
|
||||
const val PROP_DATA_PATH = "bot.path.data"
|
||||
const val ENV_DATA_PATH = "BOT_DATA_PATH"
|
||||
}
|
||||
|
||||
private class FileSupplier(private val supplier: Supplier<File>) {
|
||||
operator fun getValue(appPaths: AppPaths, property: KProperty<*>): File = supplier.get()
|
||||
|
||||
val file: File
|
||||
get() = supplier.get()
|
||||
}
|
||||
|
||||
private class PathSupplier(private val supplier: Supplier<String>) {
|
||||
operator fun getValue(appPaths: AppPaths, property: KProperty<*>): String = supplier.get()
|
||||
|
||||
val path: String
|
||||
get() = supplier.get()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -298,9 +267,14 @@ private fun AppPaths.defaultInitializer() {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun initialFiles() {
|
||||
val configFilesNotInitialized = !AppPaths.DEFAULT_CONFIG_APPLICATION.file.exists()
|
||||
&& !AppPaths.DEFAULT_CONFIG_BOT.file.exists()
|
||||
/**
|
||||
* 执行 AppPaths 所有项目的初始化, 并检查是否停止运行, 让用户编辑配置.
|
||||
*
|
||||
* @return 如果需要让用户编辑配置, 则返回 `true`.
|
||||
*/
|
||||
internal fun initialFiles(): Boolean {
|
||||
val configFilesNotInitialized = !AppPaths.CONFIG_APPLICATION.file.exists()
|
||||
&& !AppPaths.CONFIG_BOT.file.exists()
|
||||
|
||||
for (path in AppPaths.values()) {
|
||||
path.initial()
|
||||
@ -308,36 +282,50 @@ internal fun initialFiles() {
|
||||
|
||||
if (configFilesNotInitialized) {
|
||||
log.warn { "配置文件已初始化, 请根据需要修改配置文件后重新启动本程序." }
|
||||
exitProcess(1)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private object GsonConst {
|
||||
val baseGson: Gson = GsonBuilder()
|
||||
internal object GsonConst {
|
||||
private val baseGson: Gson = GsonBuilder()
|
||||
.setPrettyPrinting()
|
||||
.serializeNulls()
|
||||
.create()
|
||||
|
||||
val appConfigGson: Gson = baseGson.newBuilder()
|
||||
.registerTypeAdapter(DefaultBotOptions.ProxyType::class.java, ProxyTypeSerializer)
|
||||
.registerTypeAdapter(ProxyType::class.java, ProxyTypeSerializer)
|
||||
.registerTypeAdapter(MavenRepositoryConfig::class.java, MavenRepositoryConfigSerializer)
|
||||
.registerTypeAdapter(Authentication::class.java, AuthenticationSerializer)
|
||||
.registerTypeAdapter(UsernameAuthenticator::class.java, UsernameAuthenticatorSerializer)
|
||||
.create()
|
||||
|
||||
val botConfigGson: Gson = baseGson.newBuilder()
|
||||
.registerTypeAdapter(DefaultBotOptions.ProxyType::class.java, ProxyTypeSerializer)
|
||||
.registerTypeAdapter(ProxyType::class.java, ProxyTypeSerializer)
|
||||
.registerTypeAdapter(BotConfig::class.java, BotConfigSerializer)
|
||||
.registerTypeAdapter(Artifact::class.java, ArtifactSerializer)
|
||||
.registerTypeAdapter(BotAccount::class.java, BotAccountSerializer)
|
||||
.create()
|
||||
}
|
||||
|
||||
internal fun loadAppConfig(configFile: File = AppPaths.DEFAULT_CONFIG_APPLICATION.file): AppConfig {
|
||||
internal fun loadAppConfig(configFile: File = AppPaths.CONFIG_APPLICATION.file): AppConfig {
|
||||
try {
|
||||
configFile.bufferedReader(StandardCharsets.UTF_8).use {
|
||||
return GsonConst.appConfigGson.fromJson(it, AppConfig::class.java)!!
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.error { "读取 config.json 时发生错误, 请检查配置格式是否正确." }
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
internal fun loadBotConfig(botConfigFile: File = AppPaths.DEFAULT_CONFIG_BOT.file): Set<BotConfig> {
|
||||
internal fun loadBotConfigJson(botConfigFile: File = AppPaths.CONFIG_BOT.file): JsonArray? {
|
||||
try {
|
||||
botConfigFile.bufferedReader(StandardCharsets.UTF_8).use {
|
||||
return GsonConst.botConfigGson.fromJson(it, object : TypeToken<Set<BotConfig>>() {}.type)!!
|
||||
return GsonConst.botConfigGson.fromJson(it, JsonArray::class.java)!!
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.error(e) { "读取 Bot 配置文件 (bot.json) 时发生错误, 请检查配置格式是否正确." }
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
package net.lamgc.scalabot
|
||||
|
||||
import com.google.gson.JsonParseException
|
||||
import io.prometheus.client.exporter.HTTPServer
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mu.KotlinLogging
|
||||
import net.lamgc.scalabot.config.*
|
||||
import net.lamgc.scalabot.util.registerShutdownHook
|
||||
import org.eclipse.aether.repository.LocalRepository
|
||||
import org.telegram.telegrambots.bots.DefaultBotOptions
|
||||
@ -25,11 +27,13 @@ fun main(args: Array<String>): Unit = runBlocking {
|
||||
log.info { "ScalaBot 正在启动中..." }
|
||||
log.info { "数据目录: ${AppPaths.DATA_ROOT}" }
|
||||
log.debug { "启动参数: ${args.joinToString(prefix = "[", postfix = "]")}" }
|
||||
initialFiles()
|
||||
if (initialFiles()) {
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
val launcher = Launcher()
|
||||
.registerShutdownHook()
|
||||
startMetricsServer()
|
||||
startMetricsServer()?.registerShutdownHook()
|
||||
if (!launcher.launch()) {
|
||||
exitProcess(1)
|
||||
}
|
||||
@ -39,21 +43,22 @@ fun main(args: Array<String>): Unit = runBlocking {
|
||||
* 启动运行指标服务器.
|
||||
* 使用 Prometheus 指标格式.
|
||||
*/
|
||||
internal fun startMetricsServer(config: MetricsConfig = Const.config.metrics) {
|
||||
internal fun startMetricsServer(config: MetricsConfig = Const.config.metrics): HTTPServer? {
|
||||
if (!config.enable) {
|
||||
log.debug { "运行指标服务器已禁用." }
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
val builder = HTTPServer.Builder()
|
||||
.withDaemonThreads(true)
|
||||
.withAuthenticator(config.authenticator)
|
||||
.withPort(config.port)
|
||||
.withHostname(config.bindAddress)
|
||||
|
||||
val httpServer = builder
|
||||
.build()
|
||||
.registerShutdownHook()
|
||||
log.info { "运行指标服务器已启动. (Port: ${httpServer.port})" }
|
||||
return httpServer
|
||||
}
|
||||
|
||||
internal class Launcher(private val config: AppConfig = Const.config) : AutoCloseable {
|
||||
@ -69,9 +74,9 @@ internal class Launcher(private val config: AppConfig = Const.config) : AutoClos
|
||||
|
||||
private fun getMavenLocalRepository(): LocalRepository {
|
||||
val localPath =
|
||||
if (config.mavenLocalRepository != null && config.mavenLocalRepository.isNotEmpty()) {
|
||||
if (config.mavenLocalRepository != null && config.mavenLocalRepository!!.isNotEmpty()) {
|
||||
val repoPath = AppPaths.DATA_ROOT.file.toPath()
|
||||
.resolve(config.mavenLocalRepository)
|
||||
.resolve(config.mavenLocalRepository!!)
|
||||
.apply {
|
||||
if (!exists()) {
|
||||
if (!parent.isWritable() || !parent.isReadable()) {
|
||||
@ -80,17 +85,14 @@ internal class Launcher(private val config: AppConfig = Const.config) : AutoClos
|
||||
if (System.getProperty("os.name").lowercase().startsWith("windows")) {
|
||||
createDirectories()
|
||||
} else {
|
||||
createDirectories(
|
||||
PosixFilePermissions.asFileAttribute(
|
||||
setOf(
|
||||
val fileAttributes = setOf(
|
||||
PosixFilePermission.OWNER_READ,
|
||||
PosixFilePermission.OWNER_WRITE,
|
||||
PosixFilePermission.GROUP_READ,
|
||||
PosixFilePermission.GROUP_WRITE,
|
||||
PosixFilePermission.OTHERS_READ,
|
||||
)
|
||||
)
|
||||
)
|
||||
createDirectories(PosixFilePermissions.asFileAttribute(fileAttributes))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -108,22 +110,39 @@ internal class Launcher(private val config: AppConfig = Const.config) : AutoClos
|
||||
|
||||
@Synchronized
|
||||
fun launch(): Boolean {
|
||||
val botConfigs = loadBotConfig()
|
||||
if (botConfigs.isEmpty()) {
|
||||
val botConfigs = loadBotConfigJson() ?: return false
|
||||
if (botConfigs.isEmpty) {
|
||||
log.warn { "尚未配置任何机器人, 请先配置机器人后再启动本程序." }
|
||||
return false
|
||||
} else if (botConfigs.none { it.enabled }) {
|
||||
log.warn { "配置文件中没有已启用的机器人, 请至少启用一个机器人." }
|
||||
return false
|
||||
}
|
||||
for (botConfig in botConfigs) {
|
||||
var launchedCounts = 0
|
||||
for (botConfigJson in botConfigs) {
|
||||
val botConfig = try {
|
||||
GsonConst.botConfigGson.fromJson(botConfigJson, BotConfig::class.java)
|
||||
} catch (e: JsonParseException) {
|
||||
val botName = try {
|
||||
botConfigJson.asJsonObject.get("account")?.asJsonObject?.get("name")?.asString ?: "Unknown"
|
||||
} catch (e: Exception) {
|
||||
"Unknown"
|
||||
}
|
||||
log.error(e) { "机器人 `$botName` 配置有误, 跳过该机器人的启动." }
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
launchBot(botConfig)
|
||||
launchedCounts++
|
||||
} catch (e: Exception) {
|
||||
log.error(e) { "机器人 `${botConfig.account.name}` 启动时发生错误." }
|
||||
}
|
||||
}
|
||||
return true
|
||||
return if (launchedCounts != 0) {
|
||||
log.info { "已启动 $launchedCounts 个机器人." }
|
||||
true
|
||||
} else {
|
||||
log.warn { "未启动任何机器人, 请检查配置并至少启用一个机器人." }
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchBot(botConfig: BotConfig) {
|
||||
@ -132,36 +151,38 @@ internal class Launcher(private val config: AppConfig = Const.config) : AutoClos
|
||||
return
|
||||
}
|
||||
log.info { "正在启动机器人 `${botConfig.account.name}`..." }
|
||||
val botOption = DefaultBotOptions().apply {
|
||||
val proxyConfig =
|
||||
if (botConfig.proxy != null && botConfig.proxy.type != DefaultBotOptions.ProxyType.NO_PROXY) {
|
||||
if (botConfig.proxy.type != ProxyType.NO_PROXY) {
|
||||
log.debug { "[Bot ${botConfig.account.name}] 使用独立代理: ${botConfig.proxy.type}" }
|
||||
botConfig.proxy
|
||||
} else if (config.proxy.type != DefaultBotOptions.ProxyType.NO_PROXY) {
|
||||
} else if (config.proxy.type != ProxyType.NO_PROXY) {
|
||||
log.debug { "[Bot ${botConfig.account.name}] 使用全局代理: ${botConfig.proxy.type}" }
|
||||
config.proxy
|
||||
} else {
|
||||
null
|
||||
log.debug { "[Bot ${botConfig.account.name}] 不使用代理." }
|
||||
ProxyConfig(type = ProxyType.NO_PROXY)
|
||||
}
|
||||
if (proxyConfig != null) {
|
||||
proxyType = proxyConfig.type
|
||||
|
||||
val botOption = DefaultBotOptions().apply {
|
||||
if (proxyConfig.type != ProxyType.NO_PROXY) {
|
||||
proxyType = proxyConfig.type.toTelegramBotsType()
|
||||
proxyHost = config.proxy.host
|
||||
proxyPort = config.proxy.port
|
||||
log.debug { "机器人 `${botConfig.account.name}` 已启用代理配置: $proxyConfig" }
|
||||
}
|
||||
|
||||
if (botConfig.baseApiUrl != null) {
|
||||
baseUrl = botConfig.baseApiUrl
|
||||
}
|
||||
}
|
||||
val account = botConfig.account
|
||||
|
||||
val remoteRepositories = config.mavenRepositories
|
||||
.map { it.toRemoteRepository(config.proxy) }
|
||||
.map { it.toRemoteRepository(proxyConfig) }
|
||||
.toMutableList().apply {
|
||||
if (this.none {
|
||||
it.url == MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL
|
||||
|| it.url == MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL.trimEnd('/')
|
||||
}) {
|
||||
add(MavenRepositoryExtensionFinder.getMavenCentralRepository(proxy = config.proxy.toAetherProxy()))
|
||||
add(MavenRepositoryExtensionFinder.getMavenCentralRepository(proxy = proxyConfig.toAetherProxy()))
|
||||
}
|
||||
}.toList()
|
||||
val extensionPackageFinders = setOf(
|
||||
|
@ -2,6 +2,7 @@ package net.lamgc.scalabot
|
||||
|
||||
import com.google.common.io.Files
|
||||
import mu.KotlinLogging
|
||||
import net.lamgc.scalabot.config.BotAccount
|
||||
import net.lamgc.scalabot.util.toHexString
|
||||
import org.mapdb.DB
|
||||
import org.mapdb.DBException
|
||||
@ -13,10 +14,11 @@ import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* 数据库适配器.
|
||||
*
|
||||
* 数据库适配器列表.
|
||||
* 应按照新到旧的顺序放置, 新的适配器应该在上面.
|
||||
* @suppress 由于本列表需要设置已弃用的适配器以保证旧版数据库的正常使用, 故忽略弃用警告.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
private val adapters = arrayListOf<DbAdapter>(
|
||||
BotAccountIdDbAdapter, // since [v0.2.0 ~ latest)
|
||||
BotTokenDbAdapter // since [v0.0.1 ~ v0.2.0)
|
||||
@ -183,6 +185,7 @@ private object BotAccountIdDbAdapter : FileDbAdapter("BotAccountId", { botAccoun
|
||||
*
|
||||
* **已弃用**: 由于 Token 可以重新生成, 当 Token 改变后数据库文件名也会改变, 故弃用该方法.
|
||||
*/
|
||||
@Deprecated(message = "由于 BotToken 可变, 故不再使用该适配器.", level = DeprecationLevel.WARNING)
|
||||
private object BotTokenDbAdapter : FileDbAdapter("BotToken_v0.1.0", { botAccount ->
|
||||
val digest: MessageDigest = MessageDigest.getInstance("SHA-256")
|
||||
val digestBytes = digest.digest(botAccount.token.toByteArray(StandardCharsets.UTF_8))
|
||||
|
@ -14,6 +14,18 @@ import java.nio.charset.StandardCharsets
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
* 扩展加载器.
|
||||
*
|
||||
* 扩展加载器并非负责加载扩展的 Class, 而是委派搜索器发现并获取扩展, 然后加载扩展实例.
|
||||
*
|
||||
* 注意, 扩展加载器将内置两个 Finder: [FileNameFinder] 和 [MavenMetaInformationFinder].
|
||||
*
|
||||
* @param bot 扩展加载器所负责的 ScalaBot 实例.
|
||||
* @param extensionsDataFolder 提供给扩展用于数据存储的根目录(实际目录为 `{root}/{group...}/{artifact}`).
|
||||
* @param extensionsPath 提供给 Finder 用于搜索扩展的本地扩展包存放路径.
|
||||
* @param extensionFinders 加载器所使用的搜索器集合. 加载扩展时将使用所提供的的加载器.
|
||||
*/
|
||||
internal class ExtensionLoader(
|
||||
private val bot: ScalaBot,
|
||||
private val extensionsDataFolder: File = AppPaths.DATA_EXTENSIONS.file,
|
||||
@ -27,6 +39,13 @@ internal class ExtensionLoader(
|
||||
MavenMetaInformationFinder
|
||||
).apply { addAll(extensionFinders) }.toSet()
|
||||
|
||||
/**
|
||||
* 加载扩展, 并返回扩展项.
|
||||
*
|
||||
* 调用本方法后, 将会指派提供的 Finder 搜索 ScalaBot 配置的扩展包.
|
||||
*
|
||||
* @return 返回存放了所有已加载扩展项的 Set. 可通过 [LoadedExtensionEntry] 获取扩展的有关信息.
|
||||
*/
|
||||
fun getExtensions(): Set<LoadedExtensionEntry> {
|
||||
val extensionEntries = mutableSetOf<LoadedExtensionEntry>()
|
||||
for (extensionArtifact in bot.extensions) {
|
||||
@ -52,6 +71,17 @@ internal class ExtensionLoader(
|
||||
|
||||
/**
|
||||
* 检查是否发生冲突.
|
||||
*
|
||||
* 扩展包冲突有两种情况:
|
||||
* 1. 有多个同为最高优先级的搜索器搜索到了扩展包.
|
||||
* 2. 唯一的最高优先级搜索器搜索到了多个扩展包.
|
||||
*
|
||||
* 扩展包冲突指的是**有多个具有相同构件坐标的扩展包被搜索到**,
|
||||
* 如果不顾扩展包冲突直接加载的话, 将会出现安全隐患,
|
||||
* 因此在加载器发现冲突的情况下将输出相关信息, 提示用户进行排查.
|
||||
*
|
||||
* @param foundResult 扩展包搜索结果.
|
||||
*
|
||||
* @return 如果出现冲突, 返回 `true`.
|
||||
*/
|
||||
private fun checkConflict(foundResult: Map<ExtensionPackageFinder, Set<FoundExtensionPackage>>): Boolean {
|
||||
@ -68,6 +98,9 @@ internal class ExtensionLoader(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从结果中过滤出由最高优先级的搜索器搜索到的扩展包.
|
||||
*/
|
||||
private fun filterHighPriorityResult(foundResult: Map<ExtensionPackageFinder, Set<FoundExtensionPackage>>)
|
||||
: Map<ExtensionPackageFinder, Set<FoundExtensionPackage>> {
|
||||
val finders: List<ExtensionPackageFinder> = foundResult.keys
|
||||
@ -102,6 +135,11 @@ internal class ExtensionLoader(
|
||||
return factories.toSet()
|
||||
}
|
||||
|
||||
/**
|
||||
* 只是用来统计扩展包搜索结果的数量而已.
|
||||
*
|
||||
* @return 返回扩展包的数量.
|
||||
*/
|
||||
private fun allFoundedPackageNumber(filesMap: Map<ExtensionPackageFinder, Set<FoundExtensionPackage>>): Int {
|
||||
var number = 0
|
||||
for (files in filesMap.values) {
|
||||
@ -110,6 +148,14 @@ internal class ExtensionLoader(
|
||||
return number
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索指定构件坐标的依赖包.
|
||||
*
|
||||
* 搜索扩展包将根据搜索器优先级从高到低依次搜索, 当某一个优先级的搜索器搜到扩展包后将停止搜索.
|
||||
* 可以根据不同优先级的搜索器, 配置扩展包的主用与备用文件.
|
||||
*
|
||||
* @return 返回各个搜索器返回的搜索结果.
|
||||
*/
|
||||
private fun findExtensionPackage(
|
||||
extensionArtifact: Artifact,
|
||||
): Map<ExtensionPackageFinder, Set<FoundExtensionPackage>> {
|
||||
@ -138,13 +184,21 @@ internal class ExtensionLoader(
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查扩展包搜索器是否设置了 [FinderRules] 注解.
|
||||
* @return 如果已设置注解, 则返回 `true`.
|
||||
*/
|
||||
private fun checkExtensionPackageFinder(finder: ExtensionPackageFinder): Boolean =
|
||||
finder::class.java.getDeclaredAnnotation(FinderRules::class.java) != null
|
||||
|
||||
/**
|
||||
* 在日志中输出有关扩展包冲突的错误信息.
|
||||
*/
|
||||
private fun printExtensionFileConflictError(
|
||||
extensionArtifact: Artifact,
|
||||
foundResult: Map<ExtensionPackageFinder, Set<FoundExtensionPackage>>
|
||||
) {
|
||||
log.error {
|
||||
val errMessage = StringBuilder(
|
||||
"""
|
||||
[Bot ${bot.botUsername}] 扩展包 $extensionArtifact 存在多个文件, 为防止安全问题, 已禁止加载该扩展包:
|
||||
@ -160,9 +214,15 @@ internal class ExtensionLoader(
|
||||
.append(URLDecoder.decode(file.getRawUrl().toString(), StandardCharsets.UTF_8)).append('\n')
|
||||
}
|
||||
}
|
||||
log.error { errMessage }
|
||||
errMessage
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建扩展数据目录, 并返回 [File] 对象.
|
||||
* @param extensionArtifact 扩展包构件坐标.
|
||||
* @return 返回对应的数据存储目录.
|
||||
*/
|
||||
private fun getExtensionDataFolder(extensionArtifact: Artifact): File {
|
||||
val dataFolder =
|
||||
File(extensionsDataFolder, "${extensionArtifact.groupId}/${extensionArtifact.artifactId}")
|
||||
@ -172,6 +232,12 @@ internal class ExtensionLoader(
|
||||
return dataFolder
|
||||
}
|
||||
|
||||
/**
|
||||
* 已加载扩展项.
|
||||
* @property extensionArtifact 扩展的构件坐标([Artifact]).
|
||||
* @property factoryClass 扩展的工厂类.
|
||||
* @property extension 扩展实例.
|
||||
*/
|
||||
data class LoadedExtensionEntry(
|
||||
val extensionArtifact: Artifact,
|
||||
val factoryClass: Class<out BotExtensionFactory>,
|
||||
@ -181,6 +247,10 @@ internal class ExtensionLoader(
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展的类加载器清除器.
|
||||
*
|
||||
* 原计划是用来通过关闭 ClassLoader 来卸载扩展的, 但似乎并没有这么做.
|
||||
*
|
||||
* 该类为保留措施, 尚未启用.
|
||||
*/
|
||||
internal object ExtensionClassLoaderCleaner {
|
||||
@ -257,7 +327,7 @@ internal interface ExtensionPackageFinder {
|
||||
|
||||
/**
|
||||
* 已找到的扩展包信息.
|
||||
* 通过实现该接口, 以寻找远端文件的 [ExtensionPackageFinder]
|
||||
* 通过实现该接口, 以寻找远端文件的 [ExtensionPackageFinder];
|
||||
* 可以在适当的时候将扩展包下载到本地, 而无需在搜索阶段下载扩展包.
|
||||
*/
|
||||
internal interface FoundExtensionPackage {
|
||||
@ -296,6 +366,7 @@ private fun FoundExtensionPackage.createClassLoader(): ExtensionClassLoader =
|
||||
* 已找到的扩展包文件.
|
||||
* @param artifact 扩展包构件坐标.
|
||||
* @param file 已找到的扩展包文件.
|
||||
* @param finder 搜索到该扩展包的搜索器.
|
||||
*/
|
||||
internal class FileFoundExtensionPackage(
|
||||
private val artifact: Artifact,
|
||||
@ -342,14 +413,14 @@ internal class ExtensionClassLoader(urls: Array<URL>, dependencyLoader: ClassLoa
|
||||
// 以免使用了不来自扩展包的机器人扩展.
|
||||
|
||||
override fun getResources(name: String?): Enumeration<URL> {
|
||||
if (BotExtensionFactory::class.java.equals(name)) {
|
||||
if ("META-INF/services/${BotExtensionFactory::class.java.name}" == name) {
|
||||
return findResources(name)
|
||||
}
|
||||
return super.getResources(name)
|
||||
}
|
||||
|
||||
override fun getResource(name: String?): URL? {
|
||||
if (BotExtensionFactory::class.java.equals(name)) {
|
||||
if ("META-INF/services/${BotExtensionFactory::class.java}" == name) {
|
||||
return findResource(name)
|
||||
}
|
||||
return super.getResource(name)
|
||||
|
@ -257,23 +257,25 @@ internal class MavenRepositoryExtensionFinder(
|
||||
}
|
||||
|
||||
override fun findByArtifact(extensionArtifact: Artifact, extensionsPath: File): Set<FoundExtensionPackage> {
|
||||
val repositories = repositorySystem.newResolutionRepositories(repoSystemSession, remoteRepositories).toList()
|
||||
log.debug {
|
||||
StringBuilder().apply {
|
||||
append("构件 $extensionArtifact 将在以下仓库拉取: \n")
|
||||
remoteRepositories.forEach {
|
||||
append("\t- ${it}\n")
|
||||
repositories.forEach {
|
||||
append("\t- $it\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val extensionArtifactResult = repositorySystem.resolveArtifact(
|
||||
repoSystemSession,
|
||||
ArtifactRequest(
|
||||
extensionArtifact,
|
||||
repositorySystem.newResolutionRepositories(repoSystemSession, remoteRepositories),
|
||||
repositories,
|
||||
null
|
||||
)
|
||||
)
|
||||
val extResolvedArtifact = extensionArtifactResult.artifact
|
||||
val resolvedArtifact: Artifact? = extensionArtifactResult.artifact
|
||||
if (!extensionArtifactResult.isResolved) {
|
||||
if (extensionArtifactResult.isMissing) {
|
||||
log.warn { "在指定的仓库中找不到构件: ${extensionArtifactResult.artifact}" }
|
||||
@ -281,17 +283,26 @@ internal class MavenRepositoryExtensionFinder(
|
||||
printArtifactResultExceptions(extensionArtifactResult.exceptions)
|
||||
}
|
||||
return emptySet()
|
||||
} else if (resolvedArtifact == null) {
|
||||
log.warn { "无法在指定的仓库中解析构件: $extensionArtifact" }
|
||||
return emptySet()
|
||||
}
|
||||
|
||||
log.info {
|
||||
"已从 Maven 仓库 `${extensionArtifactResult.repository.id}` 中找到" +
|
||||
"扩展包 `${resolvedArtifact.groupId}:${resolvedArtifact.artifactId}` " +
|
||||
"版本号 `${resolvedArtifact.version}`."
|
||||
}
|
||||
|
||||
val request = DependencyRequest(
|
||||
CollectRequest(Dependency(extResolvedArtifact, null), remoteRepositories),
|
||||
CollectRequest(Dependency(resolvedArtifact, null), repositories),
|
||||
ScopeDependencyFilter(setOf("runtime", "compile", "provided"), null)
|
||||
)
|
||||
val dependencyResult = repositorySystem.resolveDependencies(repoSystemSession, request)
|
||||
val dependencies = checkAndCollectDependencyArtifacts(extensionArtifact, dependencyResult.artifactResults)
|
||||
?: return emptySet()
|
||||
|
||||
return setOf(MavenExtensionPackage(this, extResolvedArtifact, extensionArtifactResult.repository, dependencies))
|
||||
return setOf(MavenExtensionPackage(this, resolvedArtifact, extensionArtifactResult.repository, dependencies))
|
||||
}
|
||||
|
||||
private fun checkAndCollectDependencyArtifacts(
|
||||
|
@ -4,6 +4,7 @@ import io.prometheus.client.Counter
|
||||
import io.prometheus.client.Gauge
|
||||
import io.prometheus.client.Summary
|
||||
import mu.KotlinLogging
|
||||
import net.lamgc.scalabot.config.BotConfig
|
||||
import org.eclipse.aether.artifact.Artifact
|
||||
import org.telegram.abilitybots.api.bot.AbilityBot
|
||||
import org.telegram.abilitybots.api.db.DBContext
|
||||
|
@ -1,168 +0,0 @@
|
||||
package net.lamgc.scalabot.util
|
||||
|
||||
import com.google.gson.*
|
||||
import mu.KotlinLogging
|
||||
import net.lamgc.scalabot.MavenRepositoryConfig
|
||||
import org.eclipse.aether.artifact.Artifact
|
||||
import org.eclipse.aether.artifact.DefaultArtifact
|
||||
import org.eclipse.aether.repository.Authentication
|
||||
import org.eclipse.aether.repository.Proxy
|
||||
import org.eclipse.aether.util.repository.AuthenticationBuilder
|
||||
import org.telegram.telegrambots.bots.DefaultBotOptions
|
||||
import java.lang.reflect.Type
|
||||
import java.net.URL
|
||||
|
||||
internal object ProxyTypeSerializer : JsonDeserializer<DefaultBotOptions.ProxyType>,
|
||||
JsonSerializer<DefaultBotOptions.ProxyType> {
|
||||
|
||||
override fun deserialize(
|
||||
json: JsonElement,
|
||||
typeOfT: Type?,
|
||||
context: JsonDeserializationContext?
|
||||
): DefaultBotOptions.ProxyType {
|
||||
if (!json.isJsonPrimitive) {
|
||||
throw JsonParseException("Wrong configuration value type.")
|
||||
}
|
||||
val value = json.asString.trim()
|
||||
try {
|
||||
return DefaultBotOptions.ProxyType.valueOf(value.uppercase())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw JsonParseException("Invalid value: $value")
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(
|
||||
src: DefaultBotOptions.ProxyType,
|
||||
typeOfSrc: Type?,
|
||||
context: JsonSerializationContext?
|
||||
): JsonElement {
|
||||
return JsonPrimitive(src.toString())
|
||||
}
|
||||
}
|
||||
|
||||
internal object ArtifactSerializer : JsonSerializer<Artifact>, JsonDeserializer<Artifact> {
|
||||
override fun serialize(src: Artifact, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
|
||||
val gavBuilder = StringBuilder("${src.groupId}:${src.artifactId}")
|
||||
if (!src.extension.equals("jar")) {
|
||||
gavBuilder.append(':').append(src.extension)
|
||||
}
|
||||
if (src.classifier.isNotEmpty()) {
|
||||
gavBuilder.append(':').append(src.classifier)
|
||||
}
|
||||
return JsonPrimitive(gavBuilder.append(':').append(src.version).toString())
|
||||
}
|
||||
|
||||
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Artifact {
|
||||
if (!json!!.isJsonPrimitive) {
|
||||
throw JsonParseException("Wrong configuration value type.")
|
||||
}
|
||||
return DefaultArtifact(json.asString.trim())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal object AuthenticationSerializer : JsonDeserializer<Authentication> {
|
||||
|
||||
private val log = KotlinLogging.logger { }
|
||||
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Authentication? {
|
||||
val builder = AuthenticationBuilder()
|
||||
when (json) {
|
||||
is JsonArray -> {
|
||||
for (element in json) {
|
||||
if (element is JsonArray) {
|
||||
builder.addCustom(jsonArrayToAuthentication(element))
|
||||
} else if (element is JsonObject) {
|
||||
jsonToAuthentication(element, builder)
|
||||
}
|
||||
}
|
||||
}
|
||||
is JsonObject -> {
|
||||
jsonToAuthentication(json, builder)
|
||||
}
|
||||
else -> {
|
||||
throw JsonParseException("Unsupported JSON data type: ${json::class.java}")
|
||||
}
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun jsonArrayToAuthentication(jsonArray: JsonArray): Authentication {
|
||||
val builder = AuthenticationBuilder()
|
||||
for (element in jsonArray) {
|
||||
when (element) {
|
||||
is JsonObject -> jsonToAuthentication(element, builder)
|
||||
is JsonArray -> builder.addCustom(jsonArrayToAuthentication(element))
|
||||
else -> log.warn { "不支持的 Json 类型: ${element::class.java}" }
|
||||
}
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private const val KEY_TYPE = "type"
|
||||
|
||||
private fun jsonToAuthentication(json: JsonObject, builder: AuthenticationBuilder) {
|
||||
if (!json.has(KEY_TYPE)) {
|
||||
log.warn { "缺少 type 字段, 无法判断 Maven 认证信息类型." }
|
||||
return
|
||||
} else if (!json.get(KEY_TYPE).isJsonPrimitive) {
|
||||
log.warn { "type 字段类型错误(应为 Primitive 类型), 无法判断 Maven 认证信息类型.(实际类型: `${json::class.java}`)" }
|
||||
return
|
||||
}
|
||||
|
||||
when (json.get(KEY_TYPE).asString.trim().lowercase()) {
|
||||
"string" -> {
|
||||
builder.addString(checkJsonKey(json, "key"), checkJsonKey(json, "value"))
|
||||
}
|
||||
"secret" -> {
|
||||
builder.addSecret(checkJsonKey(json, "key"), checkJsonKey(json, "value"))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkJsonKey(json: JsonObject, key: String): String {
|
||||
if (!json.has(key)) {
|
||||
throw JsonParseException("Required field does not exist: $key")
|
||||
} else if (!json.get(key).isJsonPrimitive) {
|
||||
throw JsonParseException("Wrong field `$key` type: ${json.get(key)::class.java}")
|
||||
}
|
||||
return json.get(key).asString
|
||||
}
|
||||
|
||||
internal object MavenRepositoryConfigSerializer
|
||||
: JsonDeserializer<MavenRepositoryConfig> {
|
||||
|
||||
override fun deserialize(
|
||||
json: JsonElement,
|
||||
typeOfT: Type,
|
||||
context: JsonDeserializationContext
|
||||
): MavenRepositoryConfig {
|
||||
return when (json) {
|
||||
is JsonObject -> {
|
||||
MavenRepositoryConfig(
|
||||
id = json.get("id")?.asString,
|
||||
url = URL(checkJsonKey(json, "url")),
|
||||
proxy = if (json.has("proxy") && json.get("proxy").isJsonObject)
|
||||
context.deserialize<Proxy>(
|
||||
json.getAsJsonObject("proxy"), Proxy::class.java
|
||||
) else null,
|
||||
layout = json.get("layout").asString ?: "default",
|
||||
enableReleases = json.get("enableReleases")?.asBoolean ?: true,
|
||||
enableSnapshots = json.get("enableSnapshots")?.asBoolean ?: true,
|
||||
authentication = if (json.has("authentication") && json.get("authentication").isJsonObject)
|
||||
context.deserialize<Authentication>(
|
||||
json.getAsJsonObject("authentication"), Authentication::class.java
|
||||
) else null
|
||||
)
|
||||
}
|
||||
is JsonPrimitive -> {
|
||||
MavenRepositoryConfig(url = URL(json.asString))
|
||||
}
|
||||
else -> {
|
||||
throw JsonParseException("Unsupported Maven warehouse configuration type.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -10,16 +10,14 @@ import java.io.FilenameFilter
|
||||
|
||||
internal fun ByteArray.toHexString(): String = joinToString("") { it.toString(16) }
|
||||
|
||||
internal fun Artifact.equalsArtifact(that: Artifact): Boolean =
|
||||
internal fun Artifact.equalsArtifact(that: Artifact, checkProperties: Boolean = false): Boolean =
|
||||
this.groupId.equals(that.groupId) &&
|
||||
this.artifactId.equals(that.artifactId) &&
|
||||
this.version.equals(that.version) &&
|
||||
this.baseVersion.equals(that.baseVersion) &&
|
||||
this.isSnapshot == that.isSnapshot &&
|
||||
this.classifier.equals(that.classifier) &&
|
||||
this.extension.equals(that.extension) &&
|
||||
(if (this.file == null) that.file == null else this.file.equals(that.file)) &&
|
||||
this.properties.equals(that.properties)
|
||||
(!checkProperties || this.properties.equals(that.properties))
|
||||
|
||||
internal fun File.deepListFiles(
|
||||
addSelf: Boolean = false,
|
||||
@ -27,13 +25,13 @@ internal fun File.deepListFiles(
|
||||
fileFilter: FileFilter? = null,
|
||||
filenameFilter: FilenameFilter? = null
|
||||
): Array<File>? {
|
||||
val files = if (fileFilter != null) {
|
||||
val files = (if (fileFilter != null) {
|
||||
this.listFiles(fileFilter)
|
||||
} else if (filenameFilter != null) {
|
||||
this.listFiles(filenameFilter)
|
||||
} else {
|
||||
this.listFiles()
|
||||
} ?: return null
|
||||
}) ?: return null
|
||||
|
||||
val result = if (addSelf) mutableSetOf(this) else mutableSetOf()
|
||||
for (file in files) {
|
||||
@ -73,11 +71,10 @@ fun <T : AutoCloseable> T.registerShutdownHook(): T {
|
||||
return this
|
||||
}
|
||||
|
||||
private val log = KotlinLogging.logger { }
|
||||
|
||||
private object UtilsInternal {
|
||||
|
||||
val autoCloseableSet = mutableSetOf<AutoCloseable>()
|
||||
private val log = KotlinLogging.logger(UtilsInternal::class.java.name)
|
||||
|
||||
init {
|
||||
Runtime.getRuntime().addShutdownHook(Thread(this::doCloseResources, "Shutdown-AutoCloseable"))
|
||||
|
@ -24,7 +24,7 @@
|
||||
<appender name="FILE_OUT" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${DATA_LOGS}/latest.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>data/logs/%d{yyyy-MM-dd}.log.gz</fileNamePattern>
|
||||
<fileNamePattern>${DATA_LOGS}/%d{yyyy-MM-dd}.log.gz</fileNamePattern>
|
||||
<maxHistory>30</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
|
@ -1,62 +1,60 @@
|
||||
package net.lamgc.scalabot
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.github.stefanbirkner.systemlambda.SystemLambda
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import mu.KotlinLogging
|
||||
import net.lamgc.scalabot.config.MavenRepositoryConfig
|
||||
import net.lamgc.scalabot.config.ProxyConfig
|
||||
import net.lamgc.scalabot.config.ProxyType
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import org.telegram.telegrambots.bots.DefaultBotOptions
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
internal class BotAccountTest {
|
||||
|
||||
@Test
|
||||
fun deserializerTest() {
|
||||
val accountId = abs(Random().nextInt()).toLong()
|
||||
val creatorId = abs(Random().nextInt()).toLong()
|
||||
val botAccount = Gson().fromJson(
|
||||
"""
|
||||
{
|
||||
"name": "TestBot",
|
||||
"token": "${accountId}:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10",
|
||||
"creatorId": $creatorId
|
||||
}
|
||||
""".trimIndent(), BotAccount::class.java
|
||||
)
|
||||
assertEquals("TestBot", botAccount.name)
|
||||
assertEquals("${accountId}:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10", botAccount.token)
|
||||
assertEquals(accountId, botAccount.id, "Botaccount ID does not match expectations.")
|
||||
assertEquals(creatorId, botAccount.creatorId)
|
||||
}
|
||||
|
||||
}
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.deleteExisting
|
||||
import kotlin.test.*
|
||||
|
||||
internal class AppPathsTest {
|
||||
|
||||
@Test
|
||||
fun `Data root path priority`() {
|
||||
System.setProperty("bot.path.data", "A")
|
||||
|
||||
assertEquals("A", AppPaths.DATA_ROOT.file.path, "`DATA_ROOT`没有优先返回 Property 的值.")
|
||||
System.getProperties().remove("bot.path.data")
|
||||
if (System.getenv("BOT_DATA_PATH") != null) {
|
||||
fun `Consistency check`() {
|
||||
for (path in AppPaths.values()) {
|
||||
assertEquals(
|
||||
System.getenv("BOT_DATA_PATH"), AppPaths.DATA_ROOT.file.path,
|
||||
"`DATA_ROOT`没有返回 env 的值."
|
||||
File(path.path).canonicalPath,
|
||||
path.file.canonicalPath,
|
||||
"路径 File 与 Path 不一致: ${path.name}"
|
||||
)
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Data root path priority`() {
|
||||
System.setProperty(AppPaths.PathConst.PROP_DATA_PATH, "fromSystemProperties")
|
||||
|
||||
assertEquals("fromSystemProperties", AppPaths.DATA_ROOT.file.path, "`DATA_ROOT`没有优先返回 Property 的值.")
|
||||
System.getProperties().remove(AppPaths.PathConst.PROP_DATA_PATH)
|
||||
|
||||
val expectEnvValue = "fromEnvironmentVariable"
|
||||
SystemLambda.withEnvironmentVariable(AppPaths.PathConst.ENV_DATA_PATH, expectEnvValue).execute {
|
||||
assertEquals(
|
||||
expectEnvValue, AppPaths.DATA_ROOT.file.path,
|
||||
"`DATA_ROOT`没有优先返回 env 的值."
|
||||
)
|
||||
}
|
||||
|
||||
SystemLambda.withEnvironmentVariable(AppPaths.PathConst.ENV_DATA_PATH, null).execute {
|
||||
assertEquals(
|
||||
System.getProperty("user.dir"), AppPaths.DATA_ROOT.file.path,
|
||||
"`DATA_ROOT`没有返回 `user.dir` 的值."
|
||||
"`DATA_ROOT`没有返回 System.properties `user.dir` 的值."
|
||||
)
|
||||
val userDir = System.getProperty("user.dir")
|
||||
System.getProperties().remove("user.dir")
|
||||
assertEquals(".", AppPaths.DATA_ROOT.file.path, "`DATA_ROOT`没有返回 `.`(当前目录).")
|
||||
assertEquals(".", AppPaths.DATA_ROOT.file.path, "`DATA_ROOT`没有返回替补值 `.`(当前目录).")
|
||||
System.setProperty("user.dir", userDir)
|
||||
assertNotNull(System.getProperty("user.dir"), "环境还原失败!")
|
||||
}
|
||||
@ -115,8 +113,304 @@ internal class AppPathsTest {
|
||||
verify(exactly = 0) { mkdirs() }
|
||||
}
|
||||
|
||||
mockk<File> {
|
||||
every { exists() }.returns(false)
|
||||
every { canonicalPath }.answers { alreadyExistsFile.canonicalPath }
|
||||
every { createNewFile() }.answers { false }
|
||||
every { mkdirs() }.answers { false }
|
||||
every { mkdir() }.answers { false }
|
||||
}.apply {
|
||||
mockk<AppPaths> {
|
||||
every { file }.returns(this@apply)
|
||||
every { path }.returns(this@apply.canonicalPath)
|
||||
every { initial() }.answers {
|
||||
defaultInitializerMethod.invoke(null, this@mockk)
|
||||
}
|
||||
}.initial()
|
||||
verify(exactly = 1) { createNewFile() }
|
||||
verify(exactly = 0) { mkdir() }
|
||||
verify(exactly = 0) { mkdirs() }
|
||||
}
|
||||
|
||||
defaultInitializerMethod.isAccessible = false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadBotConfig test`(@TempDir testDir: File) {
|
||||
assertNull(loadBotConfigJson(File("/NOT_EXISTS_FILE")), "加载 BotConfigs 失败时应该返回 null.")
|
||||
|
||||
SystemLambda.withEnvironmentVariable(AppPaths.PathConst.ENV_DATA_PATH, testDir.canonicalPath).execute {
|
||||
assertNull(loadBotConfigJson(), "加载 BotConfigs 失败时应该返回 null.")
|
||||
|
||||
File(testDir, "bot.json").apply {
|
||||
//language=JSON5
|
||||
writeText(
|
||||
"""
|
||||
[
|
||||
{
|
||||
"enabled": false,
|
||||
"account": {
|
||||
"name": "TestBot",
|
||||
"token": "123456789:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10",
|
||||
"creatorId": 123456789
|
||||
},
|
||||
"proxy": {
|
||||
"host": "localhost",
|
||||
"port": 8080,
|
||||
"type": "HTTP"
|
||||
},
|
||||
"disableBuiltInAbility": false,
|
||||
"autoUpdateCommandList": true,
|
||||
"extensions": [
|
||||
"org.example.test:test-extension:1.0.0"
|
||||
],
|
||||
"baseApiUrl": "http://localhost:8080"
|
||||
}
|
||||
]
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
val botConfigJsons = loadBotConfigJson()
|
||||
assertNotNull(botConfigJsons)
|
||||
assertEquals(1, botConfigJsons.size())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadAppConfig test`(@TempDir testDir: File) {
|
||||
assertThrows<IOException>("加载失败时应该抛出 IOException.") {
|
||||
loadAppConfig(File("/NOT_EXISTS_FILE"))
|
||||
}
|
||||
|
||||
SystemLambda.withEnvironmentVariable(AppPaths.PathConst.ENV_DATA_PATH, testDir.canonicalPath).execute {
|
||||
assertThrows<IOException>("加载失败时应该抛出 IOException.") {
|
||||
loadAppConfig()
|
||||
}
|
||||
|
||||
File(testDir, "config.json").apply {
|
||||
//language=JSON5
|
||||
writeText(
|
||||
"""
|
||||
{
|
||||
"proxy": {
|
||||
"type": "HTTP",
|
||||
"host": "localhost",
|
||||
"port": 8080
|
||||
},
|
||||
"metrics": {
|
||||
"enable": true,
|
||||
"port": 8800,
|
||||
"bindAddress": "127.0.0.1",
|
||||
"authenticator": {
|
||||
"username": "username",
|
||||
"password": "password"
|
||||
}
|
||||
},
|
||||
"mavenRepositories": [
|
||||
{
|
||||
"url": "https://repository.maven.apache.org/maven2/"
|
||||
}
|
||||
],
|
||||
"mavenLocalRepository": "file:///tmp/maven-local-repository"
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
val appConfigs = loadAppConfig()
|
||||
assertNotNull(appConfigs)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ProxyType_toTelegramBotsType test`() {
|
||||
val expectTypeMapping = mapOf(
|
||||
ProxyType.NO_PROXY to DefaultBotOptions.ProxyType.NO_PROXY,
|
||||
ProxyType.SOCKS5 to DefaultBotOptions.ProxyType.SOCKS5,
|
||||
ProxyType.SOCKS4 to DefaultBotOptions.ProxyType.SOCKS4,
|
||||
ProxyType.HTTP to DefaultBotOptions.ProxyType.HTTP,
|
||||
ProxyType.HTTPS to DefaultBotOptions.ProxyType.HTTP
|
||||
)
|
||||
|
||||
for (proxyType in ProxyType.values()) {
|
||||
assertEquals(
|
||||
expectTypeMapping[proxyType],
|
||||
proxyType.toTelegramBotsType(),
|
||||
"ProxyType 转换失败."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ProxyConfig_toAetherProxy test`() {
|
||||
val host = "proxy.example.org"
|
||||
val port = 1080
|
||||
|
||||
val expectNotNullProxyType = setOf(
|
||||
ProxyType.HTTP,
|
||||
ProxyType.HTTPS
|
||||
)
|
||||
for (proxyType in ProxyType.values()) {
|
||||
val proxyConfig = ProxyConfig(proxyType, host, port)
|
||||
val aetherProxy = proxyConfig.toAetherProxy()
|
||||
if (expectNotNullProxyType.contains(proxyType)) {
|
||||
assertNotNull(aetherProxy, "支持的代理类型应该不为 null.")
|
||||
assertEquals(host, aetherProxy.host)
|
||||
assertEquals(port, aetherProxy.port)
|
||||
} else {
|
||||
assertNull(aetherProxy, "不支持的代理类型应该返回 null.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `MavenRepositoryConfig_toRemoteRepository test`() {
|
||||
val defaultMavenRepositoryConfig = MavenRepositoryConfig(
|
||||
url = URL(MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL),
|
||||
enableReleases = true,
|
||||
enableSnapshots = false
|
||||
)
|
||||
val remoteRepositoryWithoutId = defaultMavenRepositoryConfig.toRemoteRepository(
|
||||
ProxyConfig(ProxyType.NO_PROXY, "", 0)
|
||||
)
|
||||
assertEquals(MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL, remoteRepositoryWithoutId.url.toString())
|
||||
assertNotNull(remoteRepositoryWithoutId.id)
|
||||
assertTrue(remoteRepositoryWithoutId.getPolicy(false).isEnabled)
|
||||
assertFalse(remoteRepositoryWithoutId.getPolicy(true).isEnabled)
|
||||
|
||||
val remoteRepositoryWithId = defaultMavenRepositoryConfig.copy(id = "test-repo").toRemoteRepository(
|
||||
ProxyConfig(ProxyType.HTTP, "127.0.0.1", 1080)
|
||||
)
|
||||
|
||||
assertEquals("test-repo", remoteRepositoryWithId.id)
|
||||
assertEquals(MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL, remoteRepositoryWithId.url.toString())
|
||||
assertEquals("http", remoteRepositoryWithId.proxy.type)
|
||||
assertEquals("127.0.0.1", remoteRepositoryWithId.proxy.host)
|
||||
assertEquals(1080, remoteRepositoryWithId.proxy.port)
|
||||
assertEquals(remoteRepositoryWithId.id, remoteRepositoryWithId.id)
|
||||
|
||||
val remoteRepositoryWithProxy = defaultMavenRepositoryConfig.copy(
|
||||
id = "test-repo",
|
||||
proxy = ProxyConfig(ProxyType.HTTP, "example.org", 1080).toAetherProxy()
|
||||
).toRemoteRepository(ProxyConfig(ProxyType.HTTP, "localhost", 8080))
|
||||
assertEquals("http", remoteRepositoryWithProxy.proxy.type)
|
||||
assertEquals("example.org", remoteRepositoryWithProxy.proxy.host, "未优先使用 MavenRepositoryConfig 中的 proxy 属性.")
|
||||
assertEquals(1080, remoteRepositoryWithProxy.proxy.port, "未优先使用 MavenRepositoryConfig 中的 proxy 属性.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `checkRepositoryLayout test`() {
|
||||
val noProxyConfig = ProxyConfig(ProxyType.NO_PROXY, "", 0)
|
||||
assertEquals(
|
||||
"default", MavenRepositoryConfig(url = URL("https://repo.example.org"))
|
||||
.toRemoteRepository(noProxyConfig).contentType
|
||||
)
|
||||
assertEquals(
|
||||
"legacy", MavenRepositoryConfig(url = URL("https://repo.example.org"), layout = "LEgaCY")
|
||||
.toRemoteRepository(noProxyConfig).contentType
|
||||
)
|
||||
assertThrows<IllegalArgumentException> {
|
||||
MavenRepositoryConfig(
|
||||
url = URL("https://repo.example.org"),
|
||||
layout = "NOT_EXISTS_LAYOUT"
|
||||
).toRemoteRepository(noProxyConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initialFiles test`(@TempDir testDir: Path) {
|
||||
// 这么做是为了让日志文件创建在其他地方, 由于日志文件在运行时会持续占用, 在 windows 中文件会被锁定,
|
||||
// 导致测试框架无法正常清除测试所使用的临时文件夹.
|
||||
val logsDir = Files.createTempDirectory("ammmmmm-logs-")
|
||||
System.setProperty(AppPaths.PathConst.PROP_DATA_PATH, logsDir.toString())
|
||||
assertEquals(logsDir.toString(), AppPaths.DATA_ROOT.path, "日志目录设定失败.")
|
||||
KotlinLogging.logger("TEST").error { "日志占用.(无需理会), 日志目录: $logsDir" }
|
||||
AppPaths.DATA_LOGS.file.listFiles { _, name -> name.endsWith(".log") }?.forEach {
|
||||
it.deleteOnExit()
|
||||
}
|
||||
|
||||
val fullInitializeDir = Files.createTempDirectory(testDir, "fullInitialize")
|
||||
fullInitializeDir.deleteExisting()
|
||||
System.setProperty(AppPaths.PathConst.PROP_DATA_PATH, fullInitializeDir.toString())
|
||||
assertEquals(fullInitializeDir.toString(), AppPaths.DATA_ROOT.path, "测试路径设定失败.")
|
||||
|
||||
assertTrue(initialFiles(), "方法未能提醒用户编辑初始配置文件.")
|
||||
|
||||
for (path in AppPaths.values()) {
|
||||
assertTrue(path.file.exists(), "文件未初始化成功: ${path.path}")
|
||||
if (path.file.isFile) {
|
||||
assertNotEquals(0, path.file.length(), "文件未初始化成功(大小为 0): ${path.path}")
|
||||
}
|
||||
path.reset()
|
||||
}
|
||||
|
||||
assertFalse(initialFiles(), "方法试图在配置已初始化的情况下提醒用户编辑初始配置文件.")
|
||||
|
||||
for (path in AppPaths.values()) {
|
||||
assertTrue(path.file.exists(), "文件未初始化成功: ${path.path}")
|
||||
if (path.file.isFile) {
|
||||
assertNotEquals(0, path.file.length(), "文件未初始化成功(大小为 0): ${path.path}")
|
||||
}
|
||||
path.reset()
|
||||
}
|
||||
|
||||
assertTrue(AppPaths.CONFIG_APPLICATION.file.delete(), "config.json 删除失败.")
|
||||
assertFalse(initialFiles(), "方法试图在部分配置已初始化的情况下提醒用户编辑初始配置文件.")
|
||||
|
||||
for (path in AppPaths.values()) {
|
||||
assertTrue(path.file.exists(), "文件未初始化成功: ${path.path}")
|
||||
if (path.file.isFile) {
|
||||
assertNotEquals(0, path.file.length(), "文件未初始化成功(大小为 0): ${path.path}")
|
||||
}
|
||||
path.reset()
|
||||
}
|
||||
|
||||
assertTrue(AppPaths.CONFIG_BOT.file.delete(), "bot.json 删除失败.")
|
||||
assertFalse(initialFiles(), "方法试图在部分配置已初始化的情况下提醒用户编辑初始配置文件.")
|
||||
|
||||
for (path in AppPaths.values()) {
|
||||
assertTrue(path.file.exists(), "文件未初始化成功: ${path.path}")
|
||||
if (path.file.isFile) {
|
||||
assertNotEquals(0, path.file.length(), "文件未初始化成功(大小为 0): ${path.path}")
|
||||
}
|
||||
path.reset()
|
||||
}
|
||||
|
||||
assertTrue(AppPaths.CONFIG_APPLICATION.file.delete(), "config.json 删除失败.")
|
||||
assertTrue(AppPaths.CONFIG_BOT.file.delete(), "bot.json 删除失败.")
|
||||
assertTrue(
|
||||
initialFiles(),
|
||||
"在主要配置文件(config.json 和 bot.json)不存在的情况下初始化文件后, 方法未能提醒用户编辑初始配置文件."
|
||||
)
|
||||
|
||||
for (path in AppPaths.values()) {
|
||||
assertTrue(path.file.exists(), "文件未初始化成功: ${path.path}")
|
||||
if (path.file.isFile) {
|
||||
assertNotEquals(0, path.file.length(), "文件未初始化成功(大小为 0): ${path.path}")
|
||||
}
|
||||
}
|
||||
|
||||
AppPaths.CONFIG_APPLICATION.file.writeText("Test-APPLICATION")
|
||||
AppPaths.CONFIG_BOT.file.writeText("Test-BOT")
|
||||
assertFalse(initialFiles(), "方法试图在部分配置已初始化的情况下提醒用户编辑初始配置文件.")
|
||||
assertEquals(
|
||||
"Test-APPLICATION", AppPaths.CONFIG_APPLICATION.file.readText(),
|
||||
"config.json 被覆盖. initialized 并未阻止重复初始化."
|
||||
)
|
||||
assertEquals(
|
||||
"Test-BOT", AppPaths.CONFIG_BOT.file.readText(),
|
||||
"bot.json 被覆盖. initialized 并未阻止重复初始化."
|
||||
)
|
||||
|
||||
System.getProperties().remove(AppPaths.PathConst.PROP_DATA_PATH)
|
||||
}
|
||||
|
||||
private fun AppPaths.reset() {
|
||||
val method = AppPaths::class.java.getDeclaredMethod("reset")
|
||||
method.isAccessible = true
|
||||
method.invoke(this)
|
||||
method.isAccessible = false
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,35 +0,0 @@
|
||||
@file:Suppress("PackageDirectoryMismatch")
|
||||
|
||||
package net.lamgc.scalabot.util
|
||||
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParseException
|
||||
import com.google.gson.JsonPrimitive
|
||||
import org.eclipse.aether.artifact.DefaultArtifact
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
internal class ArtifactSerializerTest {
|
||||
|
||||
@Test
|
||||
fun badJsonType() {
|
||||
assertFailsWith<JsonParseException> { ArtifactSerializer.deserialize(JsonObject(), null, null) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serialize() {
|
||||
val gav = "org.example.software:test:1.0.0-SNAPSHOT"
|
||||
val expectArtifact = DefaultArtifact(gav)
|
||||
val actualArtifact = DefaultArtifact(ArtifactSerializer.serialize(expectArtifact, null, null).asString)
|
||||
assertEquals(expectArtifact, actualArtifact)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deserialize() {
|
||||
val gav = "org.example.software:test:1.0.0-SNAPSHOT"
|
||||
val expectArtifact = DefaultArtifact(gav)
|
||||
val actualArtifact = ArtifactSerializer.deserialize(JsonPrimitive(gav), null, null)
|
||||
assertEquals(expectArtifact, actualArtifact)
|
||||
}
|
||||
}
|
40
scalabot-app/src/test/kotlin/util/StdOutFilterTest.kt
Normal file
40
scalabot-app/src/test/kotlin/util/StdOutFilterTest.kt
Normal file
@ -0,0 +1,40 @@
|
||||
package net.lamgc.scalabot.util
|
||||
|
||||
import ch.qos.logback.classic.Level
|
||||
import ch.qos.logback.classic.spi.LoggingEvent
|
||||
import ch.qos.logback.core.spi.FilterReply
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class StdOutFilterTest {
|
||||
|
||||
@Test
|
||||
fun filterTest() {
|
||||
val filter = StdOutFilter()
|
||||
|
||||
for (level in listOf(
|
||||
Level.ALL,
|
||||
Level.TRACE,
|
||||
Level.DEBUG,
|
||||
Level.INFO
|
||||
)) {
|
||||
val loggingEvent = mockk<LoggingEvent> {
|
||||
every { this@mockk.level }.returns(level)
|
||||
}
|
||||
assertEquals(FilterReply.ACCEPT, filter.decide(loggingEvent))
|
||||
}
|
||||
|
||||
for (level in listOf(
|
||||
Level.WARN,
|
||||
Level.ERROR
|
||||
)) {
|
||||
val loggingEvent = mockk<LoggingEvent> {
|
||||
every { this@mockk.level }.returns(level)
|
||||
}
|
||||
assertEquals(FilterReply.DENY, filter.decide(loggingEvent))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,9 +1,6 @@
|
||||
package net.lamgc.scalabot.util
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.justRun
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import io.mockk.*
|
||||
import net.lamgc.scalabot.ExtensionPackageFinder
|
||||
import net.lamgc.scalabot.FinderPriority
|
||||
import net.lamgc.scalabot.FinderRules
|
||||
@ -13,6 +10,8 @@ import org.eclipse.aether.artifact.DefaultArtifact
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import java.io.File
|
||||
import java.io.FileFilter
|
||||
import java.io.FilenameFilter
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlin.test.*
|
||||
@ -116,4 +115,203 @@ internal class UtilsKtTest {
|
||||
|
||||
resourceSet.clear()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Artifact equals`() {
|
||||
val artifact = DefaultArtifact("org.example:artifact:jar:0.0.1")
|
||||
assertFalse(artifact.isSnapshot, "Release artifact is snapshot.")
|
||||
assertTrue(artifact.equalsArtifact(artifact))
|
||||
assertTrue(artifact.setFile(File(".")).equalsArtifact(artifact.setFile(File("."))))
|
||||
val snapshotArtifact = DefaultArtifact("org.example:artifact:jar:0.0.1-SNAPSHOT")
|
||||
val snapshotTimestampArtifact = DefaultArtifact("org.example:artifact:jar:0.0.1-20220605.130047-1")
|
||||
assertTrue(snapshotArtifact.isSnapshot, "SnapshotArtifact not snapshot.")
|
||||
assertNotEquals(artifact.isSnapshot, snapshotArtifact.isSnapshot)
|
||||
assertNotEquals(artifact.baseVersion, snapshotArtifact.baseVersion)
|
||||
assertFalse(artifact.equalsArtifact(snapshotArtifact))
|
||||
assertFalse(snapshotArtifact.equalsArtifact(snapshotTimestampArtifact))
|
||||
assertFalse(artifact.equalsArtifact(DefaultArtifact("org.example:artifact:0.0.2")))
|
||||
assertFalse(artifact.equalsArtifact(DefaultArtifact("org.example.test:artifact:0.0.1")))
|
||||
assertFalse(artifact.equalsArtifact(DefaultArtifact("org.example:artifact-a:0.0.1")))
|
||||
assertFalse(artifact.equalsArtifact(DefaultArtifact("org.example:artifact:war:0.0.1")))
|
||||
assertFalse(artifact.equalsArtifact(DefaultArtifact("org.example:artifact:war:javadoc:0.0.1")))
|
||||
assertFalse(artifact.equalsArtifact(DefaultArtifact("org.example:artifact:rar:source:0.0.1")))
|
||||
|
||||
assertFalse(
|
||||
artifact.equalsArtifact(
|
||||
DefaultArtifact("org.example:artifact:jar:0.0.1")
|
||||
.setFile(File("./xxx01.jar"))
|
||||
)
|
||||
)
|
||||
|
||||
val artifactWithExtension = DefaultArtifact("org.example:artifact:jar:0.0.1")
|
||||
assertFalse(artifactWithExtension.equalsArtifact(DefaultArtifact("org.example:artifact:war:0.0.1")))
|
||||
|
||||
assertTrue(artifact.equalsArtifact(artifact.setProperties(mapOf(Pair("a", "b"))), checkProperties = false))
|
||||
assertFalse(artifact.equalsArtifact(artifact.setProperties(mapOf(Pair("a", "b"))), checkProperties = true))
|
||||
assertTrue(
|
||||
artifact.setProperties(mapOf(Pair("a", "b")))
|
||||
.equalsArtifact(artifact.setProperties(mapOf(Pair("a", "b"))), checkProperties = true)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deepListFile Test - Basics`() {
|
||||
assertNull(mockk<File> {
|
||||
every { listFiles() } returns null
|
||||
}.deepListFiles())
|
||||
assertNull(mockk<File> {
|
||||
every { listFiles(ofType(FileFilter::class)) } returns null
|
||||
}.deepListFiles(fileFilter = { true }))
|
||||
assertNull(mockk<File> {
|
||||
every { listFiles(ofType(FilenameFilter::class)) } returns null
|
||||
}.deepListFiles(filenameFilter = { _, _ -> true }))
|
||||
|
||||
|
||||
val listFileMock = mockk<File> {
|
||||
every { listFiles() } returns arrayOf()
|
||||
every { listFiles(ofType(FileFilter::class)) } returns arrayOf()
|
||||
every { listFiles(ofType(FilenameFilter::class)) } returns arrayOf()
|
||||
}
|
||||
assertNotNull(listFileMock.deepListFiles())
|
||||
verify(exactly = 1) { listFileMock.listFiles() }
|
||||
verify(exactly = 0) { listFileMock.listFiles(ofType(FilenameFilter::class)) }
|
||||
verify(exactly = 0) { listFileMock.listFiles(ofType(FileFilter::class)) }
|
||||
clearMocks(listFileMock, answers = false)
|
||||
|
||||
assertNotNull(listFileMock.deepListFiles(filenameFilter = { _, _ -> true }))
|
||||
verify(exactly = 0) { listFileMock.listFiles() }
|
||||
verify(exactly = 1) { listFileMock.listFiles(ofType(FilenameFilter::class)) }
|
||||
verify(exactly = 0) { listFileMock.listFiles(ofType(FileFilter::class)) }
|
||||
clearMocks(listFileMock, answers = false)
|
||||
|
||||
assertNotNull(listFileMock.deepListFiles(fileFilter = { true }))
|
||||
verify(exactly = 0) { listFileMock.listFiles() }
|
||||
verify(exactly = 0) { listFileMock.listFiles(ofType(FilenameFilter::class)) }
|
||||
verify(exactly = 1) { listFileMock.listFiles(ofType(FileFilter::class)) }
|
||||
clearMocks(listFileMock, answers = false)
|
||||
|
||||
assertNotNull(listFileMock.deepListFiles(fileFilter = { true }, filenameFilter = { _, _ -> true }))
|
||||
verify(exactly = 0) { listFileMock.listFiles() }
|
||||
verify(exactly = 1) { listFileMock.listFiles(ofType(FileFilter::class)) }
|
||||
verify(exactly = 0) { listFileMock.listFiles(ofType(FilenameFilter::class)) }
|
||||
clearMocks(listFileMock, answers = false)
|
||||
|
||||
val addSelfResult = listFileMock.deepListFiles(addSelf = true)
|
||||
assertNotNull(addSelfResult)
|
||||
assertEquals(1, addSelfResult.size)
|
||||
assertTrue(addSelfResult.contains(listFileMock))
|
||||
verify(exactly = 1) { listFileMock.listFiles() }
|
||||
verify(exactly = 0) { listFileMock.listFiles(ofType(FilenameFilter::class)) }
|
||||
verify(exactly = 0) { listFileMock.listFiles(ofType(FileFilter::class)) }
|
||||
|
||||
val addSelfWithoutDirMock = createDirectory(
|
||||
"root", arrayOf(
|
||||
createDirectory(
|
||||
"dir01", arrayOf(
|
||||
createFile("test01")
|
||||
)
|
||||
),
|
||||
createDirectory(
|
||||
"dir02", arrayOf(
|
||||
createFile("test02")
|
||||
)
|
||||
),
|
||||
createDirectory(
|
||||
"dir03", arrayOf(
|
||||
createFile("test03")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val addSelfWithoutDirResult = addSelfWithoutDirMock.deepListFiles(addSelf = true, onlyFile = true)
|
||||
assertNotNull(addSelfWithoutDirResult)
|
||||
assertFalse(addSelfWithoutDirResult.isEmpty())
|
||||
assertEquals(1, addSelfWithoutDirResult.filter { it.isDirectory }.size)
|
||||
assertEquals(addSelfWithoutDirMock, addSelfWithoutDirResult.find { it.isDirectory })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deepListFile Test - Complex`() {
|
||||
val mock = createDirectory(
|
||||
"root", arrayOf(
|
||||
createFile("test"),
|
||||
createFile("test02"),
|
||||
createDirectory("dir01"),
|
||||
createDirectory("dir02")
|
||||
)
|
||||
)
|
||||
|
||||
val withDirResult = mock.deepListFiles(onlyFile = false)
|
||||
assertNotNull(withDirResult)
|
||||
assertEquals(4, withDirResult.size)
|
||||
assertEquals(2, withDirResult.filter { it.isFile }.size)
|
||||
assertEquals(2, withDirResult.filter { it.isDirectory }.size)
|
||||
|
||||
val withoutDirResult = mock.deepListFiles(onlyFile = true)
|
||||
assertNotNull(withoutDirResult)
|
||||
assertEquals(2, withoutDirResult.filter { it.isFile }.size)
|
||||
assertNull(withoutDirResult.find { it.isDirectory })
|
||||
|
||||
val subDirFailedMock = createDirectory(
|
||||
"root", arrayOf(
|
||||
mockk(name = "dir::cannotReadableDirectory") {
|
||||
every { isFile } returns false
|
||||
every { isDirectory } returns true
|
||||
every { name } returns "cannotReadableDirectory"
|
||||
every { listFiles() } returns null
|
||||
every { listFiles(ofType(FileFilter::class)) } returns null
|
||||
every { listFiles(ofType(FilenameFilter::class)) } returns null
|
||||
},
|
||||
createDirectory(
|
||||
"dir2", arrayOf(
|
||||
createFile("test")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val subDirFailedWithDirResult = subDirFailedMock.deepListFiles(onlyFile = false)
|
||||
assertNotNull(subDirFailedWithDirResult)
|
||||
assertEquals(3, subDirFailedWithDirResult.size)
|
||||
assertNotNull(subDirFailedWithDirResult.find { it.isDirectory && it.name == "cannotReadableDirectory" })
|
||||
assertNotNull(subDirFailedWithDirResult.find { it.isDirectory && it.name == "dir2" })
|
||||
assertNotNull(subDirFailedWithDirResult.find { it.isFile && it.name == "test" })
|
||||
|
||||
val subDirFailedWithoutDirResult = subDirFailedMock.deepListFiles(onlyFile = true)
|
||||
assertNotNull(subDirFailedWithoutDirResult)
|
||||
assertEquals(1, subDirFailedWithoutDirResult.size)
|
||||
assertEquals(0, subDirFailedWithoutDirResult.filter { it.isDirectory }.size)
|
||||
assertNotNull(subDirFailedWithoutDirResult.find { it.isFile && it.name == "test" })
|
||||
assertNull(subDirFailedWithoutDirResult.find { it.isDirectory && it.name == "cannotReadableDirectory" })
|
||||
assertNull(subDirFailedWithoutDirResult.find { it.isDirectory && it.name == "dir2" })
|
||||
}
|
||||
|
||||
private fun createFile(path: String): File {
|
||||
val file = File(path)
|
||||
return mockk(name = "file::$path") {
|
||||
every { isFile } returns true
|
||||
every { isDirectory } returns false
|
||||
every { name } returns file.name
|
||||
every { listFiles() } returns null
|
||||
every { listFiles(ofType(FileFilter::class)) } returns null
|
||||
every { listFiles(ofType(FilenameFilter::class)) } returns null
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDirectory(path: String, subFiles: Array<File> = arrayOf()): File {
|
||||
val file = File(path)
|
||||
return mockk(name = "dir::$path") {
|
||||
every { isFile } returns false
|
||||
every { isDirectory } returns true
|
||||
every { name } returns file.name
|
||||
every { listFiles() } returns subFiles
|
||||
every { listFiles(ofType(FileFilter::class)) } answers {
|
||||
subFiles.filter { (firstArg() as FileFilter).accept(it) }.toTypedArray()
|
||||
}
|
||||
every { listFiles(ofType(FilenameFilter::class)) } answers {
|
||||
subFiles.filter { (firstArg() as FilenameFilter).accept(file.parentFile, file.name) }.toTypedArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -2,10 +2,6 @@ plugins {
|
||||
java
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":scalabot-extension"))
|
||||
|
||||
@ -16,3 +12,9 @@ dependencies {
|
||||
tasks.getByName<Test>("test") {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
tasks.withType<Javadoc> {
|
||||
options {
|
||||
encoding = "UTF-8"
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,17 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") version "1.6.10"
|
||||
kotlin("jvm")
|
||||
java
|
||||
jacoco
|
||||
`maven-publish`
|
||||
signing
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api("org.telegram:telegrambots-abilities:6.0.1")
|
||||
api("org.telegram:telegrambots-abilities:6.1.0")
|
||||
api("org.slf4j:slf4j-api:1.7.36")
|
||||
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2")
|
||||
testImplementation("org.mockito:mockito-core:4.4.0")
|
||||
testImplementation("org.mockito:mockito-core:4.6.1")
|
||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
|
||||
}
|
||||
|
||||
@ -31,23 +30,29 @@ java {
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
finalizedBy(tasks.jacocoTestReport)
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions.jvmTarget = "11"
|
||||
tasks.jacocoTestReport {
|
||||
dependsOn(tasks.test)
|
||||
}
|
||||
|
||||
tasks.withType<AbstractArchiveTask>().configureEach {
|
||||
isPreserveFileTimestamps = false
|
||||
isReproducibleFileOrder = true
|
||||
}
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
if (project.version.toString().endsWith("-SNAPSHOT")) {
|
||||
maven("https://repo.lamgc.moe/repository/maven-snapshots/") {
|
||||
maven("https://nexus.kuku.me/repository/maven-snapshots/") {
|
||||
credentials {
|
||||
username = project.properties["repo.credentials.private.username"].toString()
|
||||
password = project.properties["repo.credentials.private.password"].toString()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
maven("https://repo.lamgc.moe/repository/maven-releases/") {
|
||||
maven("https://nexus.kuku.me/repository/maven-releases/") {
|
||||
credentials {
|
||||
username = project.properties["repo.credentials.private.username"].toString()
|
||||
password = project.properties["repo.credentials.private.password"].toString()
|
||||
|
@ -16,8 +16,8 @@ import static org.mockito.Mockito.*;
|
||||
|
||||
public class AbilityBotsTest {
|
||||
|
||||
public static final User USER = new User(1L, "first", false, "last", "username", null, false, false, false);
|
||||
public static final User CREATOR = new User(1337L, "creatorFirst", false, "creatorLast", "creatorUsername", null, false, false, false);
|
||||
public static final User USER = new User(1L, "first", false, "last", "username", null, false, false, false, false, false);
|
||||
public static final User CREATOR = new User(1337L, "creatorFirst", false, "creatorLast", "creatorUsername", null, false, false, false, false, false);
|
||||
|
||||
static Update mockFullUpdate(BaseAbilityBot bot, User user, String args) {
|
||||
bot.users().put(USER.getId(), USER);
|
||||
@ -56,8 +56,8 @@ public class AbilityBotsTest {
|
||||
|
||||
@Test
|
||||
void cancelReplyStateTest() {
|
||||
User userA = new User(10001L, "first", false, "last", "username", null, false, false, false);
|
||||
User userB = new User(10101L, "first", false, "last", "username", null, false, false, false);
|
||||
User userA = new User(10001L, "first", false, "last", "username", null, false, false, false, false, false);
|
||||
User userB = new User(10101L, "first", false, "last", "username", null, false, false, false, false, false);
|
||||
SilentSender silent = mock(SilentSender.class);
|
||||
BaseAbilityBot bot = new TestingAbilityBot("", "", silent);
|
||||
bot.onRegister();
|
||||
@ -94,6 +94,7 @@ public class AbilityBotsTest {
|
||||
this.silent = silentSender;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Ability setReply() {
|
||||
return Ability.builder()
|
||||
.name("set_reply")
|
||||
|
13
scalabot-meta/README.md
Normal file
13
scalabot-meta/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# scalabot-meta
|
||||
|
||||
本模块用于将 ScalaBot 的一些配置相关内容发布出去,以便于其他项目使用。
|
||||
|
||||
主要是配置类和相应的 Gson 序列化器(如果有,或者必要)。
|
||||
|
||||
## 关于序列化器
|
||||
|
||||
强烈建议使用序列化器!由于 Kotlin 与 Gson 之间的一些兼容性问题
|
||||
(参见[本提交](https://github.com/LamGC/ScalaBot/commit/084280564af58d1af22db5b57c67577d93bd820e)),
|
||||
如果直接让 Gson 解析 Kotlin Data 类,将会出现一些潜在的问题(比如无法使用默认值)。
|
||||
部分序列化器也可以帮助检查字段值是否合法,以防止因字段值不正确导致出现更多的问题
|
||||
(例如 BotAccount 中,如果 `token` 的格式有误,那么获取 `id` 时将引发 `NumberFormatException` 异常)。
|
113
scalabot-meta/build.gradle.kts
Normal file
113
scalabot-meta/build.gradle.kts
Normal file
@ -0,0 +1,113 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
id("org.jetbrains.kotlinx.kover")
|
||||
id("org.jetbrains.dokka") version "1.7.0"
|
||||
`maven-publish`
|
||||
signing
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val aetherVersion = "1.1.0"
|
||||
api("org.eclipse.aether:aether-api:$aetherVersion")
|
||||
implementation("org.eclipse.aether:aether-util:$aetherVersion")
|
||||
|
||||
implementation("org.telegram:telegrambots-meta:6.1.0")
|
||||
|
||||
api("com.google.code.gson:gson:2.9.0")
|
||||
|
||||
testImplementation(kotlin("test"))
|
||||
testImplementation("io.mockk:mockk:1.12.4")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2")
|
||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.2")
|
||||
|
||||
dokkaHtmlPlugin("org.jetbrains.dokka:javadoc-plugin:1.7.0")
|
||||
}
|
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
withJavadocJar()
|
||||
withSourcesJar()
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
tasks.withType<AbstractArchiveTask>().configureEach {
|
||||
isPreserveFileTimestamps = false
|
||||
isReproducibleFileOrder = true
|
||||
}
|
||||
|
||||
tasks.getByName<Test>("test") {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
val javadocJar = tasks.named<Jar>("javadocJar") {
|
||||
from(tasks.named("dokkaJavadoc"))
|
||||
}
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
if (project.version.toString().endsWith("-SNAPSHOT", ignoreCase = true)) {
|
||||
maven("https://nexus.kuku.me/repository/maven-snapshots/") {
|
||||
credentials {
|
||||
username = project.properties["repo.credentials.private.username"].toString()
|
||||
password = project.properties["repo.credentials.private.password"].toString()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
maven("https://nexus.kuku.me/repository/maven-releases/") {
|
||||
credentials {
|
||||
username = project.properties["repo.credentials.private.username"].toString()
|
||||
password = project.properties["repo.credentials.private.password"].toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publications {
|
||||
create<MavenPublication>("maven") {
|
||||
from(components["kotlin"])
|
||||
artifact(javadocJar)
|
||||
artifact(tasks.named("sourcesJar"))
|
||||
pom {
|
||||
name.set("ScalaBot-meta")
|
||||
description.set(
|
||||
"Shared components used by scalabot (such as configuration classes)"
|
||||
)
|
||||
url.set("https://github.com/LamGC/ScalaBot")
|
||||
licenses {
|
||||
license {
|
||||
name.set("The MIT License")
|
||||
url.set("https://www.opensource.org/licenses/mit-license.php")
|
||||
}
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id.set("LamGC")
|
||||
name.set("LamGC")
|
||||
email.set("lam827@lamgc.net")
|
||||
url.set("https://github.com/LamGC")
|
||||
}
|
||||
}
|
||||
scm {
|
||||
connection.set("scm:git:https://github.com/LamGC/ScalaBot.git")
|
||||
developerConnection.set("scm:git:https://github.com/LamGC/ScalaBot.git")
|
||||
url.set("https://github.com/LamGC/ScalaBot")
|
||||
}
|
||||
issueManagement {
|
||||
url.set("https://github.com/LamGC/ScalaBot/issues")
|
||||
system.set("Github Issues")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signing {
|
||||
useGpgCmd()
|
||||
sign(publishing.publications["maven"])
|
||||
}
|
153
scalabot-meta/src/main/kotlin/Configs.kt
Normal file
153
scalabot-meta/src/main/kotlin/Configs.kt
Normal file
@ -0,0 +1,153 @@
|
||||
package net.lamgc.scalabot.config
|
||||
|
||||
import org.eclipse.aether.artifact.Artifact
|
||||
import org.eclipse.aether.repository.Authentication
|
||||
import org.eclipse.aether.repository.Proxy
|
||||
import org.telegram.telegrambots.meta.ApiConstants
|
||||
import java.net.URL
|
||||
|
||||
/**
|
||||
* 机器人帐号信息.
|
||||
* @property name 机器人名称, 建议与实际设定的名称相同.
|
||||
* @property token 机器人 API Token.
|
||||
* @property creatorId 机器人创建者, 管理机器人需要使用该信息.
|
||||
* @property id 机器人账号 ID.
|
||||
*/
|
||||
data class BotAccount(
|
||||
val name: String,
|
||||
val token: String,
|
||||
val creatorId: Long
|
||||
) {
|
||||
|
||||
val id
|
||||
// 不要想着每次获取都要从 token 里取出有性能损耗.
|
||||
// 由于 Gson 解析方式, 如果不这么做, 会出现 token 设置前 id 初始化完成, 就只有"0"了,
|
||||
// 虽然能过单元测试, 但实际使用过程是不能正常用的.
|
||||
get() = token.substringBefore(":").toLong()
|
||||
}
|
||||
|
||||
/**
|
||||
* 机器人配置.
|
||||
*
|
||||
* 使用 Gson 解析时, 请添加以下类型适配器:
|
||||
* - [net.lamgc.scalabot.config.serializer.ProxyTypeSerializer]
|
||||
* - [net.lamgc.scalabot.config.serializer.BotConfigSerializer]
|
||||
* - [net.lamgc.scalabot.config.serializer.BotAccountSerializer]
|
||||
* - [net.lamgc.scalabot.config.serializer.ArtifactSerializer]
|
||||
*
|
||||
* @property enabled 是否启用机器人.
|
||||
* @property account 机器人帐号信息, 用于访问 API.
|
||||
* @property disableBuiltInAbility 是否禁用 AbilityBot 自带命令.
|
||||
* @property autoUpdateCommandList 是否自动更新机器人在 Telegram 的命令列表.
|
||||
* @property extensions 该机器人启用的扩展.
|
||||
* @property proxy 为该机器人单独设置的代理配置, 如无设置, 则使用 AppConfig 中的代理配置.
|
||||
* @property baseApiUrl 机器人所使用的 API 地址, 适用于自建 Telegram Bot API 端点.
|
||||
*/
|
||||
data class BotConfig(
|
||||
val enabled: Boolean = false,
|
||||
val account: BotAccount,
|
||||
val disableBuiltInAbility: Boolean = false,
|
||||
val autoUpdateCommandList: Boolean = false,
|
||||
/*
|
||||
* 使用构件坐标来选择机器人所使用的扩展包.
|
||||
* 这么做的原因是我暂时没找到一个合适的方法来让开发者方便地设定自己的扩展 Id,
|
||||
* 而构件坐标(POM Reference 或者叫 GAV 坐标)是开发者创建 Maven/Gradle 项目时一定会设置的,
|
||||
* 所以就直接用了. :P
|
||||
*/
|
||||
val extensions: Set<Artifact> = emptySet(),
|
||||
val proxy: ProxyConfig = ProxyConfig(type = ProxyType.NO_PROXY),
|
||||
val baseApiUrl: String = ApiConstants.BASE_URL
|
||||
)
|
||||
|
||||
/**
|
||||
* 代理类型.
|
||||
*/
|
||||
enum class ProxyType {
|
||||
NO_PROXY,
|
||||
HTTP,
|
||||
HTTPS,
|
||||
SOCKS4,
|
||||
SOCKS5
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理配置.
|
||||
* @property type 代理类型.
|
||||
* @property host 代理服务端地址.
|
||||
* @property port 代理服务端端口.
|
||||
*/
|
||||
data class ProxyConfig(
|
||||
val type: ProxyType = ProxyType.NO_PROXY,
|
||||
val host: String = "127.0.0.1",
|
||||
val port: Int = 1080,
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return if (type != ProxyType.NO_PROXY) {
|
||||
"$type://$host:$port"
|
||||
} else {
|
||||
"NO_PROXY"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ScalaBot 的运行指标公开配置.
|
||||
*
|
||||
* ScalaBot 内置了用于公开运行指标的服务端,
|
||||
* 该指标遵循 Prometheus 的标准, 可以通过 Prometheus 的工具来查看.
|
||||
*
|
||||
* @property enable 是否启用运行指标服务端.
|
||||
* @property port 运行指标服务端的端口.
|
||||
* @property bindAddress 运行指标服务端的绑定地址, 绑定后只有该地址可以访问.
|
||||
* @property authenticator 运行指标服务端的 HTTP 认证配置.
|
||||
*/
|
||||
data class MetricsConfig(
|
||||
val enable: Boolean = false,
|
||||
val port: Int = 9386,
|
||||
val bindAddress: String? = "0.0.0.0",
|
||||
val authenticator: UsernameAuthenticator? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Maven 远端仓库配置.
|
||||
* @property id 远端仓库 ID, 如果该属性未配置 (null), 那么运行时将会自动分配一个 Id.
|
||||
* @property url 仓库地址.
|
||||
* @property proxy 访问仓库所使用的代理, 仅支持 http/https 代理.
|
||||
* @property layout 仓库布局版本, Maven 2 及以上使用 `default`, Maven 1 使用 `legacy`.
|
||||
* @property enableReleases 是否在该远端仓库获取发布版本.
|
||||
* @property enableSnapshots 是否在该远端仓库获取快照版本.
|
||||
* @property authentication 访问该远端仓库所使用的认证配置.
|
||||
*/
|
||||
data class MavenRepositoryConfig(
|
||||
val id: String? = null,
|
||||
val url: URL,
|
||||
val proxy: Proxy? = null,
|
||||
val layout: String = "default",
|
||||
val enableReleases: Boolean = true,
|
||||
val enableSnapshots: Boolean = true,
|
||||
// 可能要设计个 type 来判断解析成什么类型的 Authentication.
|
||||
val authentication: Authentication? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* ScalaBot App 配置.
|
||||
*
|
||||
* App 配置信息与 BotConfig 分开, 分别存储在各自单独的文件中.
|
||||
*
|
||||
* 使用 Gson 解析时, 请添加以下类型适配器:
|
||||
* - [net.lamgc.scalabot.config.serializer.ProxyTypeSerializer]
|
||||
* - [net.lamgc.scalabot.config.serializer.MavenRepositoryConfigSerializer]
|
||||
* - [net.lamgc.scalabot.config.serializer.AuthenticationSerializer]
|
||||
* - [net.lamgc.scalabot.config.serializer.UsernameAuthenticatorSerializer]
|
||||
*
|
||||
* @property proxy Telegram API 代理配置.
|
||||
* @property metrics 运行指标数据配置. 可通过时序数据库记录运行数据.
|
||||
* @property mavenRepositories Maven 远端仓库配置.
|
||||
* @property mavenLocalRepository Maven 本地仓库路径. 相对于运行目录 (而不是 DATA_ROOT 目录)
|
||||
*/
|
||||
data class AppConfig(
|
||||
val proxy: ProxyConfig = ProxyConfig(),
|
||||
val metrics: MetricsConfig = MetricsConfig(),
|
||||
val mavenRepositories: List<MavenRepositoryConfig> = emptyList(),
|
||||
val mavenLocalRepository: String? = null
|
||||
)
|
26
scalabot-meta/src/main/kotlin/UsernameAuthenticator.kt
Normal file
26
scalabot-meta/src/main/kotlin/UsernameAuthenticator.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package net.lamgc.scalabot.config
|
||||
|
||||
import com.google.gson.JsonObject
|
||||
import com.sun.net.httpserver.BasicAuthenticator
|
||||
|
||||
class UsernameAuthenticator(private val username: String, private val password: String) :
|
||||
BasicAuthenticator("metrics") {
|
||||
override fun checkCredentials(username: String?, password: String?): Boolean =
|
||||
this.username == username && this.password == password
|
||||
|
||||
fun toJsonObject(): JsonObject = JsonObject().apply {
|
||||
addProperty("username", username)
|
||||
addProperty("password", password)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is UsernameAuthenticator && this.username == other.username && this.password == other.password
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = username.hashCode()
|
||||
result = 31 * result + password.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
300
scalabot-meta/src/main/kotlin/serializer/Serializer.kt
Normal file
300
scalabot-meta/src/main/kotlin/serializer/Serializer.kt
Normal file
@ -0,0 +1,300 @@
|
||||
package net.lamgc.scalabot.config.serializer
|
||||
|
||||
import com.google.gson.*
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import net.lamgc.scalabot.config.*
|
||||
import net.lamgc.scalabot.config.serializer.SerializeUtils.getPrimitiveValueOrThrow
|
||||
import org.eclipse.aether.artifact.AbstractArtifact
|
||||
import org.eclipse.aether.artifact.Artifact
|
||||
import org.eclipse.aether.artifact.DefaultArtifact
|
||||
import org.eclipse.aether.repository.Authentication
|
||||
import org.eclipse.aether.repository.Proxy
|
||||
import org.eclipse.aether.util.repository.AuthenticationBuilder
|
||||
import java.lang.reflect.Type
|
||||
import java.net.MalformedURLException
|
||||
import java.net.URL
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object ProxyTypeSerializer : JsonDeserializer<ProxyType>,
|
||||
JsonSerializer<ProxyType> {
|
||||
|
||||
override fun deserialize(
|
||||
json: JsonElement,
|
||||
typeOfT: Type?,
|
||||
context: JsonDeserializationContext?
|
||||
): ProxyType {
|
||||
if (json.isJsonNull) {
|
||||
return ProxyType.NO_PROXY
|
||||
}
|
||||
if (!json.isJsonPrimitive) {
|
||||
throw JsonParseException("Wrong configuration value type.")
|
||||
}
|
||||
val value = json.asString.trim()
|
||||
try {
|
||||
return ProxyType.valueOf(value.uppercase())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw JsonParseException("Invalid value: $value")
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(
|
||||
src: ProxyType,
|
||||
typeOfSrc: Type?,
|
||||
context: JsonSerializationContext?
|
||||
): JsonElement {
|
||||
return JsonPrimitive(src.toString())
|
||||
}
|
||||
}
|
||||
|
||||
object ArtifactSerializer : JsonSerializer<Artifact>, JsonDeserializer<Artifact> {
|
||||
override fun serialize(src: Artifact, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
|
||||
return if (src is AbstractArtifact) {
|
||||
JsonPrimitive(src.toString())
|
||||
} else {
|
||||
JsonPrimitive(
|
||||
DefaultArtifact(
|
||||
src.groupId,
|
||||
src.artifactId,
|
||||
src.classifier,
|
||||
src.extension,
|
||||
src.version
|
||||
).toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): Artifact {
|
||||
if (!json.isJsonPrimitive) {
|
||||
throw JsonParseException("Wrong configuration value type.")
|
||||
}
|
||||
val artifactStr = json.asString.trim()
|
||||
try {
|
||||
return DefaultArtifact(artifactStr)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw JsonParseException("Invalid artifact format: `${artifactStr}`.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object AuthenticationSerializer : JsonDeserializer<Authentication> {
|
||||
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Authentication {
|
||||
if (json !is JsonObject) {
|
||||
throw JsonParseException("Unsupported JSON type.")
|
||||
}
|
||||
val username = json.getPrimitiveValueOrThrow("username").asString
|
||||
val password = json.getPrimitiveValueOrThrow("password").asString
|
||||
val builder = AuthenticationBuilder()
|
||||
builder.addUsername(username)
|
||||
builder.addPassword(password)
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal object SerializeUtils {
|
||||
|
||||
fun JsonObject.getPrimitiveValueOrThrow(fieldName: String): JsonPrimitive {
|
||||
val value = get(fieldName) ?: throw JsonParseException("Missing `$fieldName` field.")
|
||||
if (value !is JsonPrimitive) {
|
||||
throw JsonParseException("Invalid `account` field type.")
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
object MavenRepositoryConfigSerializer
|
||||
: JsonDeserializer<MavenRepositoryConfig> {
|
||||
|
||||
override fun deserialize(
|
||||
json: JsonElement,
|
||||
typeOfT: Type,
|
||||
context: JsonDeserializationContext
|
||||
): MavenRepositoryConfig {
|
||||
return when (json) {
|
||||
is JsonObject -> {
|
||||
MavenRepositoryConfig(
|
||||
id = json.get("id")?.asString,
|
||||
url = URL(json.getPrimitiveValueOrThrow("url").asString),
|
||||
proxy = if (json.has("proxy"))
|
||||
context.deserialize<Proxy>(
|
||||
json.get("proxy"), Proxy::class.java
|
||||
) else null,
|
||||
layout = json.get("layout")?.asString ?: "default",
|
||||
enableReleases = json.get("enableReleases")?.asBoolean ?: true,
|
||||
enableSnapshots = json.get("enableSnapshots")?.asBoolean ?: true,
|
||||
authentication = if (json.has("authentication"))
|
||||
context.deserialize<Authentication>(
|
||||
json.get("authentication"), Authentication::class.java
|
||||
) else null
|
||||
)
|
||||
}
|
||||
is JsonPrimitive -> {
|
||||
try {
|
||||
return MavenRepositoryConfig(url = URL(json.asString))
|
||||
} catch (e: MalformedURLException) {
|
||||
throw JsonParseException("Invalid URL: ${json.asString}", e)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
throw JsonParseException("Unsupported Maven repository configuration type. (Only support JSON object or url string)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object UsernameAuthenticatorSerializer : JsonSerializer<UsernameAuthenticator>,
|
||||
JsonDeserializer<UsernameAuthenticator> {
|
||||
|
||||
override fun serialize(
|
||||
src: UsernameAuthenticator,
|
||||
typeOfSrc: Type?,
|
||||
context: JsonSerializationContext?
|
||||
): JsonElement {
|
||||
return src.toJsonObject()
|
||||
}
|
||||
|
||||
override fun deserialize(
|
||||
json: JsonElement,
|
||||
typeOfT: Type?,
|
||||
context: JsonDeserializationContext?
|
||||
): UsernameAuthenticator? {
|
||||
if (json.isJsonNull) {
|
||||
return null
|
||||
} else if (!json.isJsonObject) {
|
||||
throw JsonParseException("Invalid attribute value type.")
|
||||
}
|
||||
|
||||
val jsonObj = json.asJsonObject
|
||||
|
||||
if (jsonObj["username"]?.isJsonPrimitive != true) {
|
||||
throw JsonParseException("Invalid attribute value: username")
|
||||
} else if (jsonObj["password"]?.isJsonPrimitive != true) {
|
||||
throw JsonParseException("Invalid attribute value: password")
|
||||
}
|
||||
|
||||
if (jsonObj["username"].asString.isEmpty() || jsonObj["password"].asString.isEmpty()) {
|
||||
throw JsonParseException("`username` or `password` is empty.")
|
||||
}
|
||||
return UsernameAuthenticator(jsonObj["username"].asString, jsonObj["password"].asString)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object ProxyConfigSerializer : JsonSerializer<ProxyConfig>, JsonDeserializer<ProxyConfig> {
|
||||
override fun serialize(src: ProxyConfig?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
|
||||
if (src == null) {
|
||||
return JsonNull.INSTANCE
|
||||
}
|
||||
return JsonObject().apply {
|
||||
addProperty("type", src.type.name)
|
||||
addProperty("host", src.host)
|
||||
addProperty("port", src.port)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): ProxyConfig {
|
||||
if (json == null || json.isJsonNull) {
|
||||
return ProxyConfig()
|
||||
} else if (json !is JsonObject) {
|
||||
throw JsonParseException("Invalid json type.")
|
||||
}
|
||||
|
||||
val typeStr = json["type"]?.asString ?: return ProxyConfig()
|
||||
val type = try {
|
||||
ProxyType.valueOf(typeStr)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw JsonParseException("Invalid proxy type: `$typeStr`")
|
||||
}
|
||||
|
||||
if (!json.has("host") || !json.has("port")) {
|
||||
throw JsonParseException("Missing `host` field or `port` field.")
|
||||
}
|
||||
|
||||
return ProxyConfig(
|
||||
type = type,
|
||||
host = json["host"].asString,
|
||||
port = json["port"].asInt
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object BotConfigSerializer : JsonSerializer<BotConfig>, JsonDeserializer<BotConfig> {
|
||||
|
||||
private val defaultConfig = BotConfig(account = BotAccount("__Default__", "__Default__", 0))
|
||||
|
||||
override fun serialize(src: BotConfig, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
|
||||
return JsonObject().apply {
|
||||
addProperty("enabled", src.enabled)
|
||||
add("account", context.serialize(src.account))
|
||||
addProperty("disableBuiltInAbility", src.disableBuiltInAbility)
|
||||
addProperty("autoUpdateCommandList", src.autoUpdateCommandList)
|
||||
add("extensions", context.serialize(src.extensions))
|
||||
add("proxy", ProxyConfigSerializer.serialize(src.proxy, ProxyConfig::class.java, context))
|
||||
addProperty("baseApiUrl", src.baseApiUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): BotConfig {
|
||||
if (json !is JsonObject) {
|
||||
throw JsonParseException("Unsupported JSON type.")
|
||||
}
|
||||
|
||||
if (!json.has("account")) {
|
||||
throw JsonParseException("Missing `account` field.")
|
||||
} else if (!json.get("account").isJsonObject) {
|
||||
throw JsonParseException("Invalid `account` field type.")
|
||||
}
|
||||
|
||||
// 从 json 反序列化 BotConfig(使用构造函数)
|
||||
return BotConfig(
|
||||
enabled = json.get("enabled")?.asBoolean ?: defaultConfig.enabled,
|
||||
account = context.deserialize(json.get("account"), BotAccount::class.java)!!,
|
||||
disableBuiltInAbility = json.get("disableBuiltInAbility")?.asBoolean ?: defaultConfig.disableBuiltInAbility,
|
||||
autoUpdateCommandList = json.get("autoUpdateCommandList")?.asBoolean ?: defaultConfig.autoUpdateCommandList,
|
||||
extensions = context.deserialize(json.get("extensions"), object : TypeToken<Set<Artifact>>() {}.type)
|
||||
?: defaultConfig.extensions,
|
||||
proxy = context.deserialize(json.get("proxy"), ProxyConfig::class.java) ?: defaultConfig.proxy,
|
||||
baseApiUrl = json.get("baseApiUrl")?.asString ?: defaultConfig.baseApiUrl
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object BotAccountSerializer : JsonDeserializer<BotAccount> {
|
||||
|
||||
private val tokenCheckRegex = Pattern.compile("\\d+:[a-zA-Z\\d_-]{35}")
|
||||
|
||||
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): BotAccount {
|
||||
if (json == null || json.isJsonNull) {
|
||||
throw JsonParseException("Missing `account` field.")
|
||||
} else if (!json.isJsonObject) {
|
||||
throw JsonParseException("Invalid `account` field type.")
|
||||
}
|
||||
val jsonObj = json.asJsonObject
|
||||
|
||||
val name = jsonObj.getPrimitiveValueOrThrow("name").asString
|
||||
val token = jsonObj.getPrimitiveValueOrThrow("token").asString.let {
|
||||
if (it.isEmpty()) {
|
||||
throw JsonParseException("`token` cannot be empty.")
|
||||
} else if (!tokenCheckRegex.matcher(it).matches()) {
|
||||
throw JsonParseException("`token` is invalid.")
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
val creatorId = try {
|
||||
jsonObj.getPrimitiveValueOrThrow("creatorId").asLong
|
||||
} catch (e: NumberFormatException) {
|
||||
throw JsonParseException("`creatorId` must be a number.")
|
||||
}.apply {
|
||||
if (this < 0) {
|
||||
throw JsonParseException("`creatorId` must be a positive number.")
|
||||
}
|
||||
}
|
||||
|
||||
return BotAccount(name, token, creatorId)
|
||||
}
|
||||
|
||||
}
|
522
scalabot-meta/src/test/kotlin/ConfigsTest.kt
Normal file
522
scalabot-meta/src/test/kotlin/ConfigsTest.kt
Normal file
@ -0,0 +1,522 @@
|
||||
package net.lamgc.scalabot.config
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonObject
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import net.lamgc.scalabot.config.serializer.*
|
||||
import org.eclipse.aether.artifact.Artifact
|
||||
import org.eclipse.aether.artifact.DefaultArtifact
|
||||
import org.eclipse.aether.repository.Authentication
|
||||
import org.eclipse.aether.repository.AuthenticationContext
|
||||
import org.eclipse.aether.repository.Proxy
|
||||
import org.intellij.lang.annotations.Language
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.test.*
|
||||
|
||||
internal class BotAccountTest {
|
||||
|
||||
@Test
|
||||
fun `id getter`() {
|
||||
val accountId = abs(Random().nextInt()).toLong()
|
||||
Assertions.assertEquals(accountId, BotAccount("Test", "${accountId}:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10", 0).id)
|
||||
}
|
||||
|
||||
private val gson = GsonBuilder()
|
||||
.create()
|
||||
|
||||
@Test
|
||||
fun deserializerTest() {
|
||||
val accountId = abs(Random().nextInt()).toLong()
|
||||
val creatorId = abs(Random().nextInt()).toLong()
|
||||
val botAccountJsonObject = gson.fromJson(
|
||||
"""
|
||||
{
|
||||
"name": "TestBot",
|
||||
"token": "${accountId}:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10",
|
||||
"creatorId": $creatorId
|
||||
}
|
||||
""".trimIndent(), JsonObject::class.java
|
||||
)
|
||||
val botAccount = Gson().fromJson(botAccountJsonObject, BotAccount::class.java)
|
||||
assertEquals(accountId, botAccount.id)
|
||||
assertEquals("TestBot", botAccount.name)
|
||||
assertEquals(creatorId, botAccount.creatorId)
|
||||
assertEquals("${accountId}:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10", botAccount.token)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializerTest() {
|
||||
val accountId = abs(Random().nextInt()).toLong()
|
||||
val creatorId = abs(Random().nextInt()).toLong()
|
||||
val botAccount = BotAccount("TestBot", "${accountId}:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10", creatorId)
|
||||
val botAccountJsonObject = gson.toJsonTree(botAccount)
|
||||
assertTrue(botAccountJsonObject is JsonObject)
|
||||
assertEquals(botAccount.name, botAccountJsonObject["name"].asString)
|
||||
assertEquals(botAccount.token, botAccountJsonObject["token"].asString)
|
||||
assertNull(botAccountJsonObject["id"])
|
||||
Assertions.assertEquals(creatorId, botAccountJsonObject["creatorId"].asLong)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class BotConfigTest {
|
||||
|
||||
private val gson = GsonBuilder()
|
||||
.registerTypeAdapter(BotConfig::class.java, BotConfigSerializer)
|
||||
.registerTypeAdapter(Artifact::class.java, ArtifactSerializer)
|
||||
.registerTypeAdapter(ProxyConfig::class.java, ProxyConfigSerializer)
|
||||
.create()
|
||||
|
||||
@Test
|
||||
fun `json serialize`() {
|
||||
val minimumExpectConfig = BotConfig(
|
||||
account = BotAccount(
|
||||
name = "TestBot",
|
||||
token = "123456789:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10",
|
||||
creatorId = 123456789L
|
||||
),
|
||||
)
|
||||
|
||||
val json = gson.toJsonTree(minimumExpectConfig)
|
||||
assertTrue(json is JsonObject)
|
||||
assertEquals(minimumExpectConfig.enabled, json.get("enabled").asBoolean)
|
||||
|
||||
assertEquals(minimumExpectConfig.account.name, json.get("account").asJsonObject.get("name").asString)
|
||||
assertEquals(minimumExpectConfig.account.token, json.get("account").asJsonObject.get("token").asString)
|
||||
assertEquals(minimumExpectConfig.account.creatorId, json.get("account").asJsonObject.get("creatorId").asLong)
|
||||
assertNull(json.get("account").asJsonObject.get("id"))
|
||||
|
||||
assertEquals(minimumExpectConfig.proxy.host, json.get("proxy").asJsonObject.get("host").asString)
|
||||
assertEquals(minimumExpectConfig.proxy.port, json.get("proxy").asJsonObject.get("port").asInt)
|
||||
assertEquals(minimumExpectConfig.proxy.type.name, json.get("proxy").asJsonObject.get("type").asString)
|
||||
|
||||
assertEquals(minimumExpectConfig.disableBuiltInAbility, json.get("disableBuiltInAbility").asBoolean)
|
||||
assertEquals(minimumExpectConfig.autoUpdateCommandList, json.get("autoUpdateCommandList").asBoolean)
|
||||
|
||||
assertNotNull(json.get("extensions"))
|
||||
assertTrue(json.get("extensions").isJsonArray)
|
||||
assertTrue(json.get("extensions").asJsonArray.isEmpty)
|
||||
|
||||
assertEquals(minimumExpectConfig.baseApiUrl, json.get("baseApiUrl").asString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `json deserialize`() {
|
||||
val expectExtensionArtifact = DefaultArtifact("org.example.test:test-extension:1.0.0")
|
||||
@Language("JSON5") val looksGoodJson = """
|
||||
{
|
||||
"enabled": false,
|
||||
"account": {
|
||||
"name": "TestBot",
|
||||
"token": "123456789:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10",
|
||||
"creatorId": 123456789
|
||||
},
|
||||
"proxy": {
|
||||
"host": "localhost",
|
||||
"port": 8080,
|
||||
"type": "HTTP"
|
||||
},
|
||||
"disableBuiltInAbility": false,
|
||||
"autoUpdateCommandList": true,
|
||||
"extensions": [
|
||||
"$expectExtensionArtifact"
|
||||
],
|
||||
"baseApiUrl": "http://localhost:8080"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val actualConfig = gson.fromJson(looksGoodJson, BotConfig::class.java)
|
||||
|
||||
assertEquals(false, actualConfig.enabled)
|
||||
|
||||
assertEquals("TestBot", actualConfig.account.name)
|
||||
assertEquals("123456789:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10", actualConfig.account.token)
|
||||
assertEquals(123456789L, actualConfig.account.creatorId)
|
||||
|
||||
assertEquals("localhost", actualConfig.proxy.host)
|
||||
assertEquals(8080, actualConfig.proxy.port)
|
||||
assertEquals(ProxyType.HTTP, actualConfig.proxy.type)
|
||||
|
||||
assertEquals(false, actualConfig.disableBuiltInAbility)
|
||||
assertEquals(true, actualConfig.autoUpdateCommandList)
|
||||
|
||||
assertEquals(1, actualConfig.extensions.size)
|
||||
assertEquals(expectExtensionArtifact, actualConfig.extensions.first())
|
||||
|
||||
assertEquals("http://localhost:8080", actualConfig.baseApiUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `json deserialize - minimum parameters`() {
|
||||
@Language("JSON5") val minimumLooksGoodJson = """
|
||||
{
|
||||
"account": {
|
||||
"name": "TestBot",
|
||||
"token": "123456789:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10",
|
||||
"creatorId": 123456789
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val expectDefaultConfig = BotConfig(account = BotAccount("Test", "Test", 0))
|
||||
val actualMinimumConfig = gson.fromJson(minimumLooksGoodJson, BotConfig::class.java)
|
||||
assertNotNull(actualMinimumConfig)
|
||||
assertEquals("TestBot", actualMinimumConfig.account.name)
|
||||
assertEquals("123456789:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10", actualMinimumConfig.account.token)
|
||||
assertEquals(123456789, actualMinimumConfig.account.creatorId)
|
||||
|
||||
assertEquals(expectDefaultConfig.enabled, actualMinimumConfig.enabled)
|
||||
assertEquals(expectDefaultConfig.disableBuiltInAbility, actualMinimumConfig.disableBuiltInAbility)
|
||||
assertEquals(expectDefaultConfig.autoUpdateCommandList, actualMinimumConfig.autoUpdateCommandList)
|
||||
assertEquals(expectDefaultConfig.proxy, actualMinimumConfig.proxy)
|
||||
assertEquals(expectDefaultConfig.baseApiUrl, actualMinimumConfig.baseApiUrl)
|
||||
|
||||
assertTrue(expectDefaultConfig.extensions.containsAll(actualMinimumConfig.extensions))
|
||||
assertTrue(actualMinimumConfig.extensions.containsAll(expectDefaultConfig.extensions))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class ProxyConfigTest {
|
||||
|
||||
private val gson = GsonBuilder()
|
||||
.registerTypeAdapter(ProxyConfig::class.java, ProxyConfigSerializer)
|
||||
.registerTypeAdapter(ProxyType::class.java, ProxyTypeSerializer)
|
||||
.create()
|
||||
|
||||
@Test
|
||||
fun `json serialize`() {
|
||||
val proxyConfig = ProxyConfig(
|
||||
host = "localhost",
|
||||
port = 8080,
|
||||
type = ProxyType.HTTP
|
||||
)
|
||||
val json = gson.toJsonTree(proxyConfig)
|
||||
assertTrue(json is JsonObject)
|
||||
assertEquals(proxyConfig.host, json.get("host").asString)
|
||||
assertEquals(proxyConfig.port, json.get("port").asInt)
|
||||
assertEquals(proxyConfig.type.name, json.get("type").asString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `json deserialize`() {
|
||||
@Language("JSON5") val looksGoodJson = """
|
||||
{
|
||||
"host": "localhost",
|
||||
"port": 8080,
|
||||
"type": "HTTP"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val actualConfig = gson.fromJson(looksGoodJson, ProxyConfig::class.java)
|
||||
|
||||
assertEquals("localhost", actualConfig.host)
|
||||
assertEquals(8080, actualConfig.port)
|
||||
assertEquals(ProxyType.HTTP, actualConfig.type)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toString test`() {
|
||||
assertEquals("NO_PROXY", ProxyConfig(ProxyType.NO_PROXY).toString())
|
||||
assertEquals("HTTP://example.org:1008", ProxyConfig(ProxyType.HTTP, "example.org", 1008).toString())
|
||||
}
|
||||
}
|
||||
|
||||
internal class MetricsConfigTest {
|
||||
|
||||
private val gson = GsonBuilder()
|
||||
.registerTypeAdapter(UsernameAuthenticator::class.java, UsernameAuthenticatorSerializer)
|
||||
.create()
|
||||
|
||||
@Test
|
||||
fun `json serializer`() {
|
||||
val config = MetricsConfig(
|
||||
enable = true,
|
||||
port = 8800,
|
||||
bindAddress = "127.0.0.1",
|
||||
authenticator = UsernameAuthenticator(
|
||||
username = "username",
|
||||
password = "password"
|
||||
)
|
||||
)
|
||||
|
||||
val json = gson.toJsonTree(config).asJsonObject
|
||||
|
||||
assertEquals(config.enable, json.get("enable").asBoolean)
|
||||
assertEquals(config.port, json.get("port").asInt)
|
||||
assertEquals(config.bindAddress, json.get("bindAddress").asString)
|
||||
assertNotNull(config.authenticator)
|
||||
assertTrue(
|
||||
config.authenticator!!.checkCredentials(
|
||||
json.get("authenticator").asJsonObject.get("username").asString,
|
||||
json.get("authenticator").asJsonObject.get("password").asString
|
||||
)
|
||||
)
|
||||
|
||||
val expectDefaultValueConfig = MetricsConfig()
|
||||
val defaultValueJson = gson.toJsonTree(expectDefaultValueConfig).asJsonObject
|
||||
assertEquals(expectDefaultValueConfig.enable, defaultValueJson.get("enable").asBoolean)
|
||||
assertEquals(expectDefaultValueConfig.port, defaultValueJson.get("port").asInt)
|
||||
assertEquals(expectDefaultValueConfig.bindAddress, defaultValueJson.get("bindAddress").asString)
|
||||
assertNull(defaultValueJson.get("authenticator"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `json deserializer`() {
|
||||
val json = """
|
||||
{
|
||||
"enable": true,
|
||||
"port": 8800,
|
||||
"bindAddress": "127.0.0.1",
|
||||
"authenticator": {
|
||||
"username": "username",
|
||||
"password": "password"
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val config = gson.fromJson(json, MetricsConfig::class.java)
|
||||
assertEquals(true, config.enable)
|
||||
assertEquals(8800, config.port)
|
||||
assertEquals("127.0.0.1", config.bindAddress)
|
||||
assertNotNull(config.authenticator)
|
||||
assertTrue(config.authenticator!!.checkCredentials("username", "password"))
|
||||
|
||||
val defaultValueConfig = MetricsConfig()
|
||||
val defaultValueJson = gson.toJsonTree(defaultValueConfig).asJsonObject
|
||||
assertEquals(defaultValueConfig.enable, defaultValueJson.get("enable").asBoolean)
|
||||
assertEquals(defaultValueConfig.port, defaultValueJson.get("port").asInt)
|
||||
assertEquals(defaultValueConfig.bindAddress, defaultValueJson.get("bindAddress").asString)
|
||||
assertNull(defaultValueJson.get("authenticator"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `json deserializer - default value`() {
|
||||
val actualConfig = gson.fromJson("{}", MetricsConfig::class.java)
|
||||
val expectConfig = MetricsConfig()
|
||||
|
||||
assertEquals(expectConfig, actualConfig)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class MavenRepositoryConfigTest {
|
||||
|
||||
private val gson = GsonBuilder()
|
||||
.registerTypeAdapter(MavenRepositoryConfig::class.java, MavenRepositoryConfigSerializer)
|
||||
.registerTypeAdapter(Authentication::class.java, AuthenticationSerializer)
|
||||
.create()
|
||||
|
||||
@Test
|
||||
fun `json serializer`() {
|
||||
val config = MavenRepositoryConfig(
|
||||
id = "test",
|
||||
url = URL("http://localhost:8080/repository"),
|
||||
proxy = Proxy(
|
||||
"http",
|
||||
"localhost",
|
||||
8080,
|
||||
),
|
||||
layout = "legacy",
|
||||
enableReleases = false,
|
||||
enableSnapshots = true,
|
||||
authentication = null
|
||||
)
|
||||
val json = gson.toJsonTree(config).asJsonObject
|
||||
assertEquals(config.id, json.get("id").asString)
|
||||
assertEquals(config.url.toString(), json.get("url").asString)
|
||||
|
||||
assertEquals(config.proxy!!.host, json.get("proxy").asJsonObject.get("host").asString)
|
||||
assertEquals(config.proxy!!.port, json.get("proxy").asJsonObject.get("port").asInt)
|
||||
assertEquals(config.proxy!!.type, json.get("proxy").asJsonObject.get("type").asString)
|
||||
|
||||
assertEquals(config.layout, json.get("layout").asString)
|
||||
assertEquals(config.enableReleases, json.get("enableReleases").asBoolean)
|
||||
assertEquals(config.enableSnapshots, json.get("enableSnapshots").asBoolean)
|
||||
assertNull(json.get("authentication"))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `json deserializer`() {
|
||||
@Language("JSON5")
|
||||
val json = """
|
||||
{
|
||||
"id": "test",
|
||||
"url": "http://localhost:8080/repository",
|
||||
"proxy": {
|
||||
"host": "localhost",
|
||||
"port": 8080,
|
||||
"type": "HTTP"
|
||||
},
|
||||
"layout": "legacy",
|
||||
"enableReleases": false,
|
||||
"enableSnapshots": true,
|
||||
"authentication": {
|
||||
"username": "testUser",
|
||||
"password": "testPassword"
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val config = gson.fromJson(json, MavenRepositoryConfig::class.java)
|
||||
|
||||
assertEquals("test", config.id)
|
||||
assertEquals(URL("http://localhost:8080/repository"), config.url)
|
||||
assertEquals(
|
||||
Proxy(
|
||||
"HTTP",
|
||||
"localhost",
|
||||
8080
|
||||
), config.proxy
|
||||
)
|
||||
assertEquals("legacy", config.layout)
|
||||
assertEquals(false, config.enableReleases)
|
||||
assertEquals(true, config.enableSnapshots)
|
||||
assertNotNull(config.authentication)
|
||||
|
||||
val authContext = mockk<AuthenticationContext> {
|
||||
every { put(ofType(String::class), any()) } answers { }
|
||||
}
|
||||
config.authentication!!.fill(authContext, null, emptyMap())
|
||||
|
||||
verify {
|
||||
authContext.put(any(), "testUser")
|
||||
authContext.put(any(), "testPassword".toCharArray())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class AppConfigTest {
|
||||
|
||||
private val gson = GsonBuilder()
|
||||
.registerTypeAdapter(ProxyType::class.java, ProxyTypeSerializer)
|
||||
.registerTypeAdapter(ProxyConfig::class.java, ProxyConfigSerializer)
|
||||
.registerTypeAdapter(UsernameAuthenticator::class.java, UsernameAuthenticatorSerializer)
|
||||
.registerTypeAdapter(MavenRepositoryConfig::class.java, MavenRepositoryConfigSerializer)
|
||||
.create()
|
||||
|
||||
@Test
|
||||
fun `json serializer - default value`() {
|
||||
val config = AppConfig()
|
||||
val json = gson.toJsonTree(config).asJsonObject
|
||||
assertEquals(config.proxy.type.name, json.get("proxy").asJsonObject.get("type").asString)
|
||||
assertEquals(config.proxy.host, json.get("proxy").asJsonObject.get("host").asString)
|
||||
assertEquals(config.proxy.port, json.get("proxy").asJsonObject.get("port").asInt)
|
||||
assertEquals(config.metrics.enable, json.get("metrics").asJsonObject.get("enable").asBoolean)
|
||||
assertEquals(config.metrics.port, json.get("metrics").asJsonObject.get("port").asInt)
|
||||
assertEquals(config.metrics.bindAddress, json.get("metrics").asJsonObject.get("bindAddress").asString)
|
||||
assertNull(json["metrics"].asJsonObject.get("authenticator"))
|
||||
assertTrue(json["mavenRepositories"].asJsonArray.isEmpty)
|
||||
assertNull(json["mavenLocalRepository"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `json serializer - Provide values`() {
|
||||
val config = AppConfig(
|
||||
proxy = ProxyConfig(
|
||||
type = ProxyType.HTTP,
|
||||
host = "localhost",
|
||||
port = 8080
|
||||
),
|
||||
metrics = MetricsConfig(
|
||||
enable = true,
|
||||
port = 8800,
|
||||
bindAddress = "127.0.0.1",
|
||||
authenticator = UsernameAuthenticator(
|
||||
username = "username",
|
||||
password = "password"
|
||||
)
|
||||
),
|
||||
mavenRepositories = listOf(
|
||||
MavenRepositoryConfig(
|
||||
url = URL("https://repository.maven.apache.org/maven2/")
|
||||
)
|
||||
),
|
||||
mavenLocalRepository = "file:///tmp/maven-local-repository"
|
||||
)
|
||||
|
||||
val json = gson.toJsonTree(config).asJsonObject
|
||||
|
||||
assertEquals(config.proxy.type.name, json.get("proxy").asJsonObject.get("type").asString)
|
||||
assertEquals(config.proxy.host, json.get("proxy").asJsonObject.get("host").asString)
|
||||
assertEquals(config.proxy.port, json.get("proxy").asJsonObject.get("port").asInt)
|
||||
|
||||
assertEquals(config.metrics.enable, json.get("metrics").asJsonObject.get("enable").asBoolean)
|
||||
assertEquals(config.metrics.port, json.get("metrics").asJsonObject.get("port").asInt)
|
||||
assertEquals(config.metrics.bindAddress, json.get("metrics").asJsonObject.get("bindAddress").asString)
|
||||
assertNotNull(config.metrics.authenticator)
|
||||
assertTrue(
|
||||
config.metrics.authenticator!!.checkCredentials(
|
||||
json.get("metrics").asJsonObject.get("authenticator").asJsonObject.get("username").asString,
|
||||
json.get("metrics").asJsonObject.get("authenticator").asJsonObject.get("password").asString
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(1, json["mavenRepositories"].asJsonArray.size())
|
||||
assertEquals(
|
||||
config.mavenRepositories[0].url.toString(),
|
||||
json["mavenRepositories"].asJsonArray[0].asJsonObject.get("url").asString
|
||||
)
|
||||
|
||||
assertEquals(config.mavenLocalRepository, json["mavenLocalRepository"].asString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `json deserializer - complete`() {
|
||||
val json = """
|
||||
{
|
||||
"proxy": {
|
||||
"type": "HTTP",
|
||||
"host": "localhost",
|
||||
"port": 8080
|
||||
},
|
||||
"metrics": {
|
||||
"enable": true,
|
||||
"port": 8800,
|
||||
"bindAddress": "127.0.0.1",
|
||||
"authenticator": {
|
||||
"username": "username",
|
||||
"password": "password"
|
||||
}
|
||||
},
|
||||
"mavenRepositories": [
|
||||
{
|
||||
"url": "https://repository.maven.apache.org/maven2/"
|
||||
}
|
||||
],
|
||||
"mavenLocalRepository": "file:///tmp/maven-local-repository"
|
||||
}
|
||||
""".trimIndent()
|
||||
val config = gson.fromJson(json, AppConfig::class.java)
|
||||
|
||||
assertEquals(ProxyType.HTTP, config.proxy.type)
|
||||
assertEquals("localhost", config.proxy.host)
|
||||
assertEquals(8080, config.proxy.port)
|
||||
|
||||
assertEquals(true, config.metrics.enable)
|
||||
assertEquals(8800, config.metrics.port)
|
||||
assertEquals("127.0.0.1", config.metrics.bindAddress)
|
||||
assertNotNull(config.metrics.authenticator)
|
||||
assertTrue(config.metrics.authenticator!!.checkCredentials("username", "password"))
|
||||
|
||||
assertEquals(1, config.mavenRepositories.size)
|
||||
assertEquals(URL("https://repository.maven.apache.org/maven2/"), config.mavenRepositories[0].url)
|
||||
|
||||
assertEquals("file:///tmp/maven-local-repository", config.mavenLocalRepository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `json deserializer - default value`() {
|
||||
val actualConfig = gson.fromJson("{}", AppConfig::class.java)
|
||||
val expectConfig = AppConfig()
|
||||
|
||||
assertEquals(expectConfig, actualConfig)
|
||||
}
|
||||
|
||||
}
|
38
scalabot-meta/src/test/kotlin/UsernameAuthenticatorTest.kt
Normal file
38
scalabot-meta/src/test/kotlin/UsernameAuthenticatorTest.kt
Normal file
@ -0,0 +1,38 @@
|
||||
package net.lamgc.scalabot.config
|
||||
|
||||
import kotlin.test.*
|
||||
|
||||
internal class UsernameAuthenticatorTest {
|
||||
|
||||
@Test
|
||||
fun checkCredentialsTest() {
|
||||
val authenticator = UsernameAuthenticator("testUser", "testPassword")
|
||||
assertTrue(authenticator.checkCredentials("testUser", "testPassword"))
|
||||
assertFalse(authenticator.checkCredentials("falseUser", "testPassword"))
|
||||
assertFalse(authenticator.checkCredentials("testUser", "falsePassword"))
|
||||
assertFalse(authenticator.checkCredentials("falseUser", "falsePassword"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun toJsonObjectTest() {
|
||||
val authenticator = UsernameAuthenticator("testUser", "testPassword")
|
||||
val jsonObject = authenticator.toJsonObject()
|
||||
assertEquals("testUser", jsonObject["username"]?.asString)
|
||||
assertEquals("testPassword", jsonObject["password"]?.asString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun equalsTest() {
|
||||
val authenticator = UsernameAuthenticator("testUser", "testPassword")
|
||||
assertEquals(authenticator, UsernameAuthenticator("testUser", "testPassword"))
|
||||
assertEquals(authenticator.hashCode(), UsernameAuthenticator("testUser", "testPassword").hashCode())
|
||||
assertNotEquals(authenticator, UsernameAuthenticator("testUser", "falsePassword"))
|
||||
assertNotEquals(authenticator.hashCode(), UsernameAuthenticator("testUser", "falsePassword").hashCode())
|
||||
assertNotEquals(authenticator, UsernameAuthenticator("falseUser", "testPassword"))
|
||||
assertNotEquals(authenticator.hashCode(), UsernameAuthenticator("falseUser", "testPassword").hashCode())
|
||||
assertNotEquals(authenticator, UsernameAuthenticator("falseUser", "falsePassword"))
|
||||
assertNotEquals(authenticator.hashCode(), UsernameAuthenticator("falseUser", "falsePassword").hashCode())
|
||||
assertFalse(authenticator.equals(null))
|
||||
}
|
||||
|
||||
}
|
852
scalabot-meta/src/test/kotlin/serializer/SerializeUtilsTest.kt
Normal file
852
scalabot-meta/src/test/kotlin/serializer/SerializeUtilsTest.kt
Normal file
@ -0,0 +1,852 @@
|
||||
package net.lamgc.scalabot.config.serializer
|
||||
|
||||
import com.google.gson.*
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import net.lamgc.scalabot.config.*
|
||||
import net.lamgc.scalabot.config.serializer.SerializeUtils.getPrimitiveValueOrThrow
|
||||
import org.eclipse.aether.artifact.Artifact
|
||||
import org.eclipse.aether.artifact.DefaultArtifact
|
||||
import org.eclipse.aether.repository.Authentication
|
||||
import org.eclipse.aether.repository.AuthenticationContext
|
||||
import org.eclipse.aether.repository.Proxy
|
||||
import org.intellij.lang.annotations.Language
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import java.lang.reflect.Type
|
||||
import java.net.URL
|
||||
import kotlin.test.*
|
||||
|
||||
internal class SerializeUtilsTest {
|
||||
|
||||
@Test
|
||||
fun `getPrimitiveValueOrThrow test`() {
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
JsonObject().getPrimitiveValueOrThrow("NOT_EXIST_KEY")
|
||||
}
|
||||
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
JsonObject().apply {
|
||||
add("testKey", JsonArray())
|
||||
}.getPrimitiveValueOrThrow("testKey")
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
JsonObject().apply {
|
||||
add("testKey", JsonObject())
|
||||
}.getPrimitiveValueOrThrow("testKey")
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
JsonObject().apply {
|
||||
add("testKey", JsonNull.INSTANCE)
|
||||
}.getPrimitiveValueOrThrow("testKey")
|
||||
}
|
||||
|
||||
val expectKey = "STRING_KEY"
|
||||
val expectValue = JsonPrimitive("A STRING")
|
||||
assertEquals(expectValue, JsonObject()
|
||||
.apply { add(expectKey, expectValue) }
|
||||
.getPrimitiveValueOrThrow(expectKey))
|
||||
}
|
||||
}
|
||||
|
||||
internal class ProxyTypeSerializerTest {
|
||||
|
||||
@Test
|
||||
fun `serialize test`() {
|
||||
for (type in ProxyType.values()) {
|
||||
assertEquals(
|
||||
JsonPrimitive(type.name), ProxyTypeSerializer.serialize(type, null, null),
|
||||
"ProxyType 序列化结果与预期不符."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize test`() {
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
ProxyTypeSerializer.deserialize(JsonObject(), null, null)
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
ProxyTypeSerializer.deserialize(JsonArray(), null, null)
|
||||
}
|
||||
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
ProxyTypeSerializer.deserialize(JsonPrimitive("NOT_IN_ENUM_VALUE"), null, null)
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
ProxyType.NO_PROXY,
|
||||
ProxyTypeSerializer.deserialize(JsonNull.INSTANCE, null, null)
|
||||
)
|
||||
|
||||
for (type in ProxyType.values()) {
|
||||
assertEquals(
|
||||
type, ProxyTypeSerializer.deserialize(JsonPrimitive(type.name), null, null),
|
||||
"ProxyType 反序列化结果与预期不符."
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
type, ProxyTypeSerializer.deserialize(JsonPrimitive(" ${type.name} "), null, null),
|
||||
"ProxyType 反序列化时未对 Json 字符串进行修剪(trim)."
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class MavenRepositoryConfigSerializerTest {
|
||||
|
||||
@Test
|
||||
fun `unsupported json type deserialize test`() {
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
MavenRepositoryConfigSerializer.deserialize(
|
||||
JsonArray(),
|
||||
MavenRepositoryConfig::class.java,
|
||||
TestJsonSerializationContext.default()
|
||||
)
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
MavenRepositoryConfigSerializer.deserialize(
|
||||
JsonNull.INSTANCE,
|
||||
MavenRepositoryConfig::class.java,
|
||||
TestJsonSerializationContext.default()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `json primitive deserialize test`() {
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
MavenRepositoryConfigSerializer.deserialize(
|
||||
JsonPrimitive("NOT A URL."),
|
||||
MavenRepositoryConfig::class.java,
|
||||
TestJsonSerializationContext.default()
|
||||
)
|
||||
}
|
||||
|
||||
val expectRepoUrl = "https://repo.example.org/maven"
|
||||
val config = MavenRepositoryConfigSerializer.deserialize(
|
||||
JsonPrimitive(expectRepoUrl),
|
||||
MavenRepositoryConfig::class.java,
|
||||
TestJsonSerializationContext.default()
|
||||
)
|
||||
|
||||
assertNull(config.id)
|
||||
assertEquals(URL(expectRepoUrl), config.url)
|
||||
assertNull(config.proxy, "Proxy 默认值不为 null.")
|
||||
assertEquals("default", config.layout)
|
||||
assertTrue(config.enableReleases)
|
||||
assertTrue(config.enableSnapshots)
|
||||
assertNull(config.authentication)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `json object default deserialize test`() {
|
||||
val expectRepoUrl = "https://repo.example.org/maven"
|
||||
val jsonObject = JsonObject()
|
||||
jsonObject.addProperty("url", expectRepoUrl)
|
||||
val config = MavenRepositoryConfigSerializer.deserialize(
|
||||
jsonObject,
|
||||
MavenRepositoryConfig::class.java,
|
||||
TestJsonSerializationContext.default()
|
||||
)
|
||||
|
||||
assertNull(config.id)
|
||||
assertEquals(URL(expectRepoUrl), config.url)
|
||||
assertNull(config.proxy, "Proxy 默认值不为 null.")
|
||||
assertEquals("default", config.layout)
|
||||
assertTrue(config.enableReleases)
|
||||
assertTrue(config.enableSnapshots)
|
||||
assertNull(config.authentication)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `json object deserialize test`() {
|
||||
@Language("JSON5")
|
||||
val looksGoodJsonString = """
|
||||
{
|
||||
"id": "test-repository",
|
||||
"url": "https://repo.example.org/maven",
|
||||
"proxy": {
|
||||
"type": "http",
|
||||
"host": "127.0.1.1",
|
||||
"port": 10800
|
||||
},
|
||||
"layout": "default",
|
||||
"enableReleases": false,
|
||||
"enableSnapshots": true
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val jsonObject = Gson().fromJson(looksGoodJsonString, JsonObject::class.java)
|
||||
var config = MavenRepositoryConfigSerializer.deserialize(
|
||||
jsonObject,
|
||||
MavenRepositoryConfig::class.java,
|
||||
TestJsonSerializationContext.default()
|
||||
)
|
||||
|
||||
assertEquals(jsonObject["id"].asString, config.id)
|
||||
assertEquals(URL(jsonObject["url"].asString), config.url)
|
||||
assertEquals(Proxy("http", "127.0.1.1", 10800), config.proxy)
|
||||
assertEquals(jsonObject["layout"].asString, config.layout)
|
||||
assertEquals(jsonObject["enableReleases"].asBoolean, config.enableReleases)
|
||||
assertEquals(jsonObject["enableSnapshots"].asBoolean, config.enableSnapshots)
|
||||
|
||||
// ------------------------------------
|
||||
|
||||
jsonObject.add("proxy", JsonNull.INSTANCE)
|
||||
jsonObject.remove("layout")
|
||||
|
||||
config = MavenRepositoryConfigSerializer.deserialize(
|
||||
jsonObject,
|
||||
MavenRepositoryConfig::class.java,
|
||||
TestJsonSerializationContext.default()
|
||||
)
|
||||
|
||||
assertEquals(jsonObject["id"].asString, config.id)
|
||||
assertEquals(URL(jsonObject["url"].asString), config.url)
|
||||
assertNull(config.proxy)
|
||||
assertEquals("default", config.layout)
|
||||
assertEquals(jsonObject["enableReleases"].asBoolean, config.enableReleases)
|
||||
assertEquals(jsonObject["enableSnapshots"].asBoolean, config.enableSnapshots)
|
||||
|
||||
// ------------------------------------
|
||||
|
||||
jsonObject.add("layout", mockk<JsonPrimitive> {
|
||||
every { asString }.returns(null)
|
||||
})
|
||||
|
||||
config = MavenRepositoryConfigSerializer.deserialize(
|
||||
jsonObject,
|
||||
MavenRepositoryConfig::class.java,
|
||||
TestJsonSerializationContext.default()
|
||||
)
|
||||
|
||||
assertEquals(jsonObject["id"].asString, config.id)
|
||||
assertEquals(URL(jsonObject["url"].asString), config.url)
|
||||
assertNull(config.proxy)
|
||||
assertEquals("default", config.layout)
|
||||
assertEquals(jsonObject["enableReleases"].asBoolean, config.enableReleases)
|
||||
assertEquals(jsonObject["enableSnapshots"].asBoolean, config.enableSnapshots)
|
||||
assertNull(config.authentication)
|
||||
|
||||
// ------------------------------------
|
||||
|
||||
jsonObject.add("authentication", JsonObject().apply {
|
||||
addProperty("username", "testUsername")
|
||||
addProperty("password", "testPassword")
|
||||
})
|
||||
|
||||
config = MavenRepositoryConfigSerializer.deserialize(
|
||||
jsonObject,
|
||||
MavenRepositoryConfig::class.java,
|
||||
TestJsonSerializationContext.default()
|
||||
)
|
||||
|
||||
assertEquals(jsonObject["id"].asString, config.id)
|
||||
assertEquals(URL(jsonObject["url"].asString), config.url)
|
||||
assertNull(config.proxy)
|
||||
assertEquals("default", config.layout)
|
||||
assertEquals(jsonObject["enableReleases"].asBoolean, config.enableReleases)
|
||||
assertEquals(jsonObject["enableSnapshots"].asBoolean, config.enableSnapshots)
|
||||
assertNotNull(config.authentication)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class TestJsonSerializationContext(private val gson: Gson) : JsonDeserializationContext,
|
||||
JsonSerializationContext {
|
||||
|
||||
override fun <T : Any?> deserialize(json: JsonElement?, typeOfT: Type): T {
|
||||
return gson.fromJson(json, typeOfT)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun default(): TestJsonSerializationContext {
|
||||
return TestJsonSerializationContext(
|
||||
GsonBuilder()
|
||||
.registerTypeAdapter(MavenRepositoryConfig::class.java, MavenRepositoryConfigSerializer)
|
||||
.registerTypeAdapter(BotConfig::class.java, BotConfigSerializer)
|
||||
.registerTypeAdapter(ProxyType::class.java, ProxyTypeSerializer)
|
||||
.registerTypeAdapter(Artifact::class.java, ArtifactSerializer)
|
||||
.registerTypeAdapter(Authentication::class.java, AuthenticationSerializer)
|
||||
.registerTypeAdapter(UsernameAuthenticator::class.java, UsernameAuthenticatorSerializer)
|
||||
.registerTypeAdapter(ProxyConfig::class.java, ProxyConfigSerializer)
|
||||
.create()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(src: Any?): JsonElement {
|
||||
return gson.toJsonTree(src)
|
||||
}
|
||||
|
||||
override fun serialize(src: Any?, typeOfSrc: Type?): JsonElement {
|
||||
return gson.toJsonTree(src, typeOfSrc)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class AuthenticationSerializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize test`() {
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
AuthenticationSerializer.deserialize(
|
||||
JsonNull.INSTANCE,
|
||||
Authentication::class.java, TestJsonSerializationContext.default()
|
||||
)
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
AuthenticationSerializer.deserialize(
|
||||
JsonArray(),
|
||||
Authentication::class.java, TestJsonSerializationContext.default()
|
||||
)
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
AuthenticationSerializer.deserialize(
|
||||
JsonPrimitive("A STRING"),
|
||||
Authentication::class.java, TestJsonSerializationContext.default()
|
||||
)
|
||||
}
|
||||
|
||||
val expectJsonObject = JsonObject().apply {
|
||||
addProperty("username", "testUsername")
|
||||
addProperty("password", "testPassword")
|
||||
}
|
||||
|
||||
val mockContext = mockk<AuthenticationContext> {
|
||||
every { put(any(), any()) }.answers { }
|
||||
}
|
||||
|
||||
val result = AuthenticationSerializer.deserialize(
|
||||
expectJsonObject,
|
||||
Authentication::class.java, TestJsonSerializationContext.default()
|
||||
)
|
||||
|
||||
assertNotNull(result)
|
||||
result.fill(mockContext, "username", null)
|
||||
result.fill(mockContext, "password", null)
|
||||
|
||||
verify {
|
||||
mockContext.put("username", "testUsername")
|
||||
mockContext.put("password", "testPassword".toCharArray())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class BotConfigSerializerTest {
|
||||
|
||||
private val gson = GsonBuilder()
|
||||
.registerTypeAdapter(BotConfig::class.java, BotConfigSerializer)
|
||||
.registerTypeAdapter(Artifact::class.java, ArtifactSerializer)
|
||||
.registerTypeAdapter(ProxyType::class.java, ProxyTypeSerializer)
|
||||
.registerTypeAdapter(ProxyConfig::class.java, ProxyConfigSerializer)
|
||||
.create()
|
||||
|
||||
@Test
|
||||
fun `serializer test`() {
|
||||
// 检查 BotConfig 的序列化
|
||||
val botConfig = BotConfig(
|
||||
account = BotAccount(
|
||||
name = "test-bot",
|
||||
token = "test-token",
|
||||
creatorId = 10000
|
||||
)
|
||||
)
|
||||
|
||||
// 使用 gson 序列化 botConfig, 并检查序列化结果
|
||||
val jsonObject = gson.toJsonTree(botConfig) as JsonObject
|
||||
assertEquals("test-bot", jsonObject["account"].asJsonObject["name"].asString)
|
||||
assertEquals("test-token", jsonObject["account"].asJsonObject["token"].asString)
|
||||
assertEquals(10000, jsonObject["account"].asJsonObject["creatorId"].asInt)
|
||||
|
||||
assertEquals(botConfig.enabled, jsonObject["enabled"].asBoolean)
|
||||
assertEquals(botConfig.proxy.host, jsonObject["proxy"].asJsonObject["host"].asString)
|
||||
assertEquals(botConfig.proxy.port, jsonObject["proxy"].asJsonObject["port"].asInt)
|
||||
assertEquals(botConfig.proxy.type.name, jsonObject["proxy"].asJsonObject["type"].asString)
|
||||
assertEquals(botConfig.disableBuiltInAbility, jsonObject["disableBuiltInAbility"].asBoolean)
|
||||
assertEquals(botConfig.autoUpdateCommandList, jsonObject["autoUpdateCommandList"].asBoolean)
|
||||
assertEquals(botConfig.extensions.isEmpty(), jsonObject["extensions"].asJsonArray.isEmpty)
|
||||
assertEquals(botConfig.baseApiUrl, jsonObject["baseApiUrl"].asString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize test`() {
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
BotConfigSerializer.deserialize(
|
||||
JsonNull.INSTANCE,
|
||||
BotConfig::class.java, TestJsonSerializationContext(gson)
|
||||
)
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
BotConfigSerializer.deserialize(
|
||||
JsonArray(),
|
||||
BotConfig::class.java, TestJsonSerializationContext(gson)
|
||||
)
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
BotConfigSerializer.deserialize(
|
||||
JsonPrimitive("A STRING"),
|
||||
BotConfig::class.java, TestJsonSerializationContext(gson)
|
||||
)
|
||||
}
|
||||
|
||||
// 检查 BotConfig 的反序列化中是否能正确判断 account 的类型
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
BotConfigSerializer.deserialize(
|
||||
JsonObject().apply {
|
||||
addProperty("account", "A STRING")
|
||||
},
|
||||
BotConfig::class.java, TestJsonSerializationContext(gson)
|
||||
)
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
BotConfigSerializer.deserialize(
|
||||
JsonObject().apply {
|
||||
add("account", JsonNull.INSTANCE)
|
||||
},
|
||||
BotConfig::class.java, TestJsonSerializationContext(gson)
|
||||
)
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
BotConfigSerializer.deserialize(
|
||||
JsonObject().apply {
|
||||
add("account", JsonArray())
|
||||
},
|
||||
BotConfig::class.java, TestJsonSerializationContext(gson)
|
||||
)
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
BotConfigSerializer.deserialize(
|
||||
JsonObject(),
|
||||
BotConfig::class.java, TestJsonSerializationContext(gson)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
val expectBotAccount = BotAccount(
|
||||
name = "test-bot",
|
||||
token = "test-token",
|
||||
creatorId = 10000
|
||||
)
|
||||
val expectDefaultBotConfig = BotConfig(account = expectBotAccount)
|
||||
val minimumJsonObject = JsonObject().apply {
|
||||
add("account", gson.toJsonTree(expectBotAccount))
|
||||
}
|
||||
val actualMinimumBotConfig = BotConfigSerializer.deserialize(
|
||||
minimumJsonObject, BotConfig::class.java, TestJsonSerializationContext(gson)
|
||||
)
|
||||
assertNotNull(actualMinimumBotConfig)
|
||||
assertEquals(expectDefaultBotConfig, actualMinimumBotConfig)
|
||||
|
||||
val expectDefaultProxy = ProxyConfig(
|
||||
type = ProxyType.HTTP,
|
||||
host = "https://example.com",
|
||||
port = 443
|
||||
)
|
||||
|
||||
// -------------------------------------------------
|
||||
|
||||
val jsonObject = JsonObject().apply {
|
||||
add(
|
||||
"account", gson.toJsonTree(
|
||||
BotAccount(
|
||||
name = "test-bot",
|
||||
token = "test-token",
|
||||
creatorId = 10000
|
||||
)
|
||||
)
|
||||
)
|
||||
addProperty("enabled", true)
|
||||
add("proxy", gson.toJsonTree(expectDefaultProxy))
|
||||
addProperty("disableBuiltInAbility", true)
|
||||
addProperty("autoUpdateCommandList", true)
|
||||
addProperty("baseApiUrl", "https://test.com")
|
||||
add("extensions", JsonArray().apply {
|
||||
add("org.example:test:1.0.0-SNAPSHOT")
|
||||
})
|
||||
}
|
||||
|
||||
val botConfig = BotConfigSerializer.deserialize(
|
||||
jsonObject,
|
||||
BotConfig::class.java, TestJsonSerializationContext(gson)
|
||||
)
|
||||
|
||||
assertEquals("test-bot", botConfig.account.name)
|
||||
assertEquals("test-token", botConfig.account.token)
|
||||
assertEquals(10000, botConfig.account.creatorId)
|
||||
assertEquals(true, botConfig.enabled)
|
||||
assertEquals(expectDefaultProxy, botConfig.proxy)
|
||||
assertEquals(true, botConfig.disableBuiltInAbility)
|
||||
assertEquals(true, botConfig.autoUpdateCommandList)
|
||||
assertEquals("https://test.com", botConfig.baseApiUrl)
|
||||
assertEquals(false, botConfig.extensions.isEmpty())
|
||||
assertEquals(1, botConfig.extensions.size)
|
||||
}
|
||||
}
|
||||
|
||||
internal class ProxyConfigSerializerTest {
|
||||
|
||||
// 测试 ProxyConfig 的 Json 序列化
|
||||
@Test
|
||||
fun `serialize test`() {
|
||||
assertEquals(JsonNull.INSTANCE, ProxyConfigSerializer.serialize(null, null, null))
|
||||
|
||||
val expectDefaultConfig = ProxyConfig()
|
||||
val actualDefaultJson = ProxyConfigSerializer.serialize(expectDefaultConfig, null, null)
|
||||
assertTrue(actualDefaultJson is JsonObject)
|
||||
assertEquals(expectDefaultConfig.type.name, actualDefaultJson["type"].asString)
|
||||
assertEquals(expectDefaultConfig.host, actualDefaultJson["host"].asString)
|
||||
assertEquals(expectDefaultConfig.port, actualDefaultJson["port"].asInt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Bad type deserialize test`() {
|
||||
val defaultConfig = ProxyConfig()
|
||||
assertEquals(defaultConfig, ProxyConfigSerializer.deserialize(null, null, null))
|
||||
assertEquals(defaultConfig, ProxyConfigSerializer.deserialize(JsonNull.INSTANCE, null, null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize test - object`() {
|
||||
val defaultConfig = ProxyConfig()
|
||||
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
ProxyConfigSerializer.deserialize(JsonArray(), null, null)
|
||||
}
|
||||
|
||||
val jsonWithoutType = JsonObject().apply {
|
||||
addProperty("host", "example.com")
|
||||
addProperty("port", 8080)
|
||||
}
|
||||
assertEquals(defaultConfig, ProxyConfigSerializer.deserialize(jsonWithoutType, null, null))
|
||||
|
||||
val looksGoodJson = JsonObject().apply {
|
||||
addProperty("type", "HTTP")
|
||||
addProperty("host", "example.com")
|
||||
addProperty("port", 8080)
|
||||
}
|
||||
assertEquals(
|
||||
ProxyConfig(
|
||||
type = ProxyType.HTTP,
|
||||
host = "example.com",
|
||||
port = 8080
|
||||
), ProxyConfigSerializer.deserialize(looksGoodJson, null, null)
|
||||
)
|
||||
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
ProxyConfigSerializer.deserialize(JsonObject().apply {
|
||||
addProperty("type", "UNKNOWN")
|
||||
addProperty("host", "example.com")
|
||||
addProperty("port", 8080)
|
||||
}, null, null)
|
||||
}
|
||||
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
ProxyConfigSerializer.deserialize(JsonObject().apply {
|
||||
addProperty("type", "HTTP")
|
||||
addProperty("host", "example.com")
|
||||
}, null, null)
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
ProxyConfigSerializer.deserialize(JsonObject().apply {
|
||||
addProperty("type", "HTTP")
|
||||
addProperty("port", 8080)
|
||||
}, null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class ArtifactSerializerTest {
|
||||
|
||||
@Test
|
||||
fun badJsonType() {
|
||||
assertFailsWith<JsonParseException> { ArtifactSerializer.deserialize(JsonObject(), null, null) }
|
||||
assertFailsWith<JsonParseException> { ArtifactSerializer.deserialize(JsonArray(), null, null) }
|
||||
assertFailsWith<JsonParseException> { ArtifactSerializer.deserialize(JsonPrimitive("A STRING"), null, null) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Basic format serialization`() {
|
||||
val gav = "org.example.software:test:1.0.0-SNAPSHOT"
|
||||
val expectArtifact = DefaultArtifact(gav)
|
||||
val actualArtifact = DefaultArtifact(ArtifactSerializer.serialize(expectArtifact, null, null).asString)
|
||||
assertEquals(expectArtifact, actualArtifact)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Full format serialization`() {
|
||||
val gav = "org.example.software:test:war:javadoc:1.0.0-SNAPSHOT"
|
||||
val expectArtifact = DefaultArtifact(gav)
|
||||
val actualArtifact = DefaultArtifact(ArtifactSerializer.serialize(expectArtifact, null, null).asString)
|
||||
assertEquals(expectArtifact, actualArtifact)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Bad format serialization`() {
|
||||
assertFailsWith<JsonParseException> {
|
||||
ArtifactSerializer.deserialize(JsonPrimitive("org.example~test"), null, null)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Other artifact implementation serialization`() {
|
||||
val gav = "org.example.software:test:war:javadoc:1.0.0-SNAPSHOT"
|
||||
val expectArtifact = DefaultArtifact(gav)
|
||||
val otherArtifactImpl = mockk<Artifact> {
|
||||
every { groupId } returns expectArtifact.groupId
|
||||
every { artifactId } returns expectArtifact.artifactId
|
||||
every { version } returns expectArtifact.version
|
||||
every { classifier } returns expectArtifact.classifier
|
||||
every { extension } returns expectArtifact.extension
|
||||
}
|
||||
|
||||
val json = ArtifactSerializer.serialize(otherArtifactImpl, null, null)
|
||||
assertTrue(json is JsonPrimitive)
|
||||
assertEquals(expectArtifact.toString(), json.asString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deserialize() {
|
||||
val gav = "org.example.software:test:war:javadoc:1.0.0-SNAPSHOT"
|
||||
val expectArtifact = DefaultArtifact(gav)
|
||||
val actualArtifact = ArtifactSerializer.deserialize(JsonPrimitive(gav), null, null)
|
||||
assertEquals(expectArtifact, actualArtifact)
|
||||
}
|
||||
}
|
||||
|
||||
internal class UsernameAuthenticatorSerializerTest {
|
||||
|
||||
@Test
|
||||
fun serializeTest() {
|
||||
val authenticator = UsernameAuthenticator("testUser", "testPassword")
|
||||
val jsonElement = UsernameAuthenticatorSerializer.serialize(authenticator, null, null)
|
||||
assertTrue(jsonElement.isJsonObject)
|
||||
val jsonObject = jsonElement.asJsonObject
|
||||
assertEquals("testUser", jsonObject["username"]?.asString)
|
||||
assertEquals("testPassword", jsonObject["password"]?.asString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deserializeTest() {
|
||||
org.junit.jupiter.api.assertThrows<JsonParseException> {
|
||||
UsernameAuthenticatorSerializer.deserialize(JsonArray(), null, null)
|
||||
}
|
||||
org.junit.jupiter.api.assertThrows<JsonParseException> {
|
||||
UsernameAuthenticatorSerializer.deserialize(JsonPrimitive(""), null, null)
|
||||
}
|
||||
assertNull(UsernameAuthenticatorSerializer.deserialize(JsonNull.INSTANCE, null, null))
|
||||
org.junit.jupiter.api.assertThrows<JsonParseException> {
|
||||
UsernameAuthenticatorSerializer.deserialize(JsonObject().apply {
|
||||
addProperty("username", "testUser")
|
||||
}, null, null)
|
||||
}
|
||||
org.junit.jupiter.api.assertThrows<JsonParseException> {
|
||||
UsernameAuthenticatorSerializer.deserialize(JsonObject().apply {
|
||||
addProperty("username", "testUser")
|
||||
add("password", JsonArray())
|
||||
}, null, null)
|
||||
}
|
||||
org.junit.jupiter.api.assertThrows<JsonParseException> {
|
||||
UsernameAuthenticatorSerializer.deserialize(JsonObject().apply {
|
||||
addProperty("password", "testPassword")
|
||||
}, null, null)
|
||||
}
|
||||
org.junit.jupiter.api.assertThrows<JsonParseException> {
|
||||
UsernameAuthenticatorSerializer.deserialize(JsonObject().apply {
|
||||
add("username", JsonArray())
|
||||
addProperty("password", "testPassword")
|
||||
}, null, null)
|
||||
}
|
||||
org.junit.jupiter.api.assertThrows<JsonParseException> {
|
||||
UsernameAuthenticatorSerializer.deserialize(JsonObject().apply {
|
||||
addProperty("username", "")
|
||||
addProperty("password", "")
|
||||
}, null, null)
|
||||
}
|
||||
org.junit.jupiter.api.assertThrows<JsonParseException> {
|
||||
UsernameAuthenticatorSerializer.deserialize(JsonObject().apply {
|
||||
addProperty("username", "testUser")
|
||||
addProperty("password", "")
|
||||
}, null, null)
|
||||
}
|
||||
org.junit.jupiter.api.assertThrows<JsonParseException> {
|
||||
UsernameAuthenticatorSerializer.deserialize(JsonObject().apply {
|
||||
addProperty("username", "")
|
||||
addProperty("password", "testPassword")
|
||||
}, null, null)
|
||||
}
|
||||
|
||||
val authenticator = UsernameAuthenticatorSerializer.deserialize(JsonObject().apply {
|
||||
addProperty("username", "testUser")
|
||||
addProperty("password", "testPassword")
|
||||
}, null, null)
|
||||
assertNotNull(authenticator)
|
||||
|
||||
assertTrue(authenticator.checkCredentials("testUser", "testPassword"))
|
||||
assertFalse(authenticator.checkCredentials("falseUser", "testPassword"))
|
||||
assertFalse(authenticator.checkCredentials("testUser", "falsePassword"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class BotAccountSerializerTest {
|
||||
|
||||
private val expectToken = "123456789:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10"
|
||||
private val gson = GsonBuilder()
|
||||
.registerTypeAdapter(BotAccount::class.java, BotAccountSerializer)
|
||||
.create()
|
||||
|
||||
@Test
|
||||
fun `Invalid json type check test`() {
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
BotAccountSerializer.deserialize(null, null, null)
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
BotAccountSerializer.deserialize(JsonNull.INSTANCE, null, null)
|
||||
}
|
||||
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
BotAccountSerializer.deserialize(JsonPrimitive("A STRING"), null, null)
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
BotAccountSerializer.deserialize(JsonArray(), null, null)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Field missing test`() {
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
BotAccountSerializer.deserialize(JsonObject(), null, null)
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
BotAccountSerializer.deserialize(JsonObject().apply {
|
||||
addProperty("token", expectToken)
|
||||
addProperty("creatorId", 1)
|
||||
}, null, null)
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
BotAccountSerializer.deserialize(JsonObject().apply {
|
||||
addProperty("name", "testUser")
|
||||
addProperty("creatorId", 1)
|
||||
}, null, null)
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
BotAccountSerializer.deserialize(JsonObject().apply {
|
||||
addProperty("name", "testUser")
|
||||
addProperty("token", expectToken)
|
||||
}, null, null)
|
||||
}
|
||||
|
||||
val account = BotAccountSerializer.deserialize(JsonObject().apply {
|
||||
addProperty("name", "testUser")
|
||||
addProperty("token", expectToken)
|
||||
addProperty("creatorId", 1)
|
||||
}, null, null)
|
||||
assertNotNull(account)
|
||||
assertEquals("testUser", account.name)
|
||||
assertEquals(expectToken, account.token)
|
||||
assertEquals(1, account.creatorId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `'token' check test`() {
|
||||
val jsonObject = JsonObject().apply {
|
||||
addProperty("name", "testUser")
|
||||
addProperty("token", expectToken)
|
||||
addProperty("creatorId", 123456789123456789)
|
||||
}
|
||||
|
||||
val looksGoodAccount = BotAccountSerializer.deserialize(jsonObject, null, null)
|
||||
|
||||
assertNotNull(looksGoodAccount)
|
||||
assertEquals("testUser", looksGoodAccount.name)
|
||||
assertEquals(expectToken, looksGoodAccount.token)
|
||||
assertEquals(123456789123456789, looksGoodAccount.creatorId)
|
||||
|
||||
try {
|
||||
BotAccountSerializer.deserialize(jsonObject.deepCopy().apply {
|
||||
addProperty("token", "")
|
||||
}, null, null)
|
||||
fail("Token 为空,但是没有抛出异常。")
|
||||
} catch (e: JsonParseException) {
|
||||
assertEquals("`token` cannot be empty.", e.message)
|
||||
}
|
||||
|
||||
try {
|
||||
BotAccountSerializer.deserialize(jsonObject.deepCopy().apply {
|
||||
addProperty("token", "abcdefghijklmnopqrstuvwxyz")
|
||||
}, null, null)
|
||||
fail("Token 格式错误(基本格式错误),但是没有抛出异常。")
|
||||
} catch (e: JsonParseException) {
|
||||
assertEquals("`token` is invalid.", e.message)
|
||||
}
|
||||
|
||||
try {
|
||||
BotAccountSerializer.deserialize(jsonObject.deepCopy().apply {
|
||||
addProperty("token", "abcdefgh:ijklmnopqrstuvwxyz-1234567890_abcde")
|
||||
}, null, null)
|
||||
fail("Token 格式错误(ID 不为数字),但是没有抛出异常。")
|
||||
} catch (e: JsonParseException) {
|
||||
assertEquals("`token` is invalid.", e.message)
|
||||
}
|
||||
|
||||
try {
|
||||
BotAccountSerializer.deserialize(jsonObject.deepCopy().apply {
|
||||
addProperty("token", "0123456789:ijklmnopqrstu-vwxyz_123456")
|
||||
}, null, null)
|
||||
fail("Token 格式错误(授权令牌长度错误),但是没有抛出异常。")
|
||||
} catch (e: JsonParseException) {
|
||||
assertEquals("`token` is invalid.", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `'creatorId' check test`() {
|
||||
val jsonObject = JsonObject().apply {
|
||||
addProperty("name", "testUser")
|
||||
addProperty("token", expectToken)
|
||||
addProperty("creatorId", 1)
|
||||
}
|
||||
|
||||
val looksGoodAccount = gson.fromJson(jsonObject, BotAccount::class.java)
|
||||
assertEquals(1, looksGoodAccount.creatorId)
|
||||
|
||||
try {
|
||||
BotAccountSerializer.deserialize(jsonObject.deepCopy().apply {
|
||||
addProperty("creatorId", "A STRING")
|
||||
}, null, null)
|
||||
fail("creatorId 不是一个数字,但是没有抛出异常。")
|
||||
} catch (e: JsonParseException) {
|
||||
assertEquals("`creatorId` must be a number.", e.message)
|
||||
}
|
||||
|
||||
try {
|
||||
BotAccountSerializer.deserialize(jsonObject.deepCopy().apply {
|
||||
addProperty("creatorId", -1)
|
||||
}, null, null)
|
||||
fail("creatorId 不能为负数,但是没有抛出异常。")
|
||||
} catch (e: JsonParseException) {
|
||||
assertEquals("`creatorId` must be a positive number.", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `json deserialize test`() {
|
||||
val jsonObject = JsonObject().apply {
|
||||
addProperty("name", "testUser")
|
||||
addProperty("token", expectToken)
|
||||
addProperty("creatorId", 1)
|
||||
}
|
||||
|
||||
val looksGoodAccount = gson.fromJson(jsonObject, BotAccount::class.java)
|
||||
|
||||
assertNotNull(looksGoodAccount)
|
||||
assertEquals("testUser", looksGoodAccount.name)
|
||||
assertEquals(expectToken, looksGoodAccount.token)
|
||||
assertEquals(1, looksGoodAccount.creatorId)
|
||||
}
|
||||
}
|
@ -4,3 +4,4 @@ rootProject.name = "scalabot"
|
||||
include(":scalabot-app")
|
||||
include(":scalabot-extension")
|
||||
include("scalabot-ext-example")
|
||||
include("scalabot-meta")
|
||||
|
Reference in New Issue
Block a user