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