Files
story/internal/onboarding/local_image_captcha_test.go
T

96 lines
3.7 KiB
Go
Raw Normal View History

package onboarding
import (
"bytes"
"encoding/base64"
"encoding/json"
"image/png"
"strings"
"testing"
"github.com/google/uuid"
)
// Generate the real PNG and JSON record, then replace only the answer digest in
// disposable test Redis. This makes verification deterministic without solving
// a live CAPTCHA or adding an HTTP bypass.
func (f *loginFixture) knownGeneratedImageCaptcha() string {
f.t.Helper()
w, challenge := f.request("POST", "/api/v1/web/login/captcha", struct{}{}, f.cookie, f.csrf, "")
if w.Code != 200 || challenge["mode"] != "image" || challenge["captcha_app_id"] != nil {
f.t.Fatal("local image challenge unavailable")
}
encoded, ok := challenge["image"].(string)
if !ok || !strings.HasPrefix(encoded, "data:image/png;base64,") {
f.t.Fatal("local image payload missing")
}
raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(encoded, "data:image/png;base64,"))
must(f.t, err)
image, err := png.Decode(bytes.NewReader(raw))
must(f.t, err)
if image.Bounds().Dx() != 240 || image.Bounds().Dy() != 80 || strings.Contains(w.Body.String(), "answer_hash") {
f.t.Fatal("image dimensions or response confidentiality changed")
}
hash := digest([]byte(f.cookie.Value))
key := f.s.captchaKey(hash)
value, err := f.s.redis.Get(f.ctx, key).Result()
must(f.t, err)
var stored storedCaptcha
must(f.t, json.Unmarshal([]byte(value), &stored))
if stored.ID != challenge["captcha_id"] || stored.Mode != "image" || stored.Client != "saas" || stored.Version < 1 || stored.AnswerHash == "" {
f.t.Fatal("generated JSON challenge metadata missing")
}
ttl, err := f.s.redis.PTTL(f.ctx, key).Result()
must(f.t, err)
if ttl <= 0 {
f.t.Fatal("generated challenge expiry missing")
}
stored.AnswerHash = domainMAC(f.s.crypt.hashKey, "CAPTCHA1\n"+hash+"\n"+stored.ID+"\n654321")
must(f.t, f.s.redis.Set(f.ctx, key, captchaJSON(stored), ttl).Err())
return stored.ID
}
func TestLocalImageCaptchaJSONSuccessAndOneTime(t *testing.T) {
f := newLoginFixture(t)
setCaptchaTestPolicy(t, f.fixture, "image", "image")
f.s.options.Captcha = nil
id := f.knownGeneratedImageCaptcha()
hash := digest([]byte(f.cookie.Value))
must(t, f.s.verifyLoginCaptcha(f.ctx, hash, id, "654321"))
if f.s.verifyLoginCaptcha(f.ctx, hash, id, "654321") == nil {
t.Fatal("generated image challenge replay accepted")
}
}
func TestLocalImageCaptchaJSONPolicyChangeInvalidates(t *testing.T) {
f := newLoginFixture(t)
setCaptchaTestPolicy(t, f.fixture, "image", "image")
id := f.knownGeneratedImageCaptcha()
setCaptchaTestPolicy(t, f.fixture, "tencent", "image")
if f.s.verifyLoginCaptcha(f.ctx, digest([]byte(f.cookie.Value)), id, "654321") == nil {
t.Fatal("old image JSON challenge survived a policy change")
}
}
func TestLocalImageCaptchaJSONSMSDeliveryDoesNotUseTencent(t *testing.T) {
f := newLoginFixture(t)
setCaptchaTestPolicy(t, f.fixture, "image", "image")
provider := &fakeCaptchaProvider{}
f.s.options.Captcha = provider
id := f.knownGeneratedImageCaptcha()
input := SMSSendInput{Phone: f.phone, Purpose: "LOGIN", CaptchaID: id, CaptchaCode: "654321"}
request := uuid.NewString()
w, _ := f.request("POST", "/api/v1/web/login/sms/send", input, f.cookie, f.csrf, request)
if w.Code != 200 || f.sender.calls != 1 || provider.calls != 0 {
t.Fatal("local JSON image verification did not deliver one test SMS independently")
}
w, _ = f.request("POST", "/api/v1/web/login/sms/send", input, f.cookie, f.csrf, request)
if w.Code != 200 || f.sender.calls != 1 || provider.calls != 0 {
t.Fatal("exact SMS retry changed delivery or used Tencent CAPTCHA")
}
w, _ = f.request("POST", "/api/v1/web/login/sms/send", input, f.cookie, f.csrf, "")
if w.Code != 401 || f.sender.calls != 1 || provider.calls != 0 {
t.Fatal("generated image challenge replay caused another delivery")
}
}