Files
tamabox/internal/security/censor/policy_test.go
T
2026-09-10 18:04:19 +08:00

288 lines
9.4 KiB
Go

// Copyright 2022 E99p1ant. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package censor
import (
"testing"
"github.com/stretchr/testify/assert"
)
// resp 构造一个带指定维度判定的审核结果,省去每个用例重复拼结构体。
func resp(suggestion string, details ...LabelScore) *TextCensorResponse {
return &TextCensorResponse{
SourceName: "qiniu",
Pass: suggestion != SuggestionBlock,
ProviderSuggestion: suggestion,
Details: details,
}
}
func TestDecideBaselineAlwaysBlocks(t *testing.T) {
// 底线维度(涉政 / 暴恐 / 色情 / 自定义词库)是强制项:任何档位、任何置信度都必须拦截,
// 且不受审核服务整体建议的影响(这里刻意给 pass 建议,验证仍然拦得住)。
baselines := []ForbiddenType{
ForbiddenTypePolitics,
ForbiddenTypeTerrorism,
ForbiddenTypePorn,
ForbiddenTypeCustomized,
}
levels := []string{"lax", "standard", "strict", "", "unknown-level"}
for _, label := range baselines {
for _, level := range levels {
t.Run(string(label)+"/"+level, func(t *testing.T) {
decision, detail := Decide(level, resp(SuggestionPass, LabelScore{Label: label, Score: 0.01}))
assert.Equal(t, DecisionBlock, decision)
assert.NotNil(t, detail)
assert.Equal(t, label, detail.Label)
})
}
}
}
func TestDecideByLevel(t *testing.T) {
for _, tc := range []struct {
name string
level string
response *TextCensorResponse
wantBlock bool
wantDetail ForbiddenType
}{
{
name: "宽松:高置信度辱骂也放行",
level: "lax",
response: resp(SuggestionReview, LabelScore{Label: ForbiddenTypeAbuse, Score: 0.99}),
wantBlock: false,
wantDetail: "",
},
{
name: "宽松:垃圾信息放行",
level: "lax",
response: resp(SuggestionPass, LabelScore{Label: ForbiddenTypeSpam, Score: 0.9}),
wantBlock: false,
wantDetail: "",
},
{
name: "宽松:不采信审核服务的整体 block 建议",
level: "lax",
response: resp(SuggestionBlock),
wantBlock: false,
wantDetail: "",
},
{
name: "标准:高置信度辱骂拦截",
level: "standard",
response: resp(SuggestionReview, LabelScore{Label: ForbiddenTypeAbuse, Score: 0.95}),
wantBlock: true,
wantDetail: ForbiddenTypeAbuse,
},
{
name: "标准:辱骂置信度不足 90% 放行(收敛误伤)",
level: "standard",
response: resp(SuggestionReview, LabelScore{Label: ForbiddenTypeAbuse, Score: 0.72}),
wantBlock: false,
wantDetail: "",
},
{
name: "标准:辱骂恰好 90% 拦截(阈值含等号)",
level: "standard",
response: resp(SuggestionReview, LabelScore{Label: ForbiddenTypeAbuse, Score: 0.9}),
wantBlock: true,
wantDetail: ForbiddenTypeAbuse,
},
{
name: "标准:违禁不限置信度",
level: "standard",
response: resp(SuggestionPass, LabelScore{Label: ForbiddenTypeContraband, Score: 0.3}),
wantBlock: true,
wantDetail: ForbiddenTypeContraband,
},
{
name: "标准:垃圾信息放行",
level: "standard",
response: resp(SuggestionPass, LabelScore{Label: ForbiddenTypeSpam, Score: 0.99}),
wantBlock: false,
wantDetail: "",
},
{
name: "标准:不采信整体 block 建议",
level: "standard",
response: resp(SuggestionBlock),
wantBlock: false,
wantDetail: "",
},
{
name: "严格:辱骂不限置信度拦截(最严档)",
level: "strict",
response: resp(SuggestionReview, LabelScore{Label: ForbiddenTypeAbuse, Score: 0.2}),
wantBlock: true,
wantDetail: ForbiddenTypeAbuse,
},
{
name: "严格:无意义内容拦截",
level: "strict",
response: resp(SuggestionReview, LabelScore{Label: ForbiddenTypeMeaningless, Score: 0.5}),
wantBlock: true,
wantDetail: ForbiddenTypeMeaningless,
},
{
name: "严格:采信整体 block 建议(无明细维度)",
level: "strict",
response: resp(SuggestionBlock),
wantBlock: true,
wantDetail: "",
},
{
name: "严格:仅 review 且无维度命中则放行",
level: "strict",
response: resp(SuggestionReview),
wantBlock: false,
wantDetail: "",
},
{
name: "全部正常文本放行",
level: "strict",
response: resp(SuggestionPass),
wantBlock: false,
wantDetail: "",
},
} {
t.Run(tc.name, func(t *testing.T) {
decision, detail := Decide(tc.level, tc.response)
assert.Equal(t, tc.wantBlock, decision.Blocked())
if tc.wantDetail == "" {
assert.Nil(t, detail)
return
}
assert.NotNil(t, detail)
assert.Equal(t, tc.wantDetail, detail.Label)
})
}
}
func TestDecideNilResponse(t *testing.T) {
// 空结果(审核未返回)不应被当成命中,避免把「拿不到结论」误判为违规。
decision, detail := Decide("strict", nil)
assert.Equal(t, DecisionAllow, decision)
assert.Nil(t, detail)
}
func TestDecideUnknownLevelFallsBackToStrict(t *testing.T) {
// 脏数据 / 未识别取值必须回退「严格」,方向保守:
// 宁可沿用升级前的严格判定,也不要因为认不出档位就静默放宽。
unknown := resp(SuggestionPass, LabelScore{Label: ForbiddenTypeSpam, Score: 0.9})
decision, _ := Decide("", unknown)
assert.Equal(t, DecisionBlock, decision)
decision, _ = Decide("bogus", unknown)
assert.Equal(t, DecisionBlock, decision)
}
func TestStrictnessHelpers(t *testing.T) {
assert.True(t, IsValidStrictness("lax"))
assert.True(t, IsValidStrictness("standard"))
assert.True(t, IsValidStrictness("strict"))
assert.False(t, IsValidStrictness(""))
assert.False(t, IsValidStrictness("STRICT"))
assert.False(t, IsValidStrictness("off"))
assert.Equal(t, StrictnessStrict, ParseStrictness(""))
assert.Equal(t, StrictnessStrict, ParseStrictness("nope"))
assert.Equal(t, StrictnessLax, ParseStrictness("lax"))
assert.Equal(t, StrictnessStandard, ParseStrictness("standard"))
}
func TestQiniuParserFeedsPolicy(t *testing.T) {
// 策略层完全依赖 parser 填好的 Details / ProviderSuggestion,这里做一次端到端校验:
// 审核服务给出 review(自身判定为放行)但带 abuse 维度,严格档仍应拦得住。
raw := []byte(`{
"code": 200,
"message": "ok",
"result": {
"suggestion": "review",
"scenes": {
"antispam": {
"suggestion": "review",
"details": [
{"label": "normal", "score": 0.02, "contexts": []},
{"label": "abuse", "score": 0.94, "contexts": [{"context": "示例片段", "positions": [{"startPos": 0, "endPos": 4}]}]}
]
}
}
}
}`)
parsed, err := QiniuTextCensorParser(raw)
assert.NoError(t, err)
// normal 维度必须被过滤掉,只保留真正命中的维度。
assert.Len(t, parsed.Details, 1)
assert.Equal(t, ForbiddenTypeAbuse, parsed.Details[0].Label)
assert.Equal(t, 0.94, parsed.Details[0].Score)
assert.Equal(t, SuggestionReview, parsed.ProviderSuggestion)
assert.Equal(t, "示例片段", parsed.Hint)
// 审核服务自己判 review(Pass=true),但档位策略仍可覆盖它。
assert.True(t, parsed.Pass)
decision, detail := Decide("strict", parsed)
assert.Equal(t, DecisionBlock, decision)
assert.NotNil(t, detail)
assert.Equal(t, ForbiddenTypeAbuse, detail.Label)
decision, _ = Decide("standard", parsed)
assert.Equal(t, DecisionBlock, decision)
decision, _ = Decide("lax", parsed)
assert.False(t, decision.Blocked())
}
func TestQiniuParserProviderSuggestionFallback(t *testing.T) {
// 外层 suggestion 缺失时回退到 antispam 场景内的建议,避免整体建议丢失后
// 严格档的「采信整体 block」分支失效。
raw := []byte(`{"code":200,"result":{"scenes":{"antispam":{"suggestion":"block","details":[]}}}}`)
parsed, err := QiniuTextCensorParser(raw)
assert.NoError(t, err)
assert.Equal(t, SuggestionBlock, parsed.ProviderSuggestion)
assert.Empty(t, parsed.Details)
// 严格档:无具体维度,但整体建议为 block → 拦截。
decision, detail := Decide("strict", parsed)
assert.Equal(t, DecisionBlock, decision)
assert.Nil(t, detail)
// 标准档不采信整体建议 → 放行(这是「少误伤」的核心差异)。
decision, _ = Decide("standard", parsed)
assert.False(t, decision.Blocked())
}
func TestBlockedLabelsIncludesBaselineAtEveryLevel(t *testing.T) {
// 供设置页展示拦截范围:底线维度必须出现在每一个档位的清单里。
for _, level := range []string{"lax", "standard", "strict"} {
labels := BlockedLabels(level)
assert.Contains(t, labels, ForbiddenTypePolitics, "level=%s", level)
assert.Contains(t, labels, ForbiddenTypeTerrorism, "level=%s", level)
assert.Contains(t, labels, ForbiddenTypePorn, "level=%s", level)
assert.Contains(t, labels, ForbiddenTypeCustomized, "level=%s", level)
// 不重复展示底线维度。
seen := map[ForbiddenType]int{}
for _, label := range labels {
seen[label]++
}
for label, count := range seen {
assert.Equal(t, 1, count, "label %s 重复出现", label)
}
}
// 宽松档只含底线四个维度(涉政 / 暴恐 / 色情 / 自定义词库)。
assert.Len(t, BlockedLabels("lax"), 4)
assert.Contains(t, BlockedLabels("standard"), ForbiddenTypeAbuse)
assert.NotContains(t, BlockedLabels("standard"), ForbiddenTypeSpam)
assert.Contains(t, BlockedLabels("strict"), ForbiddenTypeSpam)
}