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,119 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user