feat: 完善白名单审计系统&单元测试
This commit is contained in:
parent
e8fbd30c5e
commit
23ca0454ff
|
|
@ -3,5 +3,5 @@ org.gradle.downloadSources=false
|
|||
org.gradle.parallel=true
|
||||
org.gradle.degree_of_parallelism=16
|
||||
project_group=top.r3944realms.ltdmanager
|
||||
project_version=1.21-SNAPSHOT
|
||||
project_version=1.22-SNAPSHOT
|
||||
dg_lab_version=4.4.14.19
|
||||
|
|
|
|||
|
|
@ -258,10 +258,11 @@ object ModuleFactory {
|
|||
}
|
||||
|
||||
private fun createWhitelistAudit(config: ModuleConfig.Module): WhitelistAuditModule {
|
||||
val whitelistGroupId = config.long("whitelist-group-id")
|
||||
val filterQqList = config.list<Long>("filter-qq-list").toSet()
|
||||
val auditAllowedUsers = config.list<Long>("audit-allowed-ids").toSet()
|
||||
val enableEmail = config.getOrDefault("enable-email", false)
|
||||
val reActivationKeywords = config.stringList("re-activation-keywords").toSet()
|
||||
val auditCommandPrefix = config.getOrDefault("audit-command-prefix", "审计")
|
||||
val gracePeriodDays = config.int("grace-period-days")
|
||||
val expiryWarningDays = config.int("expiry-warning-days")
|
||||
val pollIntervalMinutes = config.getOrDefault("poll-interval-minutes", 60L)
|
||||
|
|
@ -269,17 +270,29 @@ object ModuleFactory {
|
|||
val groupMessagePollingModule = resolveDependency(
|
||||
config.findDependency(GROUP_MESSAGE_POLLING_MODULE), "groupMessagePolling"
|
||||
) as GroupMessagePollingModule
|
||||
// 第二依赖:白名单群轮询模块(通过依赖名区分)
|
||||
val whitelistGroupPollingName = config.getOrDefault("whitelist-group-polling-dep-name", "")
|
||||
val whitelistGroupPollingModule = if (whitelistGroupPollingName.isNotEmpty()) {
|
||||
resolveDependency(
|
||||
config.dependencies?.find { it.name == whitelistGroupPollingName && it.type == GROUP_MESSAGE_POLLING_MODULE },
|
||||
"whitelistGroupPolling"
|
||||
) as GroupMessagePollingModule
|
||||
} else {
|
||||
groupMessagePollingModule
|
||||
}
|
||||
val mailModule = if (enableEmail) {
|
||||
resolveDependency(config.findDependency(MAIL_MODULE), "mailModule") as MailModule
|
||||
} else null
|
||||
return WhitelistAuditModule(
|
||||
config.name,
|
||||
whitelistGroupId,
|
||||
groupMessagePollingModule,
|
||||
whitelistGroupPollingModule,
|
||||
GlobalManager.whitelistSystemClient,
|
||||
mailModule,
|
||||
filterQqList,
|
||||
auditAllowedUsers,
|
||||
reActivationKeywords,
|
||||
auditCommandPrefix,
|
||||
gracePeriodDays,
|
||||
expiryWarningDays,
|
||||
pollIntervalMinutes,
|
||||
|
|
|
|||
|
|
@ -22,18 +22,22 @@ import java.io.File
|
|||
|
||||
class WhitelistAuditModule(
|
||||
moduleName: String,
|
||||
private val whitelistGroupId: Long,
|
||||
private val mainGroupPollingModule: GroupMessagePollingModule,
|
||||
private val whitelistGroupPollingModule: GroupMessagePollingModule,
|
||||
private val whitelistClient: WhitelistSystemClient,
|
||||
private val mailModule: MailModule?,
|
||||
private val filterQqList: Set<Long>,
|
||||
private val auditAllowedUsers: Set<Long>,
|
||||
private val reActivationKeywords: Set<String>,
|
||||
private val auditCommandPrefix: String,
|
||||
private val gracePeriodDays: Int,
|
||||
private val expiryWarningDays: Int,
|
||||
private val pollIntervalMinutes: Long,
|
||||
private val selfId: Long,
|
||||
) : BaseModule(Modules.WHITELIST_AUDIT, moduleName), PersistentState<WhitelistAuditModule.AuditState> {
|
||||
|
||||
private val whitelistGroupId: Long get() = whitelistGroupPollingModule.targetGroupId
|
||||
|
||||
private var scope: CoroutineScope? = null
|
||||
|
||||
private val stateFile: File = getStateFileInternal("whitelist_audit_state.json", name)
|
||||
|
|
@ -49,6 +53,9 @@ class WhitelistAuditModule(
|
|||
private var lastMsgRealId: Long = -1
|
||||
private var lastMsgTime: Long = 0
|
||||
|
||||
private var lastAuditRealId: Long = -1
|
||||
private var lastAuditTime: Long = 0
|
||||
|
||||
private val reactivationFilter by lazy {
|
||||
TriggerMessageFilter(
|
||||
listOf(
|
||||
|
|
@ -59,9 +66,20 @@ class WhitelistAuditModule(
|
|||
)
|
||||
}
|
||||
|
||||
private val auditCommandFilter by lazy {
|
||||
TriggerMessageFilter(
|
||||
listOf(
|
||||
IgnoreSelfFilter(selfId),
|
||||
NewMessageFilter { lastAuditTime to lastAuditRealId },
|
||||
KeywordFilter(setOf(auditCommandPrefix)),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onLoad() {
|
||||
LoggerUtil.logger.info("[$name] 白名单审计模块已装载")
|
||||
LoggerUtil.logger.info("[$name] 白名单群: $whitelistGroupId, 主群: ${mainGroupPollingModule.targetGroupId}")
|
||||
LoggerUtil.logger.info("[$name] 审计命令: $auditCommandPrefix")
|
||||
LoggerUtil.logger.info("[$name] 检查间隔: ${pollIntervalMinutes}分, 宽限期: ${gracePeriodDays}天, 过期警告: ${expiryWarningDays}天前")
|
||||
LoggerUtil.logger.info("[$name] 邮件通知: ${mailModule != null}")
|
||||
|
||||
|
|
@ -87,6 +105,15 @@ class WhitelistAuditModule(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope!!.launch {
|
||||
whitelistGroupPollingModule.messagesFlow.collect { messages ->
|
||||
if (loaded) {
|
||||
val filtered = auditCommandFilter.filter(messages)
|
||||
if (filtered.isNotEmpty()) handleAuditCommand(filtered)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onUnload() {
|
||||
|
|
@ -271,6 +298,118 @@ class WhitelistAuditModule(
|
|||
}
|
||||
}
|
||||
|
||||
// ======== 手动审计命令 ========
|
||||
|
||||
private suspend fun handleAuditCommand(messages: List<MsgHistorySpecificMsg>) {
|
||||
val msg = messages.maxByOrNull { it.time } ?: return
|
||||
|
||||
if (msg.userId !in auditAllowedUsers) {
|
||||
napCatClient.sendUnit(
|
||||
SendGroupMsgRequest(
|
||||
MessageElement.reply(ID.long(msg.realId), "你没有权限执行审计命令"),
|
||||
ID.long(whitelistGroupId)
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
lastAuditRealId = msg.realId
|
||||
lastAuditTime = msg.time
|
||||
|
||||
LoggerUtil.logger.info("[$name] ${msg.userId} 触发手动审计")
|
||||
val summary = runAdhocAudit()
|
||||
|
||||
napCatClient.sendUnit(
|
||||
SendGroupMsgRequest(
|
||||
MessageElement.reply(ID.long(msg.realId), summary),
|
||||
ID.long(whitelistGroupId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun runAdhocAudit(): String {
|
||||
val groupMembers = fetchWhitelistGroupMembers()
|
||||
if (groupMembers == null) return "获取群成员失败,审计中断"
|
||||
|
||||
val groupMemberQqSet = groupMembers.map { it.userId }.toSet()
|
||||
val whitelistEntries = whitelistClient.listApproved()
|
||||
if (whitelistEntries.isEmpty()) return "白名单通过数为0"
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val graceMs = gracePeriodDays * 24 * 60 * 60 * 1000L
|
||||
|
||||
var inGroupOk = 0
|
||||
var notInGroupNew = 0
|
||||
var notInGroupGrace = mutableListOf<String>()
|
||||
var notInGroupExpired = 0
|
||||
var inFilter = 0
|
||||
|
||||
for (entry in whitelistEntries) {
|
||||
val qqLong = entry.qq.toLongOrNull() ?: continue
|
||||
if (qqLong in filterQqList) { inFilter++; continue }
|
||||
if (qqLong in groupMemberQqSet) {
|
||||
inGroupOk++
|
||||
// 清除已有标记
|
||||
val key = entry.qq
|
||||
if (auditState.entries.containsKey(key)) {
|
||||
auditState = auditState.copy(
|
||||
entries = auditState.entries.toMutableMap().apply { remove(key) }
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
val key = entry.qq
|
||||
val existing = auditState.entries[key]
|
||||
|
||||
when {
|
||||
existing == null -> {
|
||||
notInGroupNew++
|
||||
whitelistClient.reject(entry.id)
|
||||
val newEntry = AuditEntry(
|
||||
qq = entry.qq,
|
||||
playerId = entry.id,
|
||||
playerName = entry.playerName,
|
||||
detectedTime = now,
|
||||
rejectedTime = now
|
||||
)
|
||||
auditState = auditState.copy(
|
||||
entries = auditState.entries.toMutableMap().apply { put(key, newEntry) }
|
||||
)
|
||||
}
|
||||
existing.rejectedTime > 0 && now - existing.rejectedTime >= graceMs -> {
|
||||
notInGroupExpired++
|
||||
whitelistClient.remove(entry.id)
|
||||
auditState = auditState.copy(
|
||||
entries = auditState.entries.toMutableMap().apply { remove(key) }
|
||||
)
|
||||
}
|
||||
existing.rejectedTime > 0 -> {
|
||||
val remainDays = ((graceMs - (now - existing.rejectedTime)) / (24 * 60 * 60 * 1000)).toInt()
|
||||
notInGroupGrace.add("${entry.playerName}(剩${remainDays}天)")
|
||||
}
|
||||
}
|
||||
}
|
||||
saveState(auditState)
|
||||
|
||||
return buildString {
|
||||
appendLine("审计完成")
|
||||
appendLine("─".repeat(16))
|
||||
appendLine("白名单总数: ${whitelistEntries.size}")
|
||||
appendLine("在群正常: $inGroupOk 人")
|
||||
appendLine("新发现不在群(已拒绝): $notInGroupNew 人")
|
||||
if (notInGroupGrace.isNotEmpty()) {
|
||||
appendLine("宽限期中: ${notInGroupGrace.joinToString()}")
|
||||
}
|
||||
if (notInGroupExpired > 0) {
|
||||
appendLine("已过期删除: $notInGroupExpired 人")
|
||||
}
|
||||
if (inFilter > 0) {
|
||||
appendLine("过滤列表跳过: $inFilter 人")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ======== 群成员获取 ========
|
||||
|
||||
private suspend fun fetchWhitelistGroupMembers(): List<GroupMemberData>? {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,215 @@
|
|||
package top.r394realms.ltdmanagertest.whitelist
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Test
|
||||
import top.r3944realms.ltdmanager.whitelist.data.WhitelistPlayerInfo
|
||||
import top.r3944realms.ltdmanager.whitelist.response.WhitelistApiResponse
|
||||
|
||||
class WhitelistApiResponseTest {
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true; coerceInputValues = true }
|
||||
|
||||
// ====== WhitelistApiResponse ======
|
||||
|
||||
@Test
|
||||
fun `deserialize list response with data`() {
|
||||
val response = json.decodeFromString<WhitelistApiResponse>(listResponseJson)
|
||||
assertEquals(200, response.code)
|
||||
assertEquals("查询成功", response.msg)
|
||||
assertEquals(42, response.count)
|
||||
assertEquals(2, response.data!!.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `list response player info fields`() {
|
||||
val response = json.decodeFromString<WhitelistApiResponse>(listResponseJson)
|
||||
val player = response.data!![0]
|
||||
|
||||
assertEquals(1, player.id)
|
||||
assertEquals("Steve", player.playerName)
|
||||
assertEquals("8667ba71b85a4004af54457a9734eed7", player.uuid)
|
||||
assertEquals("123456789", player.qq)
|
||||
assertEquals(1, player.status)
|
||||
assertEquals("北京市", player.regionFullName)
|
||||
assertEquals(85, player.totalScore)
|
||||
assertTrue(player.emailActive)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize action response (approve)`() {
|
||||
val response = json.decodeFromString<WhitelistApiResponse>(approveResponseJson)
|
||||
assertEquals(200, response.code)
|
||||
assertEquals("已加入白名单", response.msg)
|
||||
assertNull(response.data)
|
||||
assertEquals(0, response.count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize action response (reject)`() {
|
||||
val response = json.decodeFromString<WhitelistApiResponse>(rejectResponseJson)
|
||||
assertEquals(200, response.code)
|
||||
assertEquals("已拒绝申请", response.msg)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize action response (remove)`() {
|
||||
val response = json.decodeFromString<WhitelistApiResponse>(removeResponseJson)
|
||||
assertEquals(200, response.code)
|
||||
assertEquals("已从白名单移除", response.msg)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize error response`() {
|
||||
val response = json.decodeFromString<WhitelistApiResponse>(errorResponseJson)
|
||||
assertEquals(500, response.code)
|
||||
assertEquals("操作失败,Rcon连接错误或玩家不存在", response.msg)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize auth failure response`() {
|
||||
val response = json.decodeFromString<WhitelistApiResponse>(authFailJson)
|
||||
assertEquals(403, response.code)
|
||||
assertEquals("无权限:API Key 无效或未提供", response.msg)
|
||||
}
|
||||
|
||||
// ====== WhitelistPlayerInfo with all fields ======
|
||||
|
||||
@Test
|
||||
fun `deserialize full player info`() {
|
||||
val player = json.decodeFromString<WhitelistPlayerInfo>(fullPlayerJson)
|
||||
assertEquals(1, player.id)
|
||||
assertEquals("Steve", player.playerName)
|
||||
assertEquals("8667ba71b85a4004af54457a9734eed7", player.uuid)
|
||||
assertEquals("123456789", player.qq)
|
||||
assertEquals(1, player.status)
|
||||
assertEquals("我是建筑党", player.description)
|
||||
assertEquals("2026-06-01T12:00:00", player.createTime)
|
||||
assertEquals(110000L, player.regionCode)
|
||||
assertEquals("北京市", player.regionFullName)
|
||||
assertEquals(1, player.operatorId)
|
||||
assertEquals("admin", player.operatorUsername)
|
||||
assertEquals("管理员", player.operatorNickname)
|
||||
assertEquals(85, player.totalScore)
|
||||
assertTrue(player.emailActive)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize player with null operator fields`() {
|
||||
val player = json.decodeFromString<WhitelistPlayerInfo>(playerWithNullOperatorJson)
|
||||
assertEquals(2, player.id)
|
||||
assertEquals("Alex", player.playerName)
|
||||
assertEquals("2222222", player.qq)
|
||||
assertEquals(2, player.status)
|
||||
assertNull(player.operatorId)
|
||||
assertNull(player.operatorUsername)
|
||||
assertNull(player.operatorNickname)
|
||||
}
|
||||
|
||||
// ====== Empty/missing data ======
|
||||
|
||||
@Test
|
||||
fun `deserialize list response with empty data array`() {
|
||||
val response = this.json.decodeFromString<WhitelistApiResponse>(
|
||||
"""{"code":200,"msg":"查询成功","count":0,"data":[]}"""
|
||||
)
|
||||
assertEquals(0, response.count)
|
||||
assertTrue(response.data!!.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize response with null data`() {
|
||||
val response = json.decodeFromString<WhitelistApiResponse>(approveResponseJson)
|
||||
assertNull(response.data)
|
||||
}
|
||||
|
||||
// ====== Sample JSON payloads ======
|
||||
|
||||
companion object {
|
||||
val listResponseJson = """
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "查询成功",
|
||||
"count": 42,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"playerName": "Steve",
|
||||
"uuid": "8667ba71b85a4004af54457a9734eed7",
|
||||
"qq": "123456789",
|
||||
"status": 1,
|
||||
"description": "我是建筑党",
|
||||
"createTime": "2026-06-01T12:00:00",
|
||||
"regionCode": 110000,
|
||||
"regionFullName": "北京市",
|
||||
"operatorId": 1,
|
||||
"operatorUsername": "admin",
|
||||
"operatorNickname": "管理员",
|
||||
"totalScore": 85,
|
||||
"emailActive": true
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"playerName": "Alex",
|
||||
"uuid": "abc123",
|
||||
"qq": "987654321",
|
||||
"status": 1,
|
||||
"description": "",
|
||||
"createTime": "2026-06-02T08:00:00",
|
||||
"regionCode": 310000,
|
||||
"regionFullName": "上海市",
|
||||
"operatorId": null,
|
||||
"operatorUsername": null,
|
||||
"operatorNickname": null,
|
||||
"totalScore": 70,
|
||||
"emailActive": false
|
||||
}
|
||||
]
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val approveResponseJson = """{"code":200,"msg":"已加入白名单","data":null}"""
|
||||
val rejectResponseJson = """{"code":200,"msg":"已拒绝申请","data":null}"""
|
||||
val removeResponseJson = """{"code":200,"msg":"已从白名单移除","data":null}"""
|
||||
val errorResponseJson = """{"code":500,"msg":"操作失败,Rcon连接错误或玩家不存在","data":null}"""
|
||||
val authFailJson = """{"code":403,"msg":"无权限:API Key 无效或未提供","data":null}"""
|
||||
|
||||
val fullPlayerJson = """
|
||||
{
|
||||
"id": 1,
|
||||
"playerName": "Steve",
|
||||
"uuid": "8667ba71b85a4004af54457a9734eed7",
|
||||
"qq": "123456789",
|
||||
"status": 1,
|
||||
"description": "我是建筑党",
|
||||
"createTime": "2026-06-01T12:00:00",
|
||||
"regionCode": 110000,
|
||||
"regionFullName": "北京市",
|
||||
"operatorId": 1,
|
||||
"operatorUsername": "admin",
|
||||
"operatorNickname": "管理员",
|
||||
"totalScore": 85,
|
||||
"emailActive": true
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val playerWithNullOperatorJson = """
|
||||
{
|
||||
"id": 2,
|
||||
"playerName": "Alex",
|
||||
"uuid": "def456",
|
||||
"qq": "2222222",
|
||||
"status": 2,
|
||||
"description": "",
|
||||
"createTime": "2026-06-03T00:00:00",
|
||||
"regionCode": 0,
|
||||
"regionFullName": "",
|
||||
"operatorId": null,
|
||||
"operatorUsername": null,
|
||||
"operatorNickname": null,
|
||||
"totalScore": 0,
|
||||
"emailActive": false
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package top.r394realms.ltdmanagertest.whitelist
|
||||
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Test
|
||||
import top.r3944realms.ltdmanager.module.WhitelistAuditModule.AuditEntry
|
||||
import top.r3944realms.ltdmanager.module.WhitelistAuditModule.AuditState
|
||||
|
||||
class WhitelistAuditStateTest {
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true; coerceInputValues = true }
|
||||
|
||||
// ====== AuditState ======
|
||||
|
||||
@Test
|
||||
fun `empty AuditState round-trips correctly`() {
|
||||
val state = AuditState()
|
||||
val serialized = json.encodeToString(state)
|
||||
val deserialized = json.decodeFromString<AuditState>(serialized)
|
||||
|
||||
assertTrue(deserialized.entries.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AuditState round-trip with single entry`() {
|
||||
val entry = AuditEntry(
|
||||
qq = "123456789",
|
||||
playerId = 42,
|
||||
playerName = "Steve",
|
||||
detectedTime = 1717891200000L,
|
||||
rejectedTime = 1717891200000L,
|
||||
warningSentTime = 0L,
|
||||
reactivated = false,
|
||||
)
|
||||
val state = AuditState(entries = mapOf("123456789" to entry))
|
||||
|
||||
val serialized = json.encodeToString(state)
|
||||
val deserialized = json.decodeFromString<AuditState>(serialized)
|
||||
|
||||
assertEquals(1, deserialized.entries.size)
|
||||
val restored = deserialized.entries["123456789"]!!
|
||||
assertEquals("123456789", restored.qq)
|
||||
assertEquals(42, restored.playerId)
|
||||
assertEquals("Steve", restored.playerName)
|
||||
assertEquals(1717891200000L, restored.detectedTime)
|
||||
assertEquals(1717891200000L, restored.rejectedTime)
|
||||
assertEquals(0L, restored.warningSentTime)
|
||||
assertFalse(restored.reactivated)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AuditState round-trip with reactivated entry`() {
|
||||
val entry = AuditEntry(
|
||||
qq = "111",
|
||||
playerId = 1,
|
||||
playerName = "Alex",
|
||||
detectedTime = 1000L,
|
||||
rejectedTime = 1000L,
|
||||
warningSentTime = 5000L,
|
||||
reactivated = true,
|
||||
)
|
||||
val state = AuditState(entries = mapOf("111" to entry))
|
||||
|
||||
val serialized = json.encodeToString(state)
|
||||
val deserialized = json.decodeFromString<AuditState>(serialized)
|
||||
|
||||
assertTrue(deserialized.entries["111"]!!.reactivated)
|
||||
assertEquals(5000L, deserialized.entries["111"]!!.warningSentTime)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AuditState round-trip with multiple entries`() {
|
||||
val entries = mapOf(
|
||||
"aaa" to AuditEntry(qq = "aaa", playerId = 1, playerName = "A", detectedTime = 1L),
|
||||
"bbb" to AuditEntry(qq = "bbb", playerId = 2, playerName = "B", detectedTime = 2L, rejectedTime = 2L),
|
||||
"ccc" to AuditEntry(qq = "ccc", playerId = 3, playerName = "C", detectedTime = 3L, rejectedTime = 3L, warningSentTime = 3L, reactivated = true),
|
||||
)
|
||||
val state = AuditState(entries = entries)
|
||||
|
||||
val serialized = json.encodeToString(state)
|
||||
val deserialized = json.decodeFromString<AuditState>(serialized)
|
||||
|
||||
assertEquals(3, deserialized.entries.size)
|
||||
assertFalse(deserialized.entries["aaa"]!!.reactivated)
|
||||
assertEquals(0L, deserialized.entries["aaa"]!!.rejectedTime)
|
||||
assertTrue(deserialized.entries["ccc"]!!.reactivated)
|
||||
}
|
||||
|
||||
// ====== AuditEntry defaults ======
|
||||
|
||||
@Test
|
||||
fun `AuditEntry defaults are zero-empty-false`() {
|
||||
val entry = AuditEntry(
|
||||
qq = "test",
|
||||
playerId = 1,
|
||||
playerName = "test",
|
||||
detectedTime = 0L,
|
||||
)
|
||||
assertEquals(0L, entry.rejectedTime)
|
||||
assertEquals(0L, entry.warningSentTime)
|
||||
assertFalse(entry.reactivated)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AuditEntry copy preserves fields`() {
|
||||
val entry = AuditEntry(
|
||||
qq = "qq", playerId = 5, playerName = "P",
|
||||
detectedTime = 100L, rejectedTime = 200L, warningSentTime = 300L, reactivated = true,
|
||||
)
|
||||
val copied = entry.copy(warningSentTime = 400L, reactivated = false)
|
||||
|
||||
assertEquals("qq", copied.qq)
|
||||
assertEquals(5, copied.playerId)
|
||||
assertEquals(100L, copied.detectedTime)
|
||||
assertEquals(200L, copied.rejectedTime)
|
||||
assertEquals(400L, copied.warningSentTime)
|
||||
assertFalse(copied.reactivated)
|
||||
}
|
||||
|
||||
// ====== Deserialization with missing fields ======
|
||||
|
||||
@Test
|
||||
fun `deserialize AuditState with missing optional fields uses defaults`() {
|
||||
val jsonStr = """
|
||||
{"entries": {"test": {"qq":"test","playerId":1,"playerName":"T","detectedTime":0}}}
|
||||
""".trimIndent()
|
||||
|
||||
val state = json.decodeFromString<AuditState>(jsonStr)
|
||||
val entry = state.entries["test"]!!
|
||||
|
||||
assertEquals(0L, entry.rejectedTime)
|
||||
assertEquals(0L, entry.warningSentTime)
|
||||
assertFalse(entry.reactivated)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize empty JSON object as AuditState`() {
|
||||
val state = json.decodeFromString<AuditState>("{}")
|
||||
assertTrue(state.entries.isEmpty())
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user