package authcaptcha import ( "bytes" "encoding/base64" "strings" "testing" "time" ) func TestChallengeIsPNGSubjectBoundAndOneTime(t *testing.T) { now := time.Date(2026, 8, 15, 0, 0, 0, 0, time.UTC) random := append(bytes.Repeat([]byte{0x23}, 18), bytes.Repeat([]byte{0x00}, answerLength)...) random = append(random, bytes.Repeat([]byte{0x55}, 256)...) manager := New(Options{Clock: func() time.Time { return now }, Random: bytes.NewReader(random)}) challenge, err := manager.Create("192.0.2.10") if err != nil { t.Fatalf("Create() error = %v", err) } if !strings.HasPrefix(challenge.ImageDataURL, "data:image/png;base64,") { t.Fatalf("image data URL = %q", challenge.ImageDataURL[:minInt(len(challenge.ImageDataURL), 40)]) } payload, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(challenge.ImageDataURL, "data:image/png;base64,")) if err != nil || len(payload) < 8 || string(payload[:8]) != "\x89PNG\r\n\x1a\n" { t.Fatalf("captcha image is not a PNG: error=%v bytes=%x", err, payload[:minInt(len(payload), 8)]) } if manager.Verify("198.51.100.5", challenge.ChallengeID, "22222222") { t.Fatal("challenge verified for a different subject") } if manager.Verify("192.0.2.10", challenge.ChallengeID, "22222222") { t.Fatal("challenge was reusable after a failed subject check") } } func TestChallengeCorrectAnswerVerifiesOnceAndExpires(t *testing.T) { now := time.Date(2026, 8, 15, 0, 0, 0, 0, time.UTC) newRandom := func() *bytes.Reader { payload := append(bytes.Repeat([]byte{0x31}, 18), bytes.Repeat([]byte{0x00}, answerLength)...) payload = append(payload, bytes.Repeat([]byte{0x44}, 256)...) return bytes.NewReader(payload) } manager := New(Options{Clock: func() time.Time { return now }, Random: newRandom(), TTL: time.Minute}) challenge, err := manager.Create("peer") if err != nil { t.Fatalf("Create() error = %v", err) } if !manager.Verify("peer", challenge.ChallengeID, "22222222") { t.Fatal("correct answer was rejected") } if manager.Verify("peer", challenge.ChallengeID, "22222222") { t.Fatal("challenge verified twice") } manager = New(Options{Clock: func() time.Time { return now }, Random: newRandom(), TTL: time.Minute}) challenge, err = manager.Create("peer") if err != nil { t.Fatalf("Create() error = %v", err) } now = now.Add(time.Minute) if manager.Verify("peer", challenge.ChallengeID, "22222222") { t.Fatal("expired challenge verified") } } func TestChallengesAreBoundedPerSubject(t *testing.T) { now := time.Date(2026, 8, 15, 0, 0, 0, 0, time.UTC) randomPayload := make([]byte, 5*(18+answerLength+256)) for index := range randomPayload { randomPayload[index] = byte(index) } manager := New(Options{ Clock: func() time.Time { return now }, Random: bytes.NewReader(randomPayload), Capacity: 32, }) for index := 0; index < 5; index++ { now = now.Add(time.Second) if _, err := manager.Create("peer"); err != nil { t.Fatal(err) } } if len(manager.entries) != maxPerSubject { t.Fatalf("subject challenge count=%d, want %d", len(manager.entries), maxPerSubject) } }