mirror of
https://github.com/LamGC/ScalaBot.git
synced 2025-07-01 04:47:24 +00:00
Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
ac0a398afc
|
|||
145e5a2141
|
|||
b5c85e213b
|
|||
746221a085
|
|||
24f34aa27f
|
|||
31366575a9
|
|||
37c3275bb6
|
|||
72e26bd677
|
|||
9aab3c2a24
|
|||
cac055bb08
|
|||
efbb57f1f7
|
|||
5e18149640
|
|||
0a5313e94a
|
|||
a0afde52ac
|
|||
ef37f3b2d7
|
|||
6e59a9a5ac
|
|||
a44732a7f6
|
|||
95ad251826
|
|||
8174f2a3a2
|
|||
478480014a
|
|||
830f05c90a
|
|||
8be0978783 | |||
ce613787f6
|
|||
2389d082f4
|
|||
27f54c3c36
|
|||
7b985ce325
|
|||
77b7a7cd08
|
|||
e8b746b3f8
|
|||
d24572a4f3
|
|||
f11290c73d
|
|||
1f2ab0f9b1
|
|||
d14ef9de36
|
|||
3e51327ed7
|
|||
93cf5c4e2f
|
@ -7,5 +7,5 @@ allprojects {
|
||||
|
||||
}
|
||||
group = "net.lamgc"
|
||||
version = "0.2.0"
|
||||
version = "0.3.1"
|
||||
}
|
@ -35,6 +35,8 @@ dependencies {
|
||||
implementation("io.prometheus:simpleclient_httpserver:0.15.0")
|
||||
|
||||
testImplementation(kotlin("test"))
|
||||
testImplementation("io.mockk:mockk:1.12.3")
|
||||
testImplementation("com.github.stefanbirkner:system-lambda:1.2.1")
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
|
@ -1,17 +0,0 @@
|
||||
package net.lamgc.scalabot.util;
|
||||
|
||||
final class ByteUtils {
|
||||
|
||||
private ByteUtils() {
|
||||
}
|
||||
|
||||
public static String bytesToHexString(byte[] bytes) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (byte aByte : bytes) {
|
||||
String hexBit = Integer.toHexString(aByte & 0xFF);
|
||||
builder.append(hexBit.length() == 1 ? "0" + hexBit : hexBit);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
}
|
@ -13,12 +13,15 @@ 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
|
||||
|
||||
private val log = KotlinLogging.logger { }
|
||||
|
||||
@ -99,20 +102,40 @@ internal data class MetricsConfig(
|
||||
* @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 proxy: Proxy? = null,
|
||||
val layout: String = "default",
|
||||
val enableReleases: Boolean = true,
|
||||
val enableSnapshots: Boolean = true,
|
||||
// 可能要设计个 type 来判断解析成什么类型的 Authentication.
|
||||
val authentication: Authentication? = null
|
||||
) {
|
||||
|
||||
fun toRemoteRepository(): RemoteRepository {
|
||||
val builder = RemoteRepository.Builder(null, checkRepositoryLayout(layout), url.toString())
|
||||
fun toRemoteRepository(proxyConfig: ProxyConfig): RemoteRepository {
|
||||
val builder =
|
||||
RemoteRepository.Builder(id ?: createDefaultRepositoryId(), checkRepositoryLayout(layout), url.toString())
|
||||
if (proxy != null) {
|
||||
builder.setProxy(proxy)
|
||||
} else if (Const.config.proxy.type == DefaultBotOptions.ProxyType.HTTP) {
|
||||
builder.setProxy(Const.config.proxy.toAetherProxy())
|
||||
} else if (proxyConfig.type == DefaultBotOptions.ProxyType.HTTP) {
|
||||
builder.setProxy(proxyConfig.toAetherProxy())
|
||||
}
|
||||
|
||||
builder.setReleasePolicy(
|
||||
RepositoryPolicy(
|
||||
enableReleases,
|
||||
RepositoryPolicy.UPDATE_POLICY_NEVER,
|
||||
RepositoryPolicy.CHECKSUM_POLICY_FAIL
|
||||
)
|
||||
)
|
||||
builder.setSnapshotPolicy(
|
||||
RepositoryPolicy(
|
||||
enableSnapshots,
|
||||
RepositoryPolicy.UPDATE_POLICY_ALWAYS,
|
||||
RepositoryPolicy.CHECKSUM_POLICY_WARN
|
||||
)
|
||||
)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
@ -124,6 +147,13 @@ internal data class MavenRepositoryConfig(
|
||||
}
|
||||
return type
|
||||
}
|
||||
|
||||
private val repoNumber = AtomicInteger(1)
|
||||
|
||||
fun createDefaultRepositoryId(): String {
|
||||
return "Repository-${repoNumber.getAndIncrement()}"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,7 +209,8 @@ internal enum class AppPaths(
|
||||
AppConfig(
|
||||
mavenRepositories = listOf(
|
||||
MavenRepositoryConfig(
|
||||
URL(MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL)
|
||||
id = "central",
|
||||
url = URL(MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL)
|
||||
)
|
||||
)
|
||||
), it
|
||||
@ -268,9 +299,17 @@ private fun AppPaths.defaultInitializer() {
|
||||
}
|
||||
|
||||
internal fun initialFiles() {
|
||||
val configFilesNotInitialized = !AppPaths.DEFAULT_CONFIG_APPLICATION.file.exists()
|
||||
&& !AppPaths.DEFAULT_CONFIG_BOT.file.exists()
|
||||
|
||||
for (path in AppPaths.values()) {
|
||||
path.initial()
|
||||
}
|
||||
|
||||
if (configFilesNotInitialized) {
|
||||
log.warn { "配置文件已初始化, 请根据需要修改配置文件后重新启动本程序." }
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
private object GsonConst {
|
||||
|
@ -9,21 +9,27 @@ import org.telegram.telegrambots.bots.DefaultBotOptions
|
||||
import org.telegram.telegrambots.meta.TelegramBotsApi
|
||||
import org.telegram.telegrambots.meta.generics.BotSession
|
||||
import org.telegram.telegrambots.updatesreceivers.DefaultBotSession
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.nio.file.attribute.PosixFilePermission
|
||||
import java.nio.file.attribute.PosixFilePermissions
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.isReadable
|
||||
import kotlin.io.path.isWritable
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
private val log = KotlinLogging.logger { }
|
||||
|
||||
private val launcher = Launcher()
|
||||
.registerShutdownHook()
|
||||
|
||||
fun main(args: Array<String>): Unit = runBlocking {
|
||||
log.info { "ScalaBot 正在启动中..." }
|
||||
log.info { "数据目录: ${AppPaths.DATA_ROOT}" }
|
||||
log.debug { "启动参数: ${args.joinToString(prefix = "[", postfix = "]")}" }
|
||||
initialFiles()
|
||||
if (Const.config.metrics.enable) {
|
||||
startMetricsServer()
|
||||
}
|
||||
|
||||
val launcher = Launcher()
|
||||
.registerShutdownHook()
|
||||
startMetricsServer()
|
||||
if (!launcher.launch()) {
|
||||
exitProcess(1)
|
||||
}
|
||||
@ -33,11 +39,16 @@ fun main(args: Array<String>): Unit = runBlocking {
|
||||
* 启动运行指标服务器.
|
||||
* 使用 Prometheus 指标格式.
|
||||
*/
|
||||
fun startMetricsServer() {
|
||||
internal fun startMetricsServer(config: MetricsConfig = Const.config.metrics) {
|
||||
if (!config.enable) {
|
||||
log.debug { "运行指标服务器已禁用." }
|
||||
return
|
||||
}
|
||||
|
||||
val builder = HTTPServer.Builder()
|
||||
.withDaemonThreads(true)
|
||||
.withPort(Const.config.metrics.port)
|
||||
.withHostname(Const.config.metrics.bindAddress)
|
||||
.withPort(config.port)
|
||||
.withHostname(config.bindAddress)
|
||||
|
||||
val httpServer = builder
|
||||
.build()
|
||||
@ -45,7 +56,7 @@ fun startMetricsServer() {
|
||||
log.info { "运行指标服务器已启动. (Port: ${httpServer.port})" }
|
||||
}
|
||||
|
||||
internal class Launcher : AutoCloseable {
|
||||
internal class Launcher(private val config: AppConfig = Const.config) : AutoCloseable {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@ -54,16 +65,46 @@ internal class Launcher : AutoCloseable {
|
||||
|
||||
private val botApi = TelegramBotsApi(DefaultBotSession::class.java)
|
||||
private val botSessionMap = mutableMapOf<ScalaBot, BotSession>()
|
||||
private val mavenLocalRepository =
|
||||
if (Const.config.mavenLocalRepository != null && Const.config.mavenLocalRepository.isNotEmpty()) {
|
||||
val repoPath = AppPaths.DATA_ROOT.file.toPath()
|
||||
.resolve(Const.config.mavenLocalRepository)
|
||||
.toRealPath()
|
||||
.toFile()
|
||||
LocalRepository(repoPath)
|
||||
} else {
|
||||
LocalRepository("${System.getProperty("user.home")}/.m2/repository")
|
||||
private val mavenLocalRepository = getMavenLocalRepository()
|
||||
|
||||
private fun getMavenLocalRepository(): LocalRepository {
|
||||
val localPath =
|
||||
if (config.mavenLocalRepository != null && config.mavenLocalRepository.isNotEmpty()) {
|
||||
val repoPath = AppPaths.DATA_ROOT.file.toPath()
|
||||
.resolve(config.mavenLocalRepository)
|
||||
.apply {
|
||||
if (!exists()) {
|
||||
if (!parent.isWritable() || !parent.isReadable()) {
|
||||
throw IOException("Unable to read and write the directory where Maven repository is located.")
|
||||
}
|
||||
if (System.getProperty("os.name").lowercase().startsWith("windows")) {
|
||||
createDirectories()
|
||||
} else {
|
||||
createDirectories(
|
||||
PosixFilePermissions.asFileAttribute(
|
||||
setOf(
|
||||
PosixFilePermission.OWNER_READ,
|
||||
PosixFilePermission.OWNER_WRITE,
|
||||
PosixFilePermission.GROUP_READ,
|
||||
PosixFilePermission.GROUP_WRITE,
|
||||
PosixFilePermission.OTHERS_READ,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.toRealPath()
|
||||
.toFile()
|
||||
repoPath
|
||||
} else {
|
||||
File("${System.getProperty("user.home")}/.m2/repository")
|
||||
}
|
||||
if (!localPath.exists()) {
|
||||
localPath.mkdirs()
|
||||
}
|
||||
return LocalRepository(localPath)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun launch(): Boolean {
|
||||
@ -71,6 +112,9 @@ internal class Launcher : AutoCloseable {
|
||||
if (botConfigs.isEmpty()) {
|
||||
log.warn { "尚未配置任何机器人, 请先配置机器人后再启动本程序." }
|
||||
return false
|
||||
} else if (botConfigs.none { it.enabled }) {
|
||||
log.warn { "配置文件中没有已启用的机器人, 请至少启用一个机器人." }
|
||||
return false
|
||||
}
|
||||
for (botConfig in botConfigs) {
|
||||
try {
|
||||
@ -92,15 +136,15 @@ internal class Launcher : AutoCloseable {
|
||||
val proxyConfig =
|
||||
if (botConfig.proxy != null && botConfig.proxy.type != DefaultBotOptions.ProxyType.NO_PROXY) {
|
||||
botConfig.proxy
|
||||
} else if (Const.config.proxy.type != DefaultBotOptions.ProxyType.NO_PROXY) {
|
||||
Const.config.proxy
|
||||
} else if (config.proxy.type != DefaultBotOptions.ProxyType.NO_PROXY) {
|
||||
config.proxy
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (proxyConfig != null) {
|
||||
proxyType = proxyConfig.type
|
||||
proxyHost = Const.config.proxy.host
|
||||
proxyPort = Const.config.proxy.port
|
||||
proxyHost = config.proxy.host
|
||||
proxyPort = config.proxy.port
|
||||
log.debug { "机器人 `${botConfig.account.name}` 已启用代理配置: $proxyConfig" }
|
||||
}
|
||||
|
||||
@ -110,16 +154,21 @@ internal class Launcher : AutoCloseable {
|
||||
}
|
||||
val account = botConfig.account
|
||||
|
||||
val remoteRepositories = Const.config.mavenRepositories
|
||||
.map(MavenRepositoryConfig::toRemoteRepository)
|
||||
val remoteRepositories = config.mavenRepositories
|
||||
.map { it.toRemoteRepository(config.proxy) }
|
||||
.toMutableList().apply {
|
||||
add(MavenRepositoryExtensionFinder.getMavenCentralRepository(proxy = Const.config.proxy.toAetherProxy()))
|
||||
if (this.none {
|
||||
it.url == MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL
|
||||
|| it.url == MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL.trimEnd('/')
|
||||
}) {
|
||||
add(MavenRepositoryExtensionFinder.getMavenCentralRepository(proxy = config.proxy.toAetherProxy()))
|
||||
}
|
||||
}.toList()
|
||||
val extensionPackageFinders = setOf(
|
||||
MavenRepositoryExtensionFinder(
|
||||
localRepository = mavenLocalRepository,
|
||||
remoteRepositories = remoteRepositories,
|
||||
proxy = Const.config.proxy.toAetherProxy()
|
||||
proxy = config.proxy.toAetherProxy()
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -2,7 +2,7 @@ package net.lamgc.scalabot
|
||||
|
||||
import com.google.common.io.Files
|
||||
import mu.KotlinLogging
|
||||
import net.lamgc.scalabot.util.toHaxString
|
||||
import net.lamgc.scalabot.util.toHexString
|
||||
import org.mapdb.DB
|
||||
import org.mapdb.DBException
|
||||
import org.mapdb.DBMaker
|
||||
@ -186,5 +186,5 @@ private object BotAccountIdDbAdapter : FileDbAdapter("BotAccountId", { botAccoun
|
||||
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))
|
||||
File(AppPaths.DATA_DB.file, "${digestBytes.toHaxString()}.db")
|
||||
File(AppPaths.DATA_DB.file, "${digestBytes.toHexString()}.db")
|
||||
})
|
@ -132,7 +132,7 @@ internal class ExtensionLoader(
|
||||
result[finder] = artifacts
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.error { "搜索器 ${finder::class.java.name} 在搜索扩展 `$extensionArtifact` 时发生错误:" }
|
||||
log.error(e) { "搜索器 ${finder::class.java.name} 在搜索扩展 `$extensionArtifact` 时发生错误." }
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
@ -257,9 +257,21 @@ internal class MavenRepositoryExtensionFinder(
|
||||
}
|
||||
|
||||
override fun findByArtifact(extensionArtifact: Artifact, extensionsPath: File): Set<FoundExtensionPackage> {
|
||||
log.debug {
|
||||
StringBuilder().apply {
|
||||
append("构件 $extensionArtifact 将在以下仓库拉取: \n")
|
||||
remoteRepositories.forEach {
|
||||
append("\t- ${it}\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
val extensionArtifactResult = repositorySystem.resolveArtifact(
|
||||
repoSystemSession,
|
||||
ArtifactRequest(extensionArtifact, remoteRepositories, null)
|
||||
ArtifactRequest(
|
||||
extensionArtifact,
|
||||
repositorySystem.newResolutionRepositories(repoSystemSession, remoteRepositories),
|
||||
null
|
||||
)
|
||||
)
|
||||
val extResolvedArtifact = extensionArtifactResult.artifact
|
||||
if (!extensionArtifactResult.isResolved) {
|
||||
|
@ -103,6 +103,12 @@ internal class ScalaBot(
|
||||
}
|
||||
BotCommand(it.name(), abilityInfo)
|
||||
}
|
||||
|
||||
if (botCommands.isEmpty()) {
|
||||
log.info { "Bot 没有任何命令, 命令列表更新已跳过." }
|
||||
return true
|
||||
}
|
||||
|
||||
val setMyCommands = SetMyCommands()
|
||||
setMyCommands.commands = botCommands
|
||||
return execute(DeleteMyCommands()) && execute(setMyCommands)
|
||||
@ -114,7 +120,6 @@ internal class ScalaBot(
|
||||
}
|
||||
|
||||
override fun onClosing() {
|
||||
super.onClosing()
|
||||
onlineBotGauge.dec()
|
||||
}
|
||||
|
||||
@ -151,7 +156,7 @@ internal class ScalaBot(
|
||||
private val updateProcessTime = Summary.build()
|
||||
.name("update_process_duration_seconds")
|
||||
.help(
|
||||
"Time to process update. (This indicator includes the pre-processing of update by TelegrammBots, " +
|
||||
"Time to process update. (This indicator includes the pre-processing of update by TelegramBots, " +
|
||||
"so it may be different from the actual execution time of ability. " +
|
||||
"It is not recommended to use it as the accurate execution time of ability)"
|
||||
)
|
||||
|
@ -1,7 +1,6 @@
|
||||
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
|
||||
@ -20,6 +19,9 @@ internal object ProxyTypeSerializer : JsonDeserializer<DefaultBotOptions.ProxyTy
|
||||
typeOfT: Type?,
|
||||
context: JsonDeserializationContext?
|
||||
): DefaultBotOptions.ProxyType {
|
||||
if (json.isJsonNull) {
|
||||
return DefaultBotOptions.ProxyType.NO_PROXY
|
||||
}
|
||||
if (!json.isJsonPrimitive) {
|
||||
throw JsonParseException("Wrong configuration value type.")
|
||||
}
|
||||
@ -52,8 +54,8 @@ internal object ArtifactSerializer : JsonSerializer<Artifact>, JsonDeserializer<
|
||||
return JsonPrimitive(gavBuilder.append(':').append(src.version).toString())
|
||||
}
|
||||
|
||||
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Artifact {
|
||||
if (!json!!.isJsonPrimitive) {
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): Artifact {
|
||||
if (!json.isJsonPrimitive) {
|
||||
throw JsonParseException("Wrong configuration value type.")
|
||||
}
|
||||
return DefaultArtifact(json.asString.trim())
|
||||
@ -63,72 +65,29 @@ internal object ArtifactSerializer : JsonSerializer<Artifact>, JsonDeserializer<
|
||||
|
||||
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}")
|
||||
}
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Authentication {
|
||||
if (json !is JsonObject) {
|
||||
throw JsonParseException("Unsupported JSON type.")
|
||||
}
|
||||
val username = SerializerUtils.checkJsonKey(json, "username")
|
||||
val password = SerializerUtils.checkJsonKey(json, "password")
|
||||
val builder = AuthenticationBuilder()
|
||||
builder.addUsername(username)
|
||||
builder.addPassword(password)
|
||||
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}")
|
||||
private object SerializerUtils {
|
||||
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
|
||||
}
|
||||
return json.get(key).asString
|
||||
}
|
||||
|
||||
internal object MavenRepositoryConfigSerializer
|
||||
@ -142,12 +101,15 @@ internal object MavenRepositoryConfigSerializer
|
||||
return when (json) {
|
||||
is JsonObject -> {
|
||||
MavenRepositoryConfig(
|
||||
url = URL(checkJsonKey(json, "url")),
|
||||
id = json.get("id")?.asString,
|
||||
url = URL(SerializerUtils.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",
|
||||
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
|
||||
@ -155,7 +117,7 @@ internal object MavenRepositoryConfigSerializer
|
||||
)
|
||||
}
|
||||
is JsonPrimitive -> {
|
||||
MavenRepositoryConfig(URL(json.asString))
|
||||
MavenRepositoryConfig(url = URL(json.asString))
|
||||
}
|
||||
else -> {
|
||||
throw JsonParseException("Unsupported Maven warehouse configuration type.")
|
||||
|
@ -7,9 +7,8 @@ import org.eclipse.aether.artifact.Artifact
|
||||
import java.io.File
|
||||
import java.io.FileFilter
|
||||
import java.io.FilenameFilter
|
||||
import java.net.URL
|
||||
|
||||
internal fun ByteArray.toHaxString(): String = ByteUtils.bytesToHexString(this)
|
||||
internal fun ByteArray.toHexString(): String = joinToString("") { it.toString(16) }
|
||||
|
||||
internal fun Artifact.equalsArtifact(that: Artifact): Boolean =
|
||||
this.groupId.equals(that.groupId) &&
|
||||
@ -56,9 +55,14 @@ internal fun File.deepListFiles(
|
||||
* @return 获取 Finder 的优先级.
|
||||
* @throws NoSuchFieldException 如果 Finder 没有添加 [FinderRules] 注解时抛出该异常.
|
||||
*/
|
||||
internal fun ExtensionPackageFinder.getPriority() =
|
||||
this::class.java.getDeclaredAnnotation(FinderRules::class.java)?.priority
|
||||
internal fun ExtensionPackageFinder.getPriority(): Int {
|
||||
val value = this::class.java.getDeclaredAnnotation(FinderRules::class.java)?.priority
|
||||
?: throw NoSuchFieldException("Finder did not add `FinderRules` annotation")
|
||||
if (value < 0) {
|
||||
throw IllegalArgumentException("Priority cannot be lower than 0. (Class: ${this::class.java})")
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 [AutoCloseable] 对象注册 Jvm Shutdown 钩子.
|
||||
@ -76,30 +80,19 @@ private object UtilsInternal {
|
||||
val autoCloseableSet = mutableSetOf<AutoCloseable>()
|
||||
|
||||
init {
|
||||
Runtime.getRuntime().addShutdownHook(Thread({
|
||||
log.debug { "Closing registered hook resources..." }
|
||||
autoCloseableSet.forEach {
|
||||
try {
|
||||
it.close()
|
||||
} catch (e: Exception) {
|
||||
log.error(e) { "An exception occurred while closing the resource. (Resource: `$it`)" }
|
||||
}
|
||||
Runtime.getRuntime().addShutdownHook(Thread(this::doCloseResources, "Shutdown-AutoCloseable"))
|
||||
}
|
||||
|
||||
fun doCloseResources() {
|
||||
log.debug { "Closing registered hook resources..." }
|
||||
autoCloseableSet.removeIf {
|
||||
try {
|
||||
it.close()
|
||||
} catch (e: Exception) {
|
||||
log.error(e) { "An exception occurred while closing the resource. (Resource: `$it`)" }
|
||||
}
|
||||
log.debug { "All registered hook resources have been closed." }
|
||||
}, "Shutdown-AutoCloseable"))
|
||||
}
|
||||
}
|
||||
|
||||
fun URL.resolveToFile(canonical: Boolean = true): File {
|
||||
if ("file" != protocol) {
|
||||
throw ClassCastException("Only the URL of the `file` protocol can be converted into a File object.")
|
||||
}
|
||||
|
||||
val urlString = toString().substringAfter(':')
|
||||
val file = File(urlString)
|
||||
return if (canonical) {
|
||||
file.canonicalFile
|
||||
} else {
|
||||
file
|
||||
true
|
||||
}
|
||||
log.debug { "All registered hook resources have been closed." }
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,20 @@
|
||||
package net.lamgc.scalabot
|
||||
|
||||
import com.github.stefanbirkner.systemlambda.SystemLambda
|
||||
import com.google.gson.Gson
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
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
|
||||
|
||||
class BotAccountTest {
|
||||
internal class BotAccountTest {
|
||||
|
||||
@Test
|
||||
fun deserializerTest() {
|
||||
@ -29,3 +37,91 @@ class BotAccountTest {
|
||||
|
||||
}
|
||||
|
||||
internal class AppPathsTest {
|
||||
|
||||
@Test
|
||||
fun `Data root path priority`() {
|
||||
System.setProperty("bot.path.data", "fromSystemProperties")
|
||||
|
||||
assertEquals("fromSystemProperties", AppPaths.DATA_ROOT.file.path, "`DATA_ROOT`没有优先返回 Property 的值.")
|
||||
System.getProperties().remove("bot.path.data")
|
||||
|
||||
val expectEnvValue = "fromEnvironmentVariable"
|
||||
SystemLambda.withEnvironmentVariable("BOT_DATA_PATH", expectEnvValue).execute {
|
||||
assertEquals(
|
||||
expectEnvValue, AppPaths.DATA_ROOT.file.path,
|
||||
"`DATA_ROOT`没有优先返回 env 的值."
|
||||
)
|
||||
}
|
||||
|
||||
SystemLambda.withEnvironmentVariable("BOT_DATA_PATH", null).execute {
|
||||
assertEquals(
|
||||
System.getProperty("user.dir"), AppPaths.DATA_ROOT.file.path,
|
||||
"`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`没有返回替补值 `.`(当前目录).")
|
||||
System.setProperty("user.dir", userDir)
|
||||
assertNotNull(System.getProperty("user.dir"), "环境还原失败!")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `default initializer`(@TempDir testDir: File) {
|
||||
val defaultInitializerMethod = Class.forName("net.lamgc.scalabot.AppConfigsKt")
|
||||
.getDeclaredMethod("defaultInitializer", AppPaths::class.java)
|
||||
.apply { isAccessible = true }
|
||||
|
||||
val dirPath = "${testDir.canonicalPath}/directory/"
|
||||
val dirFile = File(dirPath)
|
||||
mockk<AppPaths> {
|
||||
every { file }.returns(File(dirPath))
|
||||
every { path }.returns(dirPath)
|
||||
every { initial() }.answers {
|
||||
defaultInitializerMethod.invoke(null, this@mockk)
|
||||
}
|
||||
}.initial()
|
||||
assertTrue(dirFile.exists() && dirFile.isDirectory, "默认初始器未正常初始化【文件夹】.")
|
||||
|
||||
File(testDir, "test.txt").apply {
|
||||
mockk<AppPaths> {
|
||||
every { file }.returns(this@apply)
|
||||
every { path }.returns(this@apply.canonicalPath)
|
||||
every { initial() }.answers {
|
||||
defaultInitializerMethod.invoke(null, this@mockk)
|
||||
}
|
||||
}.initial()
|
||||
assertTrue(this@apply.exists() && this@apply.isFile, "默认初始器未正常初始化【文件】.")
|
||||
}
|
||||
|
||||
val alreadyExistsFile = File("${testDir.canonicalPath}/alreadyExists.txt").apply {
|
||||
if (!exists()) {
|
||||
createNewFile()
|
||||
}
|
||||
}
|
||||
assertTrue(alreadyExistsFile.exists(), "文件状态与预期不符.")
|
||||
mockk<File> {
|
||||
every { exists() }.returns(true)
|
||||
every { canonicalPath }.answers { alreadyExistsFile.canonicalPath }
|
||||
every { createNewFile() }.answers { alreadyExistsFile.createNewFile() }
|
||||
every { mkdirs() }.answers { alreadyExistsFile.mkdirs() }
|
||||
every { mkdir() }.answers { alreadyExistsFile.mkdir() }
|
||||
}.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 = 0) { createNewFile() }
|
||||
verify(exactly = 0) { mkdir() }
|
||||
verify(exactly = 0) { mkdirs() }
|
||||
}
|
||||
|
||||
defaultInitializerMethod.isAccessible = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -18,13 +18,21 @@ internal class ArtifactSerializerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serialize() {
|
||||
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 deserialize() {
|
||||
val gav = "org.example.software:test:1.0.0-SNAPSHOT"
|
||||
|
325
scalabot-app/src/test/kotlin/util/SerializersKtTest.kt
Normal file
325
scalabot-app/src/test/kotlin/util/SerializersKtTest.kt
Normal file
@ -0,0 +1,325 @@
|
||||
package util
|
||||
|
||||
import com.google.gson.*
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import net.lamgc.scalabot.MavenRepositoryConfig
|
||||
import net.lamgc.scalabot.util.AuthenticationSerializer
|
||||
import net.lamgc.scalabot.util.MavenRepositoryConfigSerializer
|
||||
import net.lamgc.scalabot.util.ProxyTypeSerializer
|
||||
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 org.telegram.telegrambots.bots.DefaultBotOptions
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Type
|
||||
import java.net.URL
|
||||
import kotlin.test.*
|
||||
|
||||
internal class SerializersKtTest {
|
||||
|
||||
private val instance: Any
|
||||
private val method: Method
|
||||
|
||||
init {
|
||||
val clazz = Class.forName("net.lamgc.scalabot.util.SerializerUtils")
|
||||
method = clazz.getDeclaredMethod("checkJsonKey", JsonObject::class.java, String::class.java)
|
||||
method.isAccessible = true
|
||||
instance = clazz.getDeclaredField("INSTANCE").apply {
|
||||
isAccessible = true
|
||||
}.get(null)
|
||||
}
|
||||
|
||||
private fun invoke(json: JsonObject, key: String): String {
|
||||
try {
|
||||
return method.invoke(instance, json, key) as String
|
||||
} catch (e: InvocationTargetException) {
|
||||
throw e.targetException
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Json key checker test`() {
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
invoke(JsonObject(), "NOT_EXIST_KEY")
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
invoke(JsonObject().apply { add("NULL_KEY", JsonNull.INSTANCE) }, "NULL_KEY")
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
invoke(JsonObject().apply { add("ARRAY_KEY", JsonArray()) }, "ARRAY_KEY")
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
invoke(JsonObject().apply { add("OBJECT_KEY", JsonObject()) }, "OBJECT_KEY")
|
||||
}
|
||||
|
||||
val expectKey = "TEST"
|
||||
val expectString = "testString"
|
||||
val json = JsonObject().apply { addProperty(expectKey, expectString) }
|
||||
|
||||
assertEquals(expectString, invoke(json, expectKey))
|
||||
}
|
||||
}
|
||||
|
||||
internal class ProxyTypeSerializerTest {
|
||||
|
||||
@Test
|
||||
fun `serialize test`() {
|
||||
for (type in DefaultBotOptions.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(
|
||||
DefaultBotOptions.ProxyType.NO_PROXY,
|
||||
ProxyTypeSerializer.deserialize(JsonNull.INSTANCE, null, null)
|
||||
)
|
||||
|
||||
for (type in DefaultBotOptions.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,
|
||||
TestJsonDeserializationContext
|
||||
)
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
MavenRepositoryConfigSerializer.deserialize(
|
||||
JsonNull.INSTANCE,
|
||||
MavenRepositoryConfig::class.java,
|
||||
TestJsonDeserializationContext
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `json primitive deserialize test`() {
|
||||
val expectRepoUrl = "https://repo.example.org/maven"
|
||||
val config = MavenRepositoryConfigSerializer.deserialize(
|
||||
JsonPrimitive(expectRepoUrl),
|
||||
MavenRepositoryConfig::class.java,
|
||||
TestJsonDeserializationContext
|
||||
)
|
||||
|
||||
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,
|
||||
TestJsonDeserializationContext
|
||||
)
|
||||
|
||||
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,
|
||||
TestJsonDeserializationContext
|
||||
)
|
||||
|
||||
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,
|
||||
TestJsonDeserializationContext
|
||||
)
|
||||
|
||||
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("authentication", JsonArray())
|
||||
jsonObject.add("layout", mockk<JsonPrimitive> {
|
||||
every { asString }.returns(null)
|
||||
})
|
||||
|
||||
config = MavenRepositoryConfigSerializer.deserialize(
|
||||
jsonObject,
|
||||
MavenRepositoryConfig::class.java,
|
||||
TestJsonDeserializationContext
|
||||
)
|
||||
|
||||
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,
|
||||
TestJsonDeserializationContext
|
||||
)
|
||||
|
||||
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 object TestJsonDeserializationContext : JsonDeserializationContext {
|
||||
|
||||
private val gson = GsonBuilder()
|
||||
.registerTypeAdapter(Authentication::class.java, AuthenticationSerializer)
|
||||
.create()
|
||||
|
||||
override fun <T : Any?> deserialize(json: JsonElement, typeOfT: Type): T {
|
||||
return gson.fromJson(json, typeOfT)
|
||||
}
|
||||
}
|
||||
|
||||
internal class AuthenticationSerializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize test`() {
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
AuthenticationSerializer.deserialize(
|
||||
JsonNull.INSTANCE,
|
||||
Authentication::class.java, TestJsonDeserializationContext
|
||||
)
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
AuthenticationSerializer.deserialize(
|
||||
JsonArray(),
|
||||
Authentication::class.java, TestJsonDeserializationContext
|
||||
)
|
||||
}
|
||||
assertThrows(JsonParseException::class.java) {
|
||||
AuthenticationSerializer.deserialize(
|
||||
JsonPrimitive("A STRING"),
|
||||
Authentication::class.java, TestJsonDeserializationContext
|
||||
)
|
||||
}
|
||||
|
||||
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, TestJsonDeserializationContext
|
||||
)
|
||||
|
||||
assertNotNull(result)
|
||||
result.fill(mockContext, "username", null)
|
||||
result.fill(mockContext, "password", null)
|
||||
|
||||
verify {
|
||||
mockContext.put("username", "testUsername")
|
||||
mockContext.put("password", "testPassword".toCharArray())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
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,21 @@
|
||||
package net.lamgc.scalabot.util
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.justRun
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import net.lamgc.scalabot.ExtensionPackageFinder
|
||||
import net.lamgc.scalabot.FinderPriority
|
||||
import net.lamgc.scalabot.FinderRules
|
||||
import net.lamgc.scalabot.FoundExtensionPackage
|
||||
import org.eclipse.aether.artifact.Artifact
|
||||
import org.eclipse.aether.artifact.DefaultArtifact
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import java.io.File
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlin.test.*
|
||||
|
||||
internal class UtilsKtTest {
|
||||
|
||||
@ -16,4 +28,92 @@ internal class UtilsKtTest {
|
||||
.equalsArtifact(DefaultArtifact("com.example:demo-2:1.0.0-SNAPSHOT"))
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bytes to hex`() {
|
||||
assertEquals("48656c6c6f20576f726c64", "Hello World".toByteArray(StandardCharsets.UTF_8).toHexString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ExtensionPackageFinder - getPriority`() {
|
||||
open class BaseTestFinder : ExtensionPackageFinder {
|
||||
override fun findByArtifact(extensionArtifact: Artifact, extensionsPath: File): Set<FoundExtensionPackage> {
|
||||
throw IllegalStateException("Calling this class is not allowed.")
|
||||
}
|
||||
}
|
||||
|
||||
@FinderRules(FinderPriority.ALTERNATE)
|
||||
class StandardTestFinder : BaseTestFinder()
|
||||
assertEquals(
|
||||
FinderPriority.ALTERNATE, StandardTestFinder().getPriority(),
|
||||
"获取到的优先值与预期不符"
|
||||
)
|
||||
|
||||
@FinderRules(-1)
|
||||
class OutOfRangePriorityFinder : BaseTestFinder()
|
||||
assertThrows<IllegalArgumentException>("getPriority 方法没有对超出范围的优先值抛出异常.") {
|
||||
OutOfRangePriorityFinder().getPriority()
|
||||
}
|
||||
|
||||
class NoAnnotationFinder : BaseTestFinder()
|
||||
assertThrows<NoSuchFieldException> {
|
||||
NoAnnotationFinder().getPriority()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AutoCloseable shutdown hook`() {
|
||||
val utilsInternalClass = Class.forName("net.lamgc.scalabot.util.UtilsInternal")
|
||||
val utilsInternalObject = utilsInternalClass.getDeclaredField("INSTANCE").get(null)
|
||||
?: fail("无法获取 UtilsInternal 对象.")
|
||||
val doCloseResourcesMethod = utilsInternalClass.getDeclaredMethod("doCloseResources")
|
||||
.apply {
|
||||
isAccessible = true
|
||||
}
|
||||
|
||||
// 正常的运行过程.
|
||||
val mockResource = mockk<AutoCloseable> {
|
||||
justRun { close() }
|
||||
}.registerShutdownHook()
|
||||
doCloseResourcesMethod.invoke(utilsInternalObject)
|
||||
verify { mockResource.close() }
|
||||
|
||||
// 异常捕获检查.
|
||||
val exceptionMockResource = mockk<AutoCloseable> {
|
||||
every { close() } throws RuntimeException("Expected exception.")
|
||||
}.registerShutdownHook()
|
||||
assertDoesNotThrow("在关闭资源时出现未捕获异常.") {
|
||||
doCloseResourcesMethod.invoke(utilsInternalObject)
|
||||
}
|
||||
verify { exceptionMockResource.close() }
|
||||
|
||||
// 错误抛出检查.
|
||||
val errorMockResource = mockk<AutoCloseable> {
|
||||
every { close() } throws Error("Expected error.")
|
||||
}.registerShutdownHook()
|
||||
assertThrows<Error>("关闭资源时捕获了不该捕获的 Error.") {
|
||||
try {
|
||||
doCloseResourcesMethod.invoke(utilsInternalObject)
|
||||
} catch (e: InvocationTargetException) {
|
||||
throw e.targetException
|
||||
}
|
||||
}
|
||||
verify { errorMockResource.close() }
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val resourceSet = utilsInternalClass.getDeclaredMethod("getAutoCloseableSet").invoke(utilsInternalObject)
|
||||
as MutableSet<AutoCloseable>
|
||||
resourceSet.clear()
|
||||
|
||||
val closeRef = mockk<AutoCloseable> {
|
||||
justRun { close() }
|
||||
}
|
||||
resourceSet.add(closeRef)
|
||||
assertTrue(resourceSet.contains(closeRef), "测试用资源虚引用添加失败.")
|
||||
doCloseResourcesMethod.invoke(utilsInternalObject)
|
||||
assertFalse(resourceSet.contains(closeRef), "资源虚引用未从列表中删除.")
|
||||
|
||||
resourceSet.clear()
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ plugins {
|
||||
kotlin("jvm") version "1.6.10"
|
||||
java
|
||||
`maven-publish`
|
||||
signing
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@ -38,24 +39,21 @@ tasks.withType<KotlinCompile> {
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
val repoRootKey = "maven.repo.local.root"
|
||||
val snapshot = project.version.toString().endsWith("-SNAPSHOT")
|
||||
val repoRoot = System.getProperty(repoRootKey)?.trim()
|
||||
if (repoRoot == null || repoRoot.isEmpty()) {
|
||||
logger.warn(
|
||||
"\"$repoRootKey\" configuration item is not specified, " +
|
||||
"please add start parameter \"-D$repoRootKey {localPublishRepo}\"" +
|
||||
" (if you are not currently executing the publish task, " +
|
||||
"you can ignore this information)"
|
||||
)
|
||||
return@repositories
|
||||
}
|
||||
val repoUri = if (snapshot) {
|
||||
uri("$repoRoot/snapshots")
|
||||
if (project.version.toString().endsWith("-SNAPSHOT")) {
|
||||
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 {
|
||||
uri("$repoRoot/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()
|
||||
}
|
||||
}
|
||||
}
|
||||
maven(repoUri)
|
||||
}
|
||||
|
||||
publications {
|
||||
@ -97,3 +95,8 @@ publishing {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
signing {
|
||||
useGpgCmd()
|
||||
sign(publishing.publications["maven"])
|
||||
}
|
||||
|
Reference in New Issue
Block a user