Files
story/internal/onboarding/captcha_policy_test.go
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
2026-10-08 10:06:44 +08:00

165 lines
5.9 KiB
Go

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