package com.accounting.service; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; /** * 脱敏器的单测 —— 这是整个 AI 链路里最不能出错的一个类, * 用例按「用户笔记里真实出现过的形态」来写。 */ class SensitiveDataMaskerTest { private final SensitiveDataMasker masker = new SensitiveDataMasker(); @Test @DisplayName("IPv4 地址被脱敏") void masksIpv4() { String out = masker.mask("服务器 IP 是 155.103.159.109,SSH 端口 31022"); assertFalse(out.contains("155.103.159.109")); assertTrue(out.contains("[REDACTED_IP_")); assertTrue(out.contains("31022")); // 端口不算敏感,保留 } @Test @DisplayName("password 键值对:键名保留,值被替换") void masksPasswordKv() { String out = masker.mask("MySQL root 密码:mysql_jP65Fc,登录后记得改"); assertFalse(out.contains("mysql_jP65Fc")); assertTrue(out.contains("密码:[REDACTED_SECRET_")); } @Test @DisplayName("password= 等号形式同样生效") void masksPasswordWithEquals() { String out = masker.mask("jdbc:url?user=root&password=mySecret123"); assertFalse(out.contains("mySecret123")); } @Test @DisplayName("PEM 私钥整块被脱敏") void masksPrivateKeyBlock() { String key = "-----BEGIN OPENSSH PRIVATE KEY-----\n" + "b3BlbnNzaC1rZXktdjEAAAAA\n" + "b3BlbnNzaC1rZXktdjEAAAAA\n" + "-----END OPENSSH PRIVATE KEY-----"; String out = masker.mask("私钥如下:\n" + key + "\n记得保管好"); assertFalse(out.contains("b3BlbnNzaC1rZXktdjEAAAAA")); assertTrue(out.contains("[REDACTED_KEY_1]")); } @Test @DisplayName("无键名的 AK/SK 长随机串被脱敏") void masksBareAccessToken() { String out = masker.mask("access key 是 LTAI5tDCJuB9YgLx4KeJwc9C 备用"); assertFalse(out.contains("LTAI5tDCJuB9YgLx4KeJwc9C")); } @Test @DisplayName("同一个值在多次出现时映射到同一个占位符") void keepsPlaceholderConsistent() { String out = masker.mask("密码:abc12345,重复一下密码:abc12345"); String first = out.substring(out.indexOf("[REDACTED_")); assertEquals(1, countOccurrences(out, "[REDACTED_SECRET_1]"), "同一值应得到同一占位符: " + out); assertFalse(out.contains(first + "[")); } @Test @DisplayName("普通内容不误伤:中文、短数字、文件名都保留") void keepsNormalContent() { String text = "笔记在 migration_add_note.sql,共 3 篇,端口 31022,金额 ¥134.00"; assertEquals(text, masker.mask(text)); } @Test @DisplayName("「密码:无」这种非秘密不被误伤") void keepsShortNonSecretValues() { String out = masker.mask("密码:无"); assertTrue(out.contains("密码:无")); } @Test @DisplayName("null 和空串原样返回") void handlesNullAndEmpty() { assertEquals(null, masker.mask(null)); assertEquals("", masker.mask("")); } private int countOccurrences(String text, String target) { int count = 0; int idx = 0; while ((idx = text.indexOf(target, idx)) >= 0) { count++; idx += target.length(); } return count; } }