120 lines
3.6 KiB
Go
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)
|
||
|
|
}
|
||
|
|
}
|