feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
ci / verify (push) Canceled after 0s
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.
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user