package captcha import ( "crypto/sha256" "encoding/hex" "strconv" "testing" ) // solve reproduces what the widget does, using the package's own prng, so the // test proves CreateChallenge and Redeem agree end-to-end. func solve(token string, ch Challenge) []string { sols := make([]string, ch.C) for i := 1; i <= ch.C; i++ { salt := prng(token+strconv.Itoa(i), ch.S) target := prng(token+strconv.Itoa(i)+"d", ch.D) for n := 0; ; n++ { sum := sha256.Sum256([]byte(salt + strconv.Itoa(n))) if has := hex.EncodeToString(sum[:]); len(has) >= len(target) && has[:len(target)] == target { sols[i-1] = strconv.Itoa(n) break } } } return sols } func TestChallengeRedeemRoundTrip(t *testing.T) { // Small difficulty keeps the test fast. s := New([]byte("secret"), WithChallenge(5, 8, 2)) cr, err := s.CreateChallenge() if err != nil { t.Fatal(err) } sols := solve(cr.Token, cr.Challenge) res := s.Redeem(cr.Token, sols) if !res.Success { t.Fatal("valid solutions rejected") } if !s.VerifyToken(res.Token) { t.Fatal("issued verification token failed VerifyToken") } } func TestVerifyTokenSingleUse(t *testing.T) { s := New([]byte("secret"), WithChallenge(5, 8, 2)) cr, _ := s.CreateChallenge() res := s.Redeem(cr.Token, solve(cr.Token, cr.Challenge)) if !res.Success { t.Fatal("valid redeem rejected") } // First verification succeeds. if !s.VerifyToken(res.Token) { t.Fatal("first VerifyToken should succeed") } // Replaying the same token within its TTL must fail — single-use. if s.VerifyToken(res.Token) { t.Error("replayed verification token accepted (should be single-use)") } // A third replay stays rejected. if s.VerifyToken(res.Token) { t.Error("second replay of verification token accepted") } // A forged/garbage token never consumes and never verifies. if s.VerifyToken("not-a-real-token") { t.Error("garbage token accepted") } // An independently issued token is unaffected by another token's burn. cr2, _ := s.CreateChallenge() res2 := s.Redeem(cr2.Token, solve(cr2.Token, cr2.Challenge)) if !res2.Success { t.Fatal("second redeem rejected") } if !s.VerifyToken(res2.Token) { t.Error("fresh independent token rejected after another token was burned") } } func TestRedeemRejects(t *testing.T) { s := New([]byte("secret"), WithChallenge(5, 8, 2)) cr, _ := s.CreateChallenge() sols := solve(cr.Token, cr.Challenge) // wrong solution bad := append([]string(nil), sols...) bad[0] = "999999999" if s.Redeem(cr.Token, bad).Success { t.Error("wrong solution accepted") } // wrong count — use a fresh challenge so this exercises the count check // itself, not the consumed-nonce path from the wrong-solution redeem above. crc, _ := s.CreateChallenge() solsc := solve(crc.Token, crc.Challenge) if s.Redeem(crc.Token, solsc[:len(solsc)-1]).Success { t.Error("short solution list accepted") } // replay: first redeem consumes the nonce cr2, _ := s.CreateChallenge() sols2 := solve(cr2.Token, cr2.Challenge) if !s.Redeem(cr2.Token, sols2).Success { t.Fatal("first redeem should succeed") } if s.Redeem(cr2.Token, sols2).Success { t.Error("replayed redeem accepted") } }