feat: expand companion platform, device controls, and memoir workflows
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:
Codex Local
2026-10-08 10:06:44 +08:00
parent 98fb2965d0
commit bf1233d5df
861 changed files with 94614 additions and 3209 deletions
+131
View File
@@ -0,0 +1,131 @@
package onboarding
import (
"bytes"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"data-terminal-cloud/internal/identity"
"github.com/google/uuid"
)
func TestBLEDeviceErrorsDescribeActualCondition(t *testing.T) {
cases := []struct {
name, sql, code, message string
status int
}{
{"not_released", `UPDATE device SET onboarding_released_at=NULL WHERE id=$1::uuid`, "DEVICE_NOT_RELEASED", "尚未放行", 409},
{"quarantined", `UPDATE device SET status='quarantined' WHERE id=$1::uuid`, "DEVICE_QUARANTINED", "已被隔离", 403},
{"revoked", `UPDATE device SET status='revoked' WHERE id=$1::uuid`, "DEVICE_REVOKED", "已被停用", 403},
{"lifecycle_revoked", `UPDATE device SET activation_state='REVOKED' WHERE id=$1::uuid`, "DEVICE_REVOKED", "已被停用", 403},
{"lost", `UPDATE device SET activation_state='LOST' WHERE id=$1::uuid`, "DEVICE_LOST", "标记为丢失", 403},
{"tenant_inactive", `UPDATE tenant SET status='suspended' WHERE id=(SELECT tenant_id FROM device WHERE id=$1::uuid)`, "TENANT_INACTIVE", "组织已停用", 403},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
f := newFixture(t)
_, err := f.s.pool.Exec(f.ctx, tc.sql, f.d.ID)
must(t, err)
assertBLEAccessError(t, f, f.user, f.d.FactoryID, tc.status, tc.code, tc.message)
})
}
t.Run("malformed_and_unknown", func(t *testing.T) {
f := newFixture(t)
assertBLEAccessError(t, f, f.user, "bad-id", 400, "DEVICE_ID_INVALID", "编号格式不正确")
assertBLEAccessError(t, f, f.user, "KP01-"+strings.ToUpper(strings.ReplaceAll(uuid.NewString(), "-", "")[:12]), 404, "DEVICE_NOT_REGISTERED", "未找到该设备的注册记录")
})
t.Run("account_reasons", func(t *testing.T) {
f := newFixture(t)
assertBLEAccessError(t, f, identity.Principal{Issuer: f.user.Issuer, Subject: uuid.NewString()}, f.d.FactoryID, 403, "ACCOUNT_NOT_FOUND", "没有对应的平台账号")
assertBLEAccessError(t, f, f.p, f.d.FactoryID, 403, "CUSTOMER_REQUIRED", "切换到家庭账号")
foreign := newFixture(t)
assertBLEAccessError(t, f, foreign.user, f.d.FactoryID, 403, "DEVICE_TENANT_MISMATCH", "不属于当前账号所在组织")
for _, state := range []string{"disabled", "deleted"} {
_, err := f.s.pool.Exec(f.ctx, `UPDATE user_account SET status=$1 WHERE oidc_issuer=$2 AND oidc_subject=$3`, state, f.user.Issuer, f.user.Subject)
must(t, err)
assertBLEAccessError(t, f, f.user, f.d.FactoryID, 403, "ACCOUNT_"+strings.ToUpper(state), "当前账号已")
}
})
t.Run("legacy_device", func(t *testing.T) {
f := newFixture(t)
factoryID := "KP01-" + strings.ToUpper(strings.ReplaceAll(uuid.NewString(), "-", "")[:12])
_, err := f.s.pool.Exec(f.ctx, `INSERT INTO device(tenant_id,serial_digest,model,credential_ref,factory_device_id) VALUES($1::uuid,$2,'legacy','test',$3)`, f.tenant, digest([]byte(factoryID)), factoryID)
must(t, err)
assertBLEAccessError(t, f, f.user, factoryID, 409, "DEVICE_ENROLLMENT_UNSUPPORTED", "尚未启用蓝牙实体确认绑定")
})
t.Run("other_account_and_stale_ownership", func(t *testing.T) {
f := newFixture(t)
p := accountOnlyUser(t, f)
token := accountToken(t, f, p)
_, err := f.s.BindBLE(f.ctx, f.d, uuid.NewString(), BLEBindInput{Token: token})
must(t, err)
assertBLEAccessError(t, f, f.user, f.d.FactoryID, 403, "DEVICE_OWNED_BY_OTHER_ACCOUNT", "已添加到其他账号")
state, err := f.s.BLEDeviceState(f.ctx, p, BLEDeviceInput{DeviceID: f.d.FactoryID})
must(t, err)
if state["bound"] != true {
t.Fatal(state)
}
_, err = f.s.CreateBLEToken(f.ctx, p, BLETokenInput{DeviceID: f.d.FactoryID})
assertBLEError(t, err, 409, "ALREADY_BOUND", "无需重复添加")
_, err = f.s.pool.Exec(f.ctx, `UPDATE device SET binding_generation=binding_generation+1 WHERE id=$1::uuid`, f.d.ID)
must(t, err)
assertBLEAccessError(t, f, p, f.d.FactoryID, 409, "DEVICE_BINDING_STALE", "归属记录与当前设备状态不一致")
})
t.Run("family_management_permission", func(t *testing.T) {
f := newFixture(t)
f.bound()
p := accountOnlyUser(t, f)
assertBLEAccessError(t, f, p, f.d.FactoryID, 403, "DEVICE_MANAGE_PERMISSION_REQUIRED", "没有该家庭的设备管理权限")
_, err := f.s.pool.Exec(f.ctx, `UPDATE household_member SET role='viewer' WHERE user_id=(SELECT id FROM user_account WHERE tenant_id=$1::uuid AND oidc_issuer=$2 AND oidc_subject=$3)`, f.tenant, f.user.Issuer, f.user.Subject)
must(t, err)
assertBLEAccessError(t, f, f.user, f.d.FactoryID, 403, "DEVICE_MANAGE_PERMISSION_REQUIRED", "没有该家庭的设备管理权限")
})
}
func assertBLEError(t *testing.T, err error, status int, code, message string) {
t.Helper()
var e *Error
if !errors.As(err, &e) || e.Status != status || e.Code != code || !strings.Contains(e.Message, message) {
t.Fatalf("want %d %s containing %q, got %#v", status, code, message, err)
}
}
func assertBLEAccessError(t *testing.T, f *fixture, p identity.Principal, factoryID string, status int, code, message string) {
t.Helper()
_, err := f.s.BLEDeviceState(f.ctx, p, BLEDeviceInput{DeviceID: factoryID})
assertBLEError(t, err, status, code, message)
_, err = f.s.CreateBLEToken(f.ctx, p, BLETokenInput{DeviceID: factoryID})
assertBLEError(t, err, status, code, message)
}
func TestMiniBLEDeviceErrorEnvelope(t *testing.T) {
f := newFixture(t)
session := randomToken(32)
_, err := f.s.pool.Exec(f.ctx, `INSERT INTO onboarding_account_session(token_hash,csrf_hash,user_id,expires_at) SELECT $1,$2,id,now()+interval '1 hour' FROM user_account WHERE tenant_id=$3::uuid AND oidc_issuer=$4 AND oidc_subject=$5`, digest([]byte(session)), digest([]byte("test-csrf")), f.tenant, f.user.Issuer, f.user.Subject)
must(t, err)
_, err = f.s.pool.Exec(f.ctx, `UPDATE device SET onboarding_released_at=NULL WHERE id=$1::uuid`, f.d.ID)
must(t, err)
mux := http.NewServeMux()
f.s.RegisterRoutes(mux)
for _, endpoint := range []string{"ble-device-state", "ble-bind-tokens"} {
requestID := uuid.NewString()
r := httptest.NewRequest("POST", "https://platform.example/api/v1/mini/"+endpoint, bytes.NewReader(requestJSON(BLEDeviceInput{DeviceID: f.d.FactoryID})))
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Authorization", "Mini customer."+session)
r.Header.Set("X-Request-ID", requestID)
w := httptest.NewRecorder()
mux.ServeHTTP(w, r)
var body map[string]any
must(t, json.Unmarshal(w.Body.Bytes(), &body))
if w.Code != 409 || body["code"] != "DEVICE_NOT_RELEASED" || body["message"] != "设备已登记但尚未放行,请联系管理员完成出厂放行" || body["request_id"] != requestID || body["ok"] != false {
t.Fatal(w.Code, body)
}
if _, leaked := body["data"]; leaked {
t.Fatal("error included device data", body)
}
}
}