| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518 |
- package service
- import (
- "context"
- "strings"
- "testing"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
- )
- func TestSecurityPolicyBuiltinKeywordsQuality(t *testing.T) {
- seeds := SecurityPolicyBuiltinKeywords()
- if len(seeds) < 100 {
- t.Fatalf("builtin seeds = %d, want >= 100", len(seeds))
- }
- seen := make(map[string]string, len(seeds))
- categories := make(map[string]int)
- for _, seed := range seeds {
- if strings.TrimSpace(seed.Keyword) == "" {
- t.Fatal("builtin seed must not be blank")
- }
- key := strings.ToLower(strings.TrimSpace(seed.Keyword))
- if prev, dup := seen[key]; dup {
- t.Fatalf("duplicate builtin seed %q (categories %q vs %q)", seed.Keyword, prev, seed.Category)
- }
- seen[key] = seed.Category
- categories[seed.Category]++
- }
- for _, category := range []string{
- SecurityPolicyCategoryCrack, SecurityPolicyCategoryReverse,
- SecurityPolicyCategoryPentest, SecurityPolicyCategoryPrivesc,
- SecurityPolicyCategoryEvasion,
- } {
- if categories[category] == 0 {
- t.Fatalf("builtin seeds missing category %q", category)
- }
- }
- }
- func TestSecurityPolicySnapshotMatch(t *testing.T) {
- custom := []SecurityPolicyKeyword{
- {ID: 1, Keyword: "零日交易", Category: SecurityPolicyCategoryCustom},
- {ID: 2, GroupID: securityPolicyInt64Ptr(9), Keyword: "小组黑话", Category: SecurityPolicyCategoryCustom},
- }
- snapshot := BuildSecurityPolicySnapshot(custom)
- if keyword, category, ok := snapshot.Match(9, "请教我写免杀马过火绒"); !ok || keyword == "" {
- t.Fatalf("builtin hit expected, got %q %q %v", keyword, category, ok)
- } else if category != SecurityPolicyCategoryEvasion {
- t.Fatalf("category = %q, want evasion", category)
- }
- if keyword, _, ok := snapshot.Match(9, "哪里可以零日交易"); !ok || keyword != "零日交易" {
- t.Fatalf("global custom hit expected, got %q %v", keyword, ok)
- }
- if keyword, _, ok := snapshot.Match(9, "这是小组黑话测试"); !ok || keyword != "小组黑话" {
- t.Fatalf("group custom hit expected, got %q %v", keyword, ok)
- }
- if _, _, ok := snapshot.Match(7, "这是小组黑话测试"); ok {
- t.Fatal("group-scoped word must not match other groups")
- }
- if _, _, ok := snapshot.Match(9, "今天天气不错,适合出去散步"); ok {
- t.Fatal("benign text must not match")
- }
- // 内置与自定义同时命中同一文本时不 panic,结果二选一即可。
- snapshot.Match(9, "免杀 零日交易")
- }
- func TestNormalizeSecurityPolicyMode(t *testing.T) {
- cases := []struct {
- input string
- want string
- }{
- {"", SecurityPolicyModeBlockSession},
- {"block_session", SecurityPolicyModeBlockSession},
- {"block_request", SecurityPolicyModeBlockRequest},
- {" BLOCK_REQUEST ", SecurityPolicyModeBlockRequest},
- {"nonsense", SecurityPolicyModeBlockSession},
- }
- for _, tc := range cases {
- if got := NormalizeSecurityPolicyMode(tc.input); got != tc.want {
- t.Fatalf("NormalizeSecurityPolicyMode(%q) = %q, want %q", tc.input, got, tc.want)
- }
- }
- }
- func securityPolicyInt64Ptr(value int64) *int64 { return &value }
- // --- EvaluateRequest 测试替身 ---
- type securityPolicyTestKeywordRepo struct {
- words []SecurityPolicyKeyword
- err error
- }
- func (r *securityPolicyTestKeywordRepo) ListEffectiveKeywords(context.Context, *int64) ([]SecurityPolicyKeyword, error) {
- return r.words, r.err
- }
- func (r *securityPolicyTestKeywordRepo) ListKeywords(context.Context, *int64, bool) ([]SecurityPolicyKeyword, error) {
- return r.words, r.err
- }
- func (r *securityPolicyTestKeywordRepo) CreateKeyword(_ context.Context, in SecurityPolicyKeywordInput) (*SecurityPolicyKeyword, error) {
- return &SecurityPolicyKeyword{ID: 1, Keyword: in.Keyword, Category: in.Category, Enabled: true}, nil
- }
- func (r *securityPolicyTestKeywordRepo) UpdateKeyword(_ context.Context, id int64, in SecurityPolicyKeywordUpdate) (*SecurityPolicyKeyword, error) {
- return &SecurityPolicyKeyword{ID: id, Enabled: true}, nil
- }
- func (r *securityPolicyTestKeywordRepo) DeleteKeyword(context.Context, int64) error { return nil }
- type securityPolicyTestSessionStore struct {
- blocked map[string]map[string]bool
- findErr error
- }
- func (s *securityPolicyTestSessionStore) MarkSessionBlocked(_ context.Context, scope string, fields []string, _ time.Duration) error {
- if s.blocked == nil {
- s.blocked = make(map[string]map[string]bool)
- }
- if s.blocked[scope] == nil {
- s.blocked[scope] = make(map[string]bool)
- }
- for _, field := range fields {
- s.blocked[scope][field] = true
- }
- return nil
- }
- func (s *securityPolicyTestSessionStore) FindBlockedSessionField(_ context.Context, scope string, fields []string) (string, error) {
- if s.findErr != nil {
- return "", s.findErr
- }
- for _, field := range fields {
- if s.blocked[scope][field] {
- return field, nil
- }
- }
- return "", nil
- }
- func (s *securityPolicyTestSessionStore) UnblockSessionScope(_ context.Context, scope string) (bool, error) {
- if len(s.blocked[scope]) == 0 {
- return false, nil
- }
- delete(s.blocked, scope)
- return true, nil
- }
- type securityPolicyTestModerationRepo struct {
- logs []ContentModerationLog
- overturned []int64
- }
- func (r *securityPolicyTestModerationRepo) CreateLog(_ context.Context, log *ContentModerationLog) error {
- log.ID = int64(len(r.logs) + 1)
- r.logs = append(r.logs, *log)
- return nil
- }
- func (r *securityPolicyTestModerationRepo) ListLogs(context.Context, ContentModerationLogFilter) ([]ContentModerationLog, *pagination.PaginationResult, error) {
- return nil, nil, nil
- }
- func (r *securityPolicyTestModerationRepo) CountFlaggedByUserSince(context.Context, int64, time.Time, bool) (int, error) {
- return 0, nil
- }
- func (r *securityPolicyTestModerationRepo) CleanupExpiredLogs(context.Context, time.Time, time.Time) (*ContentModerationCleanupResult, error) {
- return &ContentModerationCleanupResult{}, nil
- }
- func (r *securityPolicyTestModerationRepo) UpdateLogEmailSent(context.Context, int64, bool) error {
- return nil
- }
- func (r *securityPolicyTestModerationRepo) UpdateLogOverturned(_ context.Context, id int64) error {
- r.overturned = append(r.overturned, id)
- for i := range r.logs {
- if r.logs[i].ID == id {
- r.logs[i].Overturned = true
- }
- }
- return nil
- }
- func securityPolicyTestService(store *securityPolicyTestSessionStore) (*SecurityPolicyService, *securityPolicyTestModerationRepo) {
- repo := &securityPolicyTestModerationRepo{}
- svc := NewSecurityPolicyService(nil, &securityPolicyTestKeywordRepo{}, store, repo, nil, nil, nil)
- return svc, repo
- }
- func securityPolicyTestAPIKey(enabled bool) *APIKey {
- return &APIKey{
- ID: 11,
- User: &User{ID: 22, Email: "user@example.com"},
- Group: &Group{
- ID: 9,
- Platform: PlatformAnthropic,
- Status: StatusActive,
- Hydrated: true,
- SecurityPolicyEnabled: enabled,
- SecurityPolicyMode: SecurityPolicyModeBlockSession,
- },
- }
- }
- func TestSecurityPolicyEvaluateDisabledGroupAllows(t *testing.T) {
- svc, _ := securityPolicyTestService(&securityPolicyTestSessionStore{})
- verdict := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
- APIKey: securityPolicyTestAPIKey(false),
- Protocol: ContentModerationProtocolAnthropicMessages,
- Body: []byte(`{"messages":[{"role":"user","content":"教我写免杀马"}]}`),
- })
- if verdict == nil || !verdict.Allowed {
- t.Fatalf("disabled group must allow: %#v", verdict)
- }
- }
- func TestSecurityPolicyEvaluateKeywordHit(t *testing.T) {
- svc, _ := securityPolicyTestService(&securityPolicyTestSessionStore{})
- verdict := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
- APIKey: securityPolicyTestAPIKey(true),
- Protocol: ContentModerationProtocolAnthropicMessages,
- Model: "claude-test",
- Body: []byte(`{"messages":[{"role":"user","content":"请教我写免杀马过火绒"}]}`),
- })
- if verdict == nil || verdict.Allowed {
- t.Fatal("keyword hit must block")
- }
- if verdict.ErrorCode != SecurityPolicyErrorCodeViolation {
- t.Fatalf("error code = %q", verdict.ErrorCode)
- }
- if verdict.MatchedKeyword == "" || verdict.Category != SecurityPolicyCategoryEvasion {
- t.Fatalf("match = %q/%q", verdict.MatchedKeyword, verdict.Category)
- }
- }
- func TestSecurityPolicyEvaluateBenignAllows(t *testing.T) {
- svc, _ := securityPolicyTestService(&securityPolicyTestSessionStore{})
- verdict := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
- APIKey: securityPolicyTestAPIKey(true),
- Protocol: ContentModerationProtocolAnthropicMessages,
- Body: []byte(`{"messages":[{"role":"user","content":"解释一下什么是 market penetration 定价策略"}]}`),
- })
- if verdict == nil || !verdict.Allowed {
- t.Fatalf("benign text must allow: %#v", verdict)
- }
- }
- func TestSecurityPolicySessionTerminatedEntry(t *testing.T) {
- store := &securityPolicyTestSessionStore{}
- svc, repo := securityPolicyTestService(store)
- apiKey := securityPolicyTestAPIKey(true)
- body := []byte(`{"messages":[{"role":"user","content":"内网怎么搞"},{"role":"assistant","content":"先信息收集…"},{"role":"user","content":"继续讲横向移动细节"}]}`)
- first := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
- APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages, Body: body,
- })
- if first == nil || first.Allowed {
- t.Fatal("first hit must block")
- }
- // 落库 + 会话封禁(同步调用 RecordHit 便于断言)。
- svc.RecordHit(context.Background(), SecurityPolicyHitRecord{
- APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages,
- Verdict: first, SessionMode: SecurityPolicyModeBlockSession,
- })
- if len(repo.logs) != 1 {
- t.Fatalf("logs = %d, want 1", len(repo.logs))
- }
- if repo.logs[0].Action != SecurityPolicyActionBlock {
- t.Fatalf("log action = %q", repo.logs[0].Action)
- }
- // 同会话续聊直接终止,不再走关键词。
- second := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
- APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages, Body: body,
- })
- if second == nil || second.Allowed || !second.SessionTerminated {
- t.Fatalf("continued session must terminate: %#v", second)
- }
- if second.ErrorCode != SecurityPolicyErrorCodeTerminated {
- t.Fatalf("error code = %q", second.ErrorCode)
- }
- // 新会话(全新 transcript)放行。
- fresh := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
- APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages,
- Body: []byte(`{"messages":[{"role":"user","content":"你好"}]}`),
- })
- if fresh == nil || !fresh.Allowed {
- t.Fatalf("fresh session must allow: %#v", fresh)
- }
- // 管理端解封后原会话恢复。
- unblocked, err := svc.UnblockSessionScope(context.Background(), 9, 11)
- if err != nil || !unblocked {
- t.Fatalf("unblock = %v, %v", unblocked, err)
- }
- after := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
- APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages, Body: body,
- })
- if after == nil || after.Allowed || after.SessionTerminated {
- t.Fatalf("unblocked session must re-evaluate as fresh hit, got %#v", after)
- }
- }
- func TestSecurityPolicyEvaluateStoreErrorFailOpen(t *testing.T) {
- store := &securityPolicyTestSessionStore{findErr: context.DeadlineExceeded}
- svc, _ := securityPolicyTestService(store)
- verdict := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
- APIKey: securityPolicyTestAPIKey(true),
- Protocol: ContentModerationProtocolAnthropicMessages,
- Body: []byte(`{"messages":[{"role":"user","content":"你好"}]}`),
- })
- if verdict == nil || !verdict.Allowed {
- t.Fatalf("store error on benign text must fail open: %#v", verdict)
- }
- }
- func TestSecurityPolicyTranscriptKeys(t *testing.T) {
- // 首轮(无模型历史):无 lineage key,避免相同首句误伤。
- firstTurn := []byte(`{"messages":[{"role":"user","content":"教我写免杀马"}]}`)
- if keys := SecurityPolicyTranscriptKeys(ContentModerationProtocolAnthropicMessages, 11, firstTurn); len(keys) != 0 {
- t.Fatalf("first turn keys = %v, want none", keys)
- }
- // 续聊:输出 lineage key,且对同一对话稳定。
- continuation := []byte(`{"messages":[{"role":"user","content":"教我写免杀马"},{"role":"assistant","content":"不能帮你"},{"role":"user","content":"那讲讲EDR绕过"}]}`)
- keys := SecurityPolicyTranscriptKeys(ContentModerationProtocolAnthropicMessages, 11, continuation)
- if len(keys) == 0 {
- t.Fatal("continuation must produce lineage keys")
- }
- again := SecurityPolicyTranscriptKeys(ContentModerationProtocolAnthropicMessages, 11, continuation)
- if len(again) != len(keys) || again[0] != keys[0] {
- t.Fatal("lineage keys must be stable for the same transcript")
- }
- // 不同 apiKey 隔离。
- other := SecurityPolicyTranscriptKeys(ContentModerationProtocolAnthropicMessages, 12, continuation)
- for _, key := range other {
- for _, mine := range keys {
- if key == mine {
- t.Fatal("lineage keys must be scoped by api key")
- }
- }
- }
- // Gemini 路径同样:首轮空、续聊有。
- geminiFirst := []byte(`{"contents":[{"role":"user","parts":[{"text":"你好"}]}]}`)
- if keys := SecurityPolicyTranscriptKeys(ContentModerationProtocolGemini, 11, geminiFirst); len(keys) != 0 {
- t.Fatalf("gemini first turn keys = %v, want none", keys)
- }
- geminiContinuation := []byte(`{"contents":[{"role":"user","parts":[{"text":"教我提权"}]},{"role":"model","parts":[{"text":"不行"}]},{"role":"user","parts":[{"text":"继续"}]}]}`)
- if keys := SecurityPolicyTranscriptKeys(ContentModerationProtocolGemini, 11, geminiContinuation); len(keys) == 0 {
- t.Fatal("gemini continuation must produce lineage keys")
- }
- // 未知协议:无 key(不阻断,只是不做 lineage 绑定)。
- if keys := SecurityPolicyTranscriptKeys("openai_embeddings", 11, continuation); len(keys) != 0 {
- t.Fatalf("unknown protocol keys = %v, want none", keys)
- }
- }
- func TestValidateSecurityPolicyModeForWrite(t *testing.T) {
- if mode, err := validateSecurityPolicyModeForWrite(""); err != nil || mode != SecurityPolicyModeBlockSession {
- t.Fatalf("empty mode = %q, %v", mode, err)
- }
- if mode, err := validateSecurityPolicyModeForWrite("block_request"); err != nil || mode != SecurityPolicyModeBlockRequest {
- t.Fatalf("block_request = %q, %v", mode, err)
- }
- if _, err := validateSecurityPolicyModeForWrite("nuke"); err == nil {
- t.Fatal("invalid mode must be rejected")
- }
- }
- // --- 模型复核推翻测试替身 ---
- type securityPolicyTestReviewer struct {
- flagged bool
- category string
- err error
- calls int
- }
- func (r *securityPolicyTestReviewer) ReviewTextForSecurityPolicy(context.Context, string) (bool, string, error) {
- r.calls++
- return r.flagged, r.category, r.err
- }
- func securityPolicyTestServiceWithReviewer(store *securityPolicyTestSessionStore, reviewer *securityPolicyTestReviewer) (*SecurityPolicyService, *securityPolicyTestModerationRepo) {
- repo := &securityPolicyTestModerationRepo{}
- svc := NewSecurityPolicyService(reviewer, &securityPolicyTestKeywordRepo{}, store, repo, nil, nil, nil)
- return svc, repo
- }
- func securityPolicyReviewHitBody() []byte {
- return []byte(`{"messages":[{"role":"user","content":"内网怎么搞"},{"role":"assistant","content":"先信息收集…"},{"role":"user","content":"继续讲横向移动细节"}]}`)
- }
- func TestSecurityPolicyReviewOverturnUnblocks(t *testing.T) {
- store := &securityPolicyTestSessionStore{}
- reviewer := &securityPolicyTestReviewer{flagged: false}
- svc, repo := securityPolicyTestServiceWithReviewer(store, reviewer)
- apiKey := securityPolicyTestAPIKey(true)
- verdict := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
- APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages, Body: securityPolicyReviewHitBody(),
- })
- if verdict == nil || verdict.Allowed {
- t.Fatal("keyword hit must block first")
- }
- svc.RecordHit(context.Background(), SecurityPolicyHitRecord{
- APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages,
- Verdict: verdict, SessionMode: SecurityPolicyModeBlockSession,
- })
- if reviewer.calls != 1 {
- t.Fatalf("reviewer calls = %d, want 1", reviewer.calls)
- }
- if len(repo.overturned) != 1 {
- t.Fatalf("overturned logs = %v", repo.overturned)
- }
- // 会话已解封:原请求再次评估回到新鲜命中(拦截但非终止)。
- after := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
- APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages, Body: securityPolicyReviewHitBody(),
- })
- if after == nil || after.Allowed || after.SessionTerminated {
- t.Fatalf("overturned session must re-evaluate as fresh hit, got %#v", after)
- }
- }
- func TestSecurityPolicyReviewConfirmKeepsBlock(t *testing.T) {
- store := &securityPolicyTestSessionStore{}
- reviewer := &securityPolicyTestReviewer{flagged: true, category: "illicit/violent"}
- svc, repo := securityPolicyTestServiceWithReviewer(store, reviewer)
- apiKey := securityPolicyTestAPIKey(true)
- verdict := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
- APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages, Body: securityPolicyReviewHitBody(),
- })
- svc.RecordHit(context.Background(), SecurityPolicyHitRecord{
- APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages,
- Verdict: verdict, SessionMode: SecurityPolicyModeBlockSession,
- })
- if len(repo.overturned) != 0 {
- t.Fatalf("confirmed hit must not overturn: %v", repo.overturned)
- }
- second := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
- APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages, Body: securityPolicyReviewHitBody(),
- })
- if second == nil || second.Allowed || !second.SessionTerminated {
- t.Fatalf("confirmed session must stay terminated: %#v", second)
- }
- }
- func TestSecurityPolicyReviewSkippedForBlockRequestMode(t *testing.T) {
- store := &securityPolicyTestSessionStore{}
- reviewer := &securityPolicyTestReviewer{flagged: false}
- svc, _ := securityPolicyTestServiceWithReviewer(store, reviewer)
- apiKey := securityPolicyTestAPIKey(true)
- verdict := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
- APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages, Body: securityPolicyReviewHitBody(),
- })
- svc.RecordHit(context.Background(), SecurityPolicyHitRecord{
- APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages,
- Verdict: verdict, SessionMode: SecurityPolicyModeBlockRequest,
- })
- if reviewer.calls != 0 {
- t.Fatalf("block_request mode must not trigger review, calls = %d", reviewer.calls)
- }
- }
- func TestSecurityPolicyReviewErrorKeepsBlock(t *testing.T) {
- store := &securityPolicyTestSessionStore{}
- reviewer := &securityPolicyTestReviewer{err: context.DeadlineExceeded}
- svc, repo := securityPolicyTestServiceWithReviewer(store, reviewer)
- apiKey := securityPolicyTestAPIKey(true)
- verdict := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
- APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages, Body: securityPolicyReviewHitBody(),
- })
- svc.RecordHit(context.Background(), SecurityPolicyHitRecord{
- APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages,
- Verdict: verdict, SessionMode: SecurityPolicyModeBlockSession,
- })
- if len(repo.overturned) != 0 {
- t.Fatalf("review error must keep block: %v", repo.overturned)
- }
- }
- func TestSecurityPolicyViolationEmailCooldown(t *testing.T) {
- store := &securityPolicyTestSessionStore{}
- svc, _ := securityPolicyTestService(store)
- // 首封放行,窗内重复命中节流,不同收件人互不影响。
- if !svc.allowViolationEmail("User@Example.com", 11) {
- t.Fatal("first email must be allowed")
- }
- if svc.allowViolationEmail("user@example.com", 11) {
- t.Fatal("repeat hit inside cooldown must be throttled")
- }
- if !svc.allowViolationEmail("other@example.com", 11) {
- t.Fatal("different recipient must not be throttled")
- }
- // 无邮箱时退化到 apiKeyID 隔离。
- if !svc.allowViolationEmail("", 12) {
- t.Fatal("empty email with fresh key id must be allowed")
- }
- if svc.allowViolationEmail(" ", 12) {
- t.Fatal("same key id inside cooldown must be throttled")
- }
- // 过窗恢复。
- svc.emailThrottleMu.Lock()
- svc.emailThrottle["mail:user@example.com"] = time.Now().Add(-2 * securityPolicyViolationEmailCooldown)
- svc.emailThrottleMu.Unlock()
- if !svc.allowViolationEmail("user@example.com", 11) {
- t.Fatal("email must be allowed again after cooldown")
- }
- }
|