Files
story/internal/onboarding/ble_test.go
T
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

120 lines
3.6 KiB
Go

package onboarding
import (
"strings"
"sync"
"testing"
"time"
"data-terminal-cloud/internal/identity"
"github.com/google/uuid"
)
func (f *fixture) bleToken() string {
f.t.Helper()
r, err := f.s.CreateBLEToken(f.ctx, f.user, BLETokenInput{DeviceID: f.d.FactoryID, ElderID: f.elder})
must(f.t, err)
return r["bind_token"].(string)
}
func TestBLEBindingLifecycle(t *testing.T) {
f := newFixture(t)
first := f.bleToken()
token := f.bleToken()
if first == token || !validB64(token, 32) {
t.Fatal("token must be fresh and contain 32 random bytes")
}
r, err := f.s.BLEStatus(f.ctx, f.user, BLEBindInput{Token: first})
must(t, err)
if r["status"] != "failed" {
t.Fatal("replaced token remained valid")
}
_, err = f.s.BindBLE(f.ctx, f.d, uuid.NewString(), BLEBindInput{Token: first})
if err == nil {
t.Fatal("replaced token accepted")
}
r, err = f.s.BLEStatus(f.ctx, f.user, BLEBindInput{Token: token})
must(t, err)
if r["status"] != "pending" {
t.Fatal(r)
}
_, err = f.s.BLEStatus(f.ctx, identity.Principal{Issuer: f.user.Issuer, Subject: uuid.NewString()}, BLEBindInput{Token: token})
if err == nil {
t.Fatal("another account read the binding result")
}
r, err = f.s.BindBLE(f.ctx, f.d, uuid.NewString(), BLEBindInput{Token: token})
must(t, err)
if r["status"] != "success" {
t.Fatal(r)
}
_, err = f.s.pool.Exec(f.ctx, `UPDATE device_ble_binding SET expires_at=now()-interval '1 minute' WHERE token_hash=$1`, digest([]byte(token)))
must(t, err)
// A lost response must be recoverable even after the token's original TTL.
r, err = f.s.BindBLE(f.ctx, f.d, uuid.NewString(), BLEBindInput{Token: token})
must(t, err)
if r["status"] != "success" {
t.Fatal(r)
}
var count int
must(t, f.s.pool.QueryRow(f.ctx, `SELECT count(*) FROM device_binding WHERE device_id=$1::uuid`, f.d.ID).Scan(&count))
if count != 1 {
t.Fatalf("duplicate bindings: %d", count)
}
f.refresh()
if f.d.Generation != 1 {
t.Fatalf("generation %d", f.d.Generation)
}
_, err = f.s.CreateBLEToken(f.ctx, f.user, BLETokenInput{DeviceID: f.d.FactoryID, ElderID: f.elder})
if err == nil {
t.Fatal("already-bound device admitted a new claim")
}
}
func TestBLEBindingRejectsExpiredAndWrongDevice(t *testing.T) {
f, other := newFixture(t), newFixture(t)
token := f.bleToken()
_, err := f.s.BindBLE(f.ctx, other.d, uuid.NewString(), BLEBindInput{Token: token})
if err == nil {
t.Fatal("token accepted by another device")
}
_, err = f.s.CreateBLEToken(f.ctx, f.user, BLETokenInput{DeviceID: f.d.FactoryID, ElderID: other.elder})
if err == nil {
t.Fatal("foreign subject accepted")
}
_, err = f.s.pool.Exec(f.ctx, `UPDATE device_ble_binding SET expires_at=$2 WHERE token_hash=$1`, digest([]byte(token)), time.Now().Add(-time.Minute))
must(t, err)
_, err = f.s.BindBLE(f.ctx, f.d, uuid.NewString(), BLEBindInput{Token: token})
if err == nil {
t.Fatal("expired token bound device")
}
_, err = f.s.BindBLE(f.ctx, f.d, uuid.NewString(), BLEBindInput{Token: strings.Repeat("x", 44)})
if err == nil {
t.Fatal("malformed token accepted")
}
}
func TestBLEConcurrentRetryIsIdempotent(t *testing.T) {
f := newFixture(t)
token := f.bleToken()
var wg sync.WaitGroup
errs := make(chan error, 2)
for i := 0; i < 2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, err := f.s.BindBLE(f.ctx, f.d, uuid.NewString(), BLEBindInput{Token: token})
errs <- err
}()
}
wg.Wait()
close(errs)
for err := range errs {
must(t, err)
}
var count int
must(t, f.s.pool.QueryRow(f.ctx, `SELECT count(*) FROM device_binding WHERE device_id=$1::uuid`, f.d.ID).Scan(&count))
if count != 1 {
t.Fatalf("concurrent bind created %d bindings", count)
}
}