internal/physical/registration_test.go
raw ยท 5618 bytes
package physical
import (
"context"
"crypto/ecdh"
"crypto/rand"
"encoding/json"
"errors"
"os"
"path/filepath"
"sync"
"sync/atomic"
"testing"
"time"
"homekey.local/provisioner/drivers/sesame"
)
type registrationLink struct {
packets chan []byte
done chan error
mu sync.Mutex
received []byte
public []byte
writes atomic.Int32
closed atomic.Bool
}
func newRegistrationLink(t *testing.T) *registrationLink {
t.Helper()
key, err := ecdh.P256().GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
l := ®istrationLink{packets: make(chan []byte, 16), done: make(chan error, 1), public: key.PublicKey().Bytes()[1:]}
l.packets <- []byte{3, 8, 14, 0x11, 0x22, 0x33, 0x44}
return l
}
func (l *registrationLink) Packets() <-chan []byte { return l.packets }
func (l *registrationLink) Done() <-chan error { return l.done }
func (l *registrationLink) Close() { l.closed.Store(true) }
func (l *registrationLink) Write(ctx context.Context, fragment []byte) error {
l.mu.Lock()
defer l.mu.Unlock()
if err := ctx.Err(); err != nil {
return err
}
if fragment[0]&1 != 0 {
l.received = nil
}
l.received = append(l.received, fragment[1:]...)
if fragment[0]>>1 == 0 {
return nil
}
l.writes.Add(1)
if l.received[0] != 1 {
return errors.New("unexpected login or operation")
}
reply := make([]byte, 80)
copy(reply, []byte{7, 1, 0, 0, 0, 0, 128, 45, 0, 18, 90, 0, 0, 0, 0, 0})
copy(reply[16:], l.public)
start := byte(1)
for len(reply) > 0 {
n := len(reply)
if n > 19 {
n = 19
}
header := start
if n == len(reply) {
header |= 2
}
l.packets <- append([]byte{header}, reply[:n]...)
reply = reply[n:]
start = 0
}
return nil
}
func awaitPreparation(t *testing.T, m *Manager, phase string) {
t.Helper()
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
states := m.RegistrationConnections()
if len(states) == 1 && states[0].Phase == phase {
return
}
time.Sleep(time.Millisecond)
}
t.Fatalf("preparation = %+v, want %s", m.RegistrationConnections(), phase)
}
func pendingTarget() sesame.Target {
return sesame.Target{UUID: "00112233-4455-6677-8899-aabbccddeeff", Model: "sesame_6_pro", Adapter: "hci0", Address: "AA:BB:CC:DD:EE:FF"}
}
func TestScanPreparationDeduplicatesAndRegistersExistingLink(t *testing.T) {
l := newRegistrationLink(t)
var dials atomic.Int32
dir := t.TempDir()
m, err := Open(context.Background(), dir, func(_ context.Context, target sesame.Target) (sesame.Link, sesame.Target, error) {
dials.Add(1)
return l, target, nil
})
if err != nil {
t.Fatal(err)
}
defer m.Close()
target := pendingTarget()
if _, err = m.Add(context.Background(), Enrollment{Name: "Door", Target: target, Mode: "register"}); err == nil {
t.Fatal("Add dialed without preparation")
}
m.Prepare([]sesame.Target{target})
awaitPreparation(t, m, "ready")
other := target
other.Adapter = "hci1"
m.Prepare([]sesame.Target{target, other})
awaitPreparation(t, m, "ready")
if dials.Load() != 1 || l.writes.Load() != 0 {
t.Fatal("duplicate dial or registration before click")
}
record, err := m.Add(context.Background(), Enrollment{Name: "Door", Target: target, Mode: "register"})
if err != nil {
t.Fatal(err)
}
if !m.Has(record.ID) || !record.Target.Registered || len(m.RegistrationConnections()) != 0 {
t.Fatal("handoff not committed")
}
if dials.Load() != 1 || l.writes.Load() != 1 || l.closed.Load() {
t.Fatal("registration reconnected, logged in, or closed")
}
views := m.Views()
if len(views) != 1 || views[0].State.Position == nil || *views[0].State.Position != 45 {
t.Fatal("registration status missing")
}
key, err := m.readKey(record.ID)
if err != nil || len(key.Secret) != 32 {
t.Fatal("missing registration key", err)
}
raw, _ := json.Marshal(m.RegistrationConnections())
if string(raw) != "[]" {
t.Fatal("prepared list not empty")
}
m.Close()
if !l.closed.Load() {
t.Fatal("shutdown leaked registered connection")
}
}
func TestDisconnectPreparationAndShutdownCancelOwnedLinks(t *testing.T) {
l := newRegistrationLink(t)
m, err := Open(context.Background(), t.TempDir(), func(_ context.Context, target sesame.Target) (sesame.Link, sesame.Target, error) {
return l, target, nil
})
if err != nil {
t.Fatal(err)
}
defer m.Close()
target := pendingTarget()
m.Prepare([]sesame.Target{target})
awaitPreparation(t, m, "ready")
if !m.DisconnectRegistration(target.Adapter, target.Address) || !l.closed.Load() || len(m.RegistrationConnections()) != 0 {
t.Fatal("waiting link not released")
}
if m.DisconnectRegistration(target.Adapter, target.Address) {
t.Fatal("claimed external link")
}
}
func TestFailedRegistryWriteClosesHandoffAndRetainsRecoveryKey(t *testing.T) {
l := newRegistrationLink(t)
dir := t.TempDir()
m, err := Open(context.Background(), dir, func(_ context.Context, target sesame.Target) (sesame.Link, sesame.Target, error) {
return l, target, nil
})
if err != nil {
t.Fatal(err)
}
defer m.Close()
target := pendingTarget()
m.Prepare([]sesame.Target{target})
awaitPreparation(t, m, "ready")
if err = os.Mkdir(filepath.Join(dir, "physical-locks.json"), 0700); err != nil {
t.Fatal(err)
}
if _, err = m.Add(context.Background(), Enrollment{Name: "Door", Target: target, Mode: "register"}); err == nil {
t.Fatal("accepted registry failure")
}
id, _ := identity(target)
if _, err = m.readKey(id); err != nil {
t.Fatal("recovery key lost", err)
}
if !l.closed.Load() || len(m.RegistrationConnections()) != 0 || m.Has(id) {
t.Fatal("failed commit leaked ownership")
}
}