Files
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

222 lines
8.1 KiB
Go

package onboarding
import (
"context"
"encoding/json"
"github.com/google/uuid"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
type fakeWeChat struct{ openID, phone string }
func (f *fakeWeChat) OpenID(context.Context, string) (string, error) { return f.openID, nil }
func (f *fakeWeChat) Phone(context.Context, string) (string, error) { return f.phone, nil }
func TestWeChatProviderExchangeCacheRefreshAndErrors(t *testing.T) {
tokens, phones := 0, 0
mode := "ok"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/sns/jscode2session":
if r.URL.Query().Get("js_code") != "login" || r.URL.Query().Get("appid") != "wx-test" {
t.Error("bad login request")
}
if mode == "badlogin" {
json.NewEncoder(w).Encode(map[string]any{"errcode": 40029, "errmsg": "secret-value"})
return
}
json.NewEncoder(w).Encode(map[string]string{"openid": "openid-test"})
case "/cgi-bin/stable_token":
tokens++
var body map[string]any
json.NewDecoder(r.Body).Decode(&body)
if r.Method != "POST" || body["secret"] != "secret-value" {
t.Error("bad token request")
}
json.NewEncoder(w).Encode(map[string]any{"access_token": "token-" + strings.Repeat("x", tokens), "expires_in": 7200})
case "/wxa/business/getuserphonenumber":
phones++
var body map[string]string
json.NewDecoder(r.Body).Decode(&body)
if body["code"] != "phone-code" || r.URL.Query().Get("access_token") == "" {
t.Error("bad phone request")
}
if mode == "refresh" && phones == 3 {
json.NewEncoder(w).Encode(map[string]int{"errcode": 40014})
return
}
if mode == "denied" {
json.NewEncoder(w).Encode(map[string]int{"errcode": 48001})
return
}
appID := "wx-test"
if mode == "watermark" {
appID = "other-app"
}
json.NewEncoder(w).Encode(map[string]any{"phone_info": map[string]any{"purePhoneNumber": "13800000000", "countryCode": "86", "watermark": map[string]string{"appid": appID}}})
default:
t.Error("unexpected endpoint")
w.WriteHeader(404)
}
}))
defer server.Close()
c := newWeChatClient("wx-test", "secret-value")
c.base = server.URL
id, err := c.OpenID(context.Background(), "login")
must(t, err)
if id != "openid-test" {
t.Fatal(id)
}
for i := 0; i < 2; i++ {
phone, err := c.Phone(context.Background(), "phone-code")
must(t, err)
if phone != "+8613800000000" {
t.Fatal(phone)
}
}
if tokens != 1 {
t.Fatal("token not cached", tokens)
}
mode = "refresh"
_, err = c.Phone(context.Background(), "phone-code")
must(t, err)
if tokens != 2 || phones != 4 {
t.Fatal("refresh retry", tokens, phones)
}
for _, m := range []string{"denied", "watermark", "badlogin"} {
mode = m
if m == "badlogin" {
_, err = c.OpenID(context.Background(), "login")
} else {
_, err = c.Phone(context.Background(), "phone-code")
}
if err == nil || strings.Contains(err.Error(), "secret-value") {
t.Fatal("invalid provider response accepted or secret leaked", m)
}
}
server.Close()
_, err = c.OpenID(context.Background(), "login")
if err == nil || strings.Contains(err.Error(), "secret-value") {
t.Fatal("network error leaked credentials")
}
}
func TestWeChatLoginPreservesAccountAndRejectsConflicts(t *testing.T) {
f := newLoginFixture(t)
account, csrf := f.smsAccount()
original, err := f.s.AccountPrincipal(f.ctx, account.Value, csrf, true)
must(t, err)
provider := &fakeWeChat{openID: uuid.NewString(), phone: f.phone}
f.s.options.WeChat = provider
f.s.options.WeChatAppID = "wx4553119e05f11660"
login := func(portal string) string {
s, d := miniRequest(t, f.s, "POST", "/api/v1/mini/login/session", "", map[string]string{"portal": portal})
if s != 200 {
t.Fatal(s)
}
return "MiniLogin " + d["login_token"].(string)
}
auth := login("customer")
status, data := miniRequest(t, f.s, "POST", "/api/v1/mini/login/wechat", auth, WeChatLoginInput{LoginCode: "first"})
if status != 200 || data["phone_required"] != true || data["access_token"] != nil {
t.Fatal("first login bypassed phone", status)
}
status, data = miniRequest(t, f.s, "POST", "/api/v1/mini/login/wechat", auth, WeChatLoginInput{LoginCode: "second", PhoneCode: "phone"})
if status != 200 {
t.Fatal(status)
}
check := func(data map[string]any) {
token := strings.TrimPrefix(data["access_token"].(string), "customer.")
p, err := f.s.AccountPrincipal(f.ctx, token, "", false)
must(t, err)
if p.Issuer != original.Issuer || p.Subject != original.Subject {
t.Fatal("account changed")
}
var verified bool
must(t, f.s.pool.QueryRow(f.ctx, `SELECT phone_verified_at IS NOT NULL FROM onboarding_account_session WHERE token_hash=$1`, digest([]byte(token))).Scan(&verified))
if verified {
t.Fatal("login granted password reset")
}
}
check(data)
status, _ = miniRequest(t, f.s, "POST", "/api/v1/mini/login/wechat", auth, WeChatLoginInput{LoginCode: "replay"})
if status != 401 {
t.Fatal("session replay", status)
}
status, data = miniRequest(t, f.s, "POST", "/api/v1/mini/login/wechat", login("customer"), WeChatLoginInput{LoginCode: "next"})
if status != 200 {
t.Fatal(status)
}
check(data)
status, _ = miniRequest(t, f.s, "POST", "/api/v1/mini/login/wechat", login("operator"), WeChatLoginInput{LoginCode: "operator"})
if status != 403 {
t.Fatal("operator accepted", status)
}
provider.openID = uuid.NewString()
status, _ = miniRequest(t, f.s, "POST", "/api/v1/mini/login/wechat", login("customer"), WeChatLoginInput{LoginCode: "other", PhoneCode: "phone"})
if status != 409 {
t.Fatal("phone conflict accepted", status)
}
provider.openID = uuid.NewString()
f.s.options.SelfSignupTenantIDs = nil
provider.phone = "+8613900000001"
status, _ = miniRequest(t, f.s, "POST", "/api/v1/mini/login/wechat", login("customer"), WeChatLoginInput{LoginCode: "new", PhoneCode: "phone"})
if status != 403 {
t.Fatal("unconfigured signup accepted", status)
}
}
func TestWeChatPasswordLinkAtomicAndLaterQuickLogin(t *testing.T) {
f := newLoginFixture(t)
account, csrf := f.smsAccount()
w, _ := f.request("POST", "/api/v1/web/account/password", SetPasswordInput{Password: "LocalOnly123!"}, account, csrf, "")
if w.Code != 200 {
t.Fatal(w.Code)
}
provider := &fakeWeChat{openID: uuid.NewString(), phone: f.phone}
f.s.options.WeChat = provider
f.s.options.WeChatAppID = "wx4553119e05f11660"
start := func() (string, string) {
status, data := miniRequest(t, f.s, "POST", "/api/v1/mini/login/session", "", map[string]string{"portal": "customer"})
if status != 200 {
t.Fatal(status)
}
auth := "MiniLogin " + data["login_token"].(string)
_, token, _ := miniCredential(auth, "MiniLogin")
return auth, digest([]byte(token))
}
auth, hash := start()
in := WeChatPasswordInput{PasswordLoginInput: PasswordLoginInput{Phone: f.phone, Password: "Wrong123!", CaptchaID: f.knownCaptchaFor(hash), CaptchaCode: "654321", TrustedDeviceToken: f.trusted.Value}, LoginCode: "link"}
status, _ := miniRequest(t, f.s, "POST", "/api/v1/mini/login/wechat-link", auth, in)
if status != 401 {
t.Fatal(status)
}
var count int
must(t, f.s.pool.QueryRow(f.ctx, `SELECT count(*) FROM customer_wechat_identity WHERE open_id=$1`, provider.openID).Scan(&count))
if count != 0 {
t.Fatal("wrong password linked identity")
}
in.Password = "LocalOnly123!"
in.CaptchaID = f.knownCaptchaFor(hash)
status, data := miniRequest(t, f.s, "POST", "/api/v1/mini/login/wechat-link", auth, in)
if status != 200 || data["access_token"] == nil {
t.Fatal("password link failed", status)
}
auth, _ = start()
status, data = miniRequest(t, f.s, "POST", "/api/v1/mini/login/wechat", auth, WeChatLoginInput{LoginCode: "later"})
if status != 200 || data["access_token"] == nil {
t.Fatal("quick login failed", status)
}
_, err := f.s.pool.Exec(f.ctx, `UPDATE user_account SET status='disabled' WHERE oidc_issuer='urn:platform:onboarding:sms' AND oidc_subject=$1`, domainMAC(f.s.crypt.hashKey, "PHONE1\n"+f.phone))
must(t, err)
auth, _ = start()
status, _ = miniRequest(t, f.s, "POST", "/api/v1/mini/login/wechat", auth, WeChatLoginInput{LoginCode: "disabled"})
if status != 403 {
t.Fatal("disabled user accepted", status)
}
}