Files

120 lines
3.6 KiB
Go
Raw Permalink Normal View History

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