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") } }