You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

85 lines
3.0 KiB

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, "2222") {
t.Fatal("challenge verified for a different subject")
}
if manager.Verify("192.0.2.10", challenge.ChallengeID, "2222") {
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, "2222") {
t.Fatal("correct answer was rejected")
}
if manager.Verify("peer", challenge.ChallengeID, "2222") {
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, "2222") {
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)
}
}