// auth_test.go:签发/校验(常数时间比对路径)/过期/吊销/配额预扣。 package auth import ( "path/filepath" "testing" "time" _ "modernc.org/sqlite" // sqlite driver(auth 测试库) "onesvm.com/onesvm/browser-server/internal/store" ) func newTestVerifier(t *testing.T) *Verifier { t.Helper() db, err := store.Open(filepath.Join(t.TempDir(), "t.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { db.Close() }) if err := db.Migrate(); err != nil { t.Fatal(err) } return NewVerifier(db, "test-seed-salt-not-a-secret-in-tests") } // TestIssueAndCheck 签发后校验通过;篡改 key 拒绝。 func TestIssueAndCheck(t *testing.T) { v := newTestVerifier(t) cid, _ := v.db.CreateConsumer("c", "") plaintext, keyID, err := v.Issue(cid, "k1", nil, 60, 100, 1000, 2, nil) if err != nil { t.Fatal(err) } if len(plaintext) < len(KeyPrefix)+8 { t.Fatalf("明文 key 形态不符: %s", plaintext) } k, aerr := v.Check(plaintext) if aerr != nil { t.Fatalf("合法 key 应通过: %v", aerr) } if k.ID != keyID { t.Fatalf("key id 不符: %d != %d", k.ID, keyID) } // 篡改一位 bad := plaintext[:len(plaintext)-1] + "A" if _, aerr := v.Check(bad); aerr == nil { t.Fatal("篡改 key 应拒绝") } // 格式错误 if _, aerr := v.Check("Bearer xyz"); aerr == nil { t.Fatal("非法前缀应拒绝") } } // TestRevoke 吊销即时生效。 func TestRevoke(t *testing.T) { v := newTestVerifier(t) cid, _ := v.db.CreateConsumer("c2", "") plaintext, keyID, _ := v.Issue(cid, "k2", nil, 60, 100, 1000, 2, nil) if _, aerr := v.Check(plaintext); aerr != nil { t.Fatalf("吊销前应通过: %v", aerr) } if err := v.db.SetKeyStatus(keyID, "disabled"); err != nil { t.Fatal(err) } if _, aerr := v.Check(plaintext); aerr == nil || aerr.Code != "unauthorized" { t.Fatalf("吊销后应拒绝: %v", aerr) } } // TestExpiry 过期 key 拒绝。 func TestExpiry(t *testing.T) { v := newTestVerifier(t) cid, _ := v.db.CreateConsumer("c3", "") past := time.Now().Add(-time.Hour) plaintext, _, err := v.Issue(cid, "k3", nil, 60, 100, 1000, 2, &past) if err != nil { t.Fatal(err) } if _, aerr := v.Check(plaintext); aerr == nil || aerr.Message != "key 已过期" { t.Fatalf("过期 key 应拒绝: %v", aerr) } } // TestConstantTimePath 假比对覆盖(查无此 key 时也走常数时间路径,不 panic)。 func TestConstantTimePath(t *testing.T) { v := newTestVerifier(t) if _, aerr := v.Check("bs_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); aerr == nil { t.Fatal("未知 key 应拒绝") } } // TestScopes scope 判定。 func TestScopes(t *testing.T) { v := newTestVerifier(t) cid, _ := v.db.CreateConsumer("c4", "") plaintext, _, _ := v.Issue(cid, "k4", []string{"search"}, 60, 100, 1000, 2, nil) k, aerr := v.Check(plaintext) if aerr != nil { t.Fatal(aerr) } if !HasScope(k, "search") || HasScope(k, "screenshot") { t.Fatalf("scope 判定不符: %v", k.Scopes) } } // TestQuotaFlow 预扣→402→结算/释放。 func TestQuotaFlow(t *testing.T) { v := newTestVerifier(t) cid, _ := v.db.CreateConsumer("c5", "") plaintext, keyID, _ := v.Issue(cid, "k5", nil, 60, 2, 100, 2, nil) k, _ := v.Check(plaintext) now := time.Now() if _, aerr := v.ReserveDaily(keyID, k.DailyQuota, now); aerr != nil { t.Fatalf("第 1 次预扣应通过: %v", aerr) } if _, aerr := v.ReserveDaily(keyID, k.DailyQuota, now); aerr != nil { t.Fatalf("第 2 次预扣应通过: %v", aerr) } if _, aerr := v.ReserveDaily(keyID, k.DailyQuota, now); aerr == nil || aerr.Code != "quota" { t.Fatalf("超限应 402: %v", aerr) } // 失败释放后可再扣 if err := v.Release(keyID, now); err != nil { t.Fatal(err) } if _, aerr := v.ReserveDaily(keyID, k.DailyQuota, now); aerr != nil { t.Fatalf("释放后应可扣: %v", aerr) } if err := v.Settle(keyID, now); err != nil { t.Fatal(err) } } // TestFailClosed 空盐必须 panic(fail-closed 纪律)。 func TestFailClosed(t *testing.T) { defer func() { if r := recover(); r == nil { t.Fatal("空盐应 panic(fail-closed)") } }() db, _ := store.Open(filepath.Join(t.TempDir(), "x.db")) defer db.Close() _ = db.Migrate() NewVerifier(db, "") // 应 panic }