package onboarding import ( "bytes" "context" "data-terminal-cloud/internal/identity" "encoding/json" "github.com/google/uuid" "net/http/httptest" "strings" "testing" "time" ) type fakeCaptchaProvider struct { calls int client, ip string err error } func (p *fakeCaptchaProvider) AppID(client string) string { if client == "mini_program" { return "456" } return "123" } func (p *fakeCaptchaProvider) Verify(_ context.Context, client, ticket, randstr, ip string) error { p.calls++ p.client = client p.ip = ip return p.err } func setCaptchaTestPolicy(t *testing.T, f *fixture, saas, mini string) { t.Helper() _, err := f.s.pool.Exec(f.ctx, `UPDATE login_captcha_policy SET saas_mode=$1,mini_mode=$2,version=version+1`, saas, mini) must(t, err) t.Cleanup(func() { _, e := f.s.pool.Exec(f.ctx, `UPDATE login_captcha_policy SET saas_mode='image',mini_mode='image',version=version+1`) if e != nil { t.Error(e) } }) } func TestCaptchaPolicyTencentSendCannotBypassOrReplay(t *testing.T) { f := newLoginFixture(t) p := &fakeCaptchaProvider{} f.s.options.Captcha = p setCaptchaTestPolicy(t, f.fixture, "tencent", "image") w, ch := f.request("POST", "/api/v1/web/login/captcha", struct{}{}, f.cookie, f.csrf, "") if w.Code != 200 || ch["mode"] != "tencent" || ch["captcha_app_id"] != "123" || ch["image"] != nil { t.Fatal(w.Body.String()) } in := SMSSendInput{Phone: f.phone, Purpose: "LOGIN", CaptchaID: ch["captcha_id"].(string), CaptchaCode: "654321"} w, _ = f.request("POST", "/api/v1/web/login/sms/send", in, f.cookie, f.csrf, "") if w.Code != 401 || p.calls != 0 || f.sender.calls != 0 { t.Fatal("numeric answer bypassed tencent") } _, ch = f.request("POST", "/api/v1/web/login/captcha", struct{}{}, f.cookie, f.csrf, "") in.CaptchaID = ch["captcha_id"].(string) in.CaptchaTicket = uuid.NewString() in.CaptchaRandstr = "random" in.CaptchaCode = "" req := uuid.NewString() w, _ = f.request("POST", "/api/v1/web/login/sms/send", in, f.cookie, f.csrf, req) if w.Code != 200 || p.calls != 1 || p.client != "saas" || p.ip != strings.Split(f.ip, ":")[0] || f.sender.calls != 1 { t.Fatal(w.Body.String()) } w, _ = f.request("POST", "/api/v1/web/login/sms/send", in, f.cookie, f.csrf, req) if w.Code != 200 || p.calls != 1 || f.sender.calls != 1 { t.Fatal("idempotency failed") } _, ch = f.request("POST", "/api/v1/web/login/captcha", struct{}{}, f.cookie, f.csrf, "") in.CaptchaID = ch["captcha_id"].(string) w, _ = f.request("POST", "/api/v1/web/login/sms/send", in, f.cookie, f.csrf, "") if w.Code != 401 || p.calls != 1 || f.sender.calls != 1 { t.Fatal("ticket replay accepted") } } func TestCaptchaPolicyClientSeparationExpiryAndCloudFailure(t *testing.T) { f := newLoginFixture(t) p := &fakeCaptchaProvider{} f.s.options.Captcha = p setCaptchaTestPolicy(t, f.fixture, "image", "tencent") webctx := context.WithValue(f.ctx, captchaRequestKey{}, captchaRequestInfo{client: "saas", ip: "203.0.113.1"}) mini := context.WithValue(f.ctx, captchaRequestKey{}, captchaRequestInfo{client: "mini_program", ip: "203.0.113.2"}) hash := digest([]byte(f.cookie.Value)) ch, err := f.s.loginCaptcha(mini, hash) must(t, err) if ch["captcha_app_id"] != "456" { t.Fatal("mixed app ids") } if f.s.verifyLoginCaptcha(webctx, hash, ch["captcha_id"].(string), "", uuid.NewString(), "") == nil { t.Fatal("cross-client bypass") } ch, err = f.s.loginCaptcha(mini, hash) must(t, err) _, err = f.s.redis.Expire(f.ctx, f.s.captchaKey(hash), -time.Second).Result() must(t, err) if f.s.verifyLoginCaptcha(mini, hash, ch["captcha_id"].(string), "", uuid.NewString(), "") == nil { t.Fatal("expired accepted") } ch, err = f.s.loginCaptcha(mini, hash) must(t, err) p.err = captchaUnavailable() if f.s.verifyLoginCaptcha(mini, hash, ch["captcha_id"].(string), "", uuid.NewString(), "") == nil || p.client != "mini_program" { t.Fatal("cloud failure accepted") } ch, err = f.s.loginCaptcha(mini, hash) must(t, err) p.err = nil _, err = f.s.pool.Exec(f.ctx, `UPDATE login_captcha_policy SET version=version+1`) must(t, err) if f.s.verifyLoginCaptcha(mini, hash, ch["captcha_id"].(string), "", uuid.NewString(), "") == nil { t.Fatal("old policy accepted") } } type captchaPolicyToken struct{ p identity.Principal } func (v captchaPolicyToken) Verify(context.Context, string) (identity.Principal, error) { return v.p, nil } func TestCaptchaPolicyAdminOnlyReadinessVersionAndAudit(t *testing.T) { f := newFixture(t) setCaptchaTestPolicy(t, f, "image", "image") f.s.options.TokenVerifier = captchaPolicyToken{f.p} call := func(method string, body any) (any, error) { raw, _ := json.Marshal(body) r := httptest.NewRequest(method, "https://platform.example/api/v1/ops/login-captcha-policy", bytes.NewReader(raw)) r.Header.Set("Authorization", "Bearer test") r.Header.Set("Content-Type", "application/json") r.Header.Set("X-Tenant-ID", f.tenantPublic) return f.s.captchaPolicyHTTP(r, uuid.NewString()) } out, err := call("GET", nil) must(t, err) policy := out.(map[string]any) in := captchaPolicy{SaaS: "tencent", Mini: "image", Version: policy["version"].(int64)} if _, err = call("POST", in); err == nil { t.Fatal("unconfigured provider accepted") } f.s.options.Captcha = &fakeCaptchaProvider{} out, err = call("POST", in) must(t, err) if out.(map[string]any)["saas_mode"] != "tencent" { t.Fatal("not persisted") } if _, err = call("POST", in); err == nil { t.Fatal("stale version accepted") } var count int must(t, f.s.pool.QueryRow(f.ctx, `SELECT count(*) FROM audit_event WHERE tenant_id=$1 AND action='login.captcha.policy.update'`, f.tenant).Scan(&count)) if count != 1 { t.Fatal("missing audit") } _, err = f.s.pool.Exec(f.ctx, `UPDATE user_account SET platform_role='ops_operator' WHERE id=$1`, f.operatorID) must(t, err) if _, err = call("GET", nil); err == nil { t.Fatal("nonadmin read") } if _, err = call("POST", captchaPolicy{SaaS: "image", Mini: "image", Version: in.Version + 1}); err == nil { t.Fatal("nonadmin write") } }