Files

132 lines
6.7 KiB
Go
Raw Permalink Normal View History

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