package auth import ( "fmt" "strings" "sync" "testing" ) func TestSimpleTokenValidator(t *testing.T) { v := NewSimpleTokenValidator() const first, second = "test-token-123", "test-token-456" isTrue(t, !v.ValidateToken(first), "empty validator rejects") v.AddToken(first) v.AddToken(second) isTrue(t, v.ValidateToken(first), "first token") isTrue(t, v.ValidateToken(second), "second token") isTrue(t, !v.ValidateToken("invalid-token"), "unknown token") // matching is exact isTrue(t, !v.ValidateToken(first+"x"), "suffix") isTrue(t, !v.ValidateToken(first[:len(first)-1]), "prefix") isTrue(t, !v.ValidateToken(strings.ToUpper(first)), "case") isTrue(t, !v.ValidateToken(" "+first), "leading space") v.RemoveToken(first) isTrue(t, !v.ValidateToken(first), "removed token") isTrue(t, v.ValidateToken(second), "surviving token") // removing an absent token is a no-op v.RemoveToken("never-added") eq(t, len(v.tokens), 1, "entry count after no-op removal") // repeated adds are idempotent v.AddToken(second) v.AddToken(second) eq(t, len(v.tokens), 1, "entry count after duplicate adds") // an empty credential must never grant access v.AddToken("") isTrue(t, !v.ValidateToken(""), "empty token rejected") v.RemoveToken("") isTrue(t, !v.ValidateToken(""), "empty token removed") } func TestSimpleTokenValidatorKeying(t *testing.T) { v := NewSimpleTokenValidator() tokens := []string{ "a", "a\x00b", "a\x00c", "🔑", strings.Repeat("a", MaxTokenLen), } for _, tok := range tokens { v.AddToken(tok) } eq(t, len(v.tokens), len(tokens), "distinct entries") for i, tok := range tokens { isTrue(t, v.ValidateToken(tok), fmt.Sprintf("token %d", i)) } for i, tok := range tokens { v.RemoveToken(tok) isTrue(t, !v.ValidateToken(tok), fmt.Sprintf("token %d removed", i)) } eq(t, len(v.tokens), 0, "empty after removal") } func TestSimpleTokenValidatorConcurrent(t *testing.T) { v := NewSimpleTokenValidator() const n = 256 // pre-populate half the space so readers see hits and misses for i := n / 2; i < n; i++ { v.AddToken(fmt.Sprintf("token-%d", i)) } // each goroutine owns one token, so the final state is deterministic var wg sync.WaitGroup for i := range n { wg.Add(1) go func() { defer wg.Done() token := fmt.Sprintf("token-%d", i) v.AddToken(token) v.ValidateToken(token) v.ValidateToken(fmt.Sprintf("absent-%d", i)) if i%2 == 0 { v.RemoveToken(token) } }() } wg.Wait() for i := range n { eq(t, v.ValidateToken(fmt.Sprintf("token-%d", i)), i%2 != 0, fmt.Sprintf("token %d", i)) } eq(t, len(v.tokens), n/2, "surviving entries") } func TestParseBearerToken(t *testing.T) { for _, tc := range []struct { header, token string want error }{ {"Bearer abc.def-_/+~==", "abc.def-_/+~==", nil}, {"bEaReR token", "token", nil}, {"Bearer ", "", ErrAuthEmptyBearerToken}, {"Bearer ", "", ErrAuthEmptyBearerToken}, {"Bearer", "", ErrAuthInvalidBearerFormat}, {"Bearer x y", "", ErrAuthInvalidBearerFormat}, {"Bearer x ", "", ErrAuthInvalidBearerFormat}, {"Bearer x\r\n", "", ErrAuthInvalidBearerFormat}, {"Bearer x\ty", "", ErrAuthInvalidBearerFormat}, {"Bearer =", "", ErrAuthInvalidBearerFormat}, {"Bearer a=b", "", ErrAuthInvalidBearerFormat}, {"Other x", "", ErrAuthInvalidBearerFormat}, {"Bearer " + strings.Repeat("x", MaxTokenLen+1), "", ErrTokenTooLong}, } { token, err := ParseBearerToken(tc.header) if tc.want != nil { errIs(t, err, tc.want, tc.header) } else { noErr(t, err, tc.header) } eq(t, token, tc.token, "token") } } func FuzzParseBearerToken(f *testing.F) { for _, seed := range []string{"Bearer token", "bearer abc+/==", "Bearer x\n", "Bearer a=b", ""} { f.Add(seed) } f.Fuzz(func(t *testing.T, header string) { token, err := ParseBearerToken(header) if err != nil { if token != "" { t.Fatal("token with error") } return } if token == "" || len(token) > MaxTokenLen || strings.ContainsAny(token, " \t\r\n") { t.Fatal("invalid token accepted") } again, err := ParseBearerToken("Bearer " + token) if err != nil || again != token { t.Fatal("noncanonical roundtrip") } }) }