Files
Bose-SoundTouch/pkg/service/soundtouchweb/registry_test.go
T
Tobias GesellchenandClaude Opus 4.7 42257aeebc refactor(soundtouch-web): relocate handlers/webtypes to pkg/service/soundtouchweb
Mechanical relocation only — zero semantic change. Sets up the package
layout that the future Preact-UI rewrite (branch `app`) wants, while
preserving every line of main's current logic. Subsequent commits will
land the additive parts (frontend rewrite, recents, zones, bass control)
on top of this clean base.

Moves (`git mv`, content unchanged except package decl):

  cmd/soundtouch-web/handlers/handlers.go      → pkg/service/soundtouchweb/handler.go
  cmd/soundtouch-web/handlers/handlers_test.go → pkg/service/soundtouchweb/handler_test.go
  cmd/soundtouch-web/handlers/websocket.go     → pkg/service/soundtouchweb/websocket.go
  cmd/soundtouch-web/handlers/registry_test.go → pkg/service/soundtouchweb/registry_test.go
  cmd/soundtouch-web/webtypes/types.go         → pkg/service/soundtouchweb/webtypes/types.go
  cmd/soundtouch-web/webtypes/types_test.go    → pkg/service/soundtouchweb/webtypes/types_test.go
  cmd/soundtouch-web/webtypes/status_test.go   → pkg/service/soundtouchweb/webtypes/status_test.go
  cmd/soundtouch-web/static/img/tunein-{dark,mono}.svg → pkg/service/soundtouchweb/static/img/

Adjustments:

- `package handlers` → `package soundtouchweb` in the 4 moved handler-tier
  files (plus their package-doc comments).
- Import paths rewritten in cmd/soundtouch-web/{main.go,spa_test.go} and
  in the moved files themselves: cmd/soundtouch-web/{handlers,webtypes}
  → pkg/service/soundtouchweb/{,webtypes}.
- `handlers.` selector renamed to `soundtouchweb.` in the callers.
- `.golangci.yml` errcheck waiver extended from `cmd/.*\.go` to also
  cover `pkg/service/soundtouchweb/.*\.go`. Same code that the
  cmd-tier waiver applied to; same waiver follows it. Documented as
  a carry-over with the intent to tighten in a follow-up review.

Not changed:

- `cmd/soundtouch-web/main.go` keeps the `//go:embed static` pointing at
  the still-vanilla `cmd/soundtouch-web/static/`. The frontend rewrite
  (Preact UI) lands in a later commit; this one is mechanical.
- `cmd/soundtouch-web/resolve_bind_addr_test.go` stays put — it tests
  main.go-local flag plumbing.

go build ./... clean. go test ./... clean (only pre-existing
TestDocsConsistency fails, untracked-file issue, unrelated).
golangci-lint run ./... 0 issues.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 22:34:26 +02:00

195 lines
4.7 KiB
Go

// Package handlers contains tests for the device registry API on
// WebApp (GetDevice, AddDevice, TouchDevice, DeviceSnapshot,
// DeviceCount).
package soundtouchweb
import (
"fmt"
"sync"
"testing"
"github.com/gesellix/bose-soundtouch/pkg/models"
"github.com/gesellix/bose-soundtouch/pkg/service/soundtouchweb/webtypes"
)
func newRegistryDevice(name string) *webtypes.DeviceConnection {
conn := webtypes.NewDeviceConnection(nil, &models.DeviceInfo{Name: name})
conn.SetStatus(&webtypes.DeviceStatus{IsConnected: true})
return conn
}
func TestAddDevice_Inserts(t *testing.T) {
app := NewWebApp()
conn := newRegistryDevice("first")
if !app.AddDevice("host-1", conn) {
t.Fatal("AddDevice returned false on first insert")
}
got, ok := app.GetDevice("host-1")
if !ok {
t.Fatal("GetDevice did not find the device after AddDevice")
}
if got != conn {
t.Errorf("GetDevice returned a different pointer than inserted")
}
}
func TestAddDevice_RejectsDuplicateAndBumpsLastSeen(t *testing.T) {
app := NewWebApp()
original := newRegistryDevice("first")
app.AddDevice("host-1", original)
originalSeen := original.LastSeen
replacement := newRegistryDevice("second")
if app.AddDevice("host-1", replacement) {
t.Fatal("AddDevice returned true on duplicate; expected false")
}
got, _ := app.GetDevice("host-1")
if got != original {
t.Error("Duplicate AddDevice replaced the existing device pointer")
}
if !got.LastSeen.After(originalSeen) {
t.Error("Duplicate AddDevice did not bump LastSeen on existing device")
}
}
func TestTouchDevice(t *testing.T) {
app := NewWebApp()
if app.TouchDevice("missing") {
t.Error("TouchDevice returned true for unknown id")
}
conn := newRegistryDevice("first")
app.AddDevice("host-1", conn)
seenBefore := conn.LastSeen
if !app.TouchDevice("host-1") {
t.Fatal("TouchDevice returned false for known id")
}
if !conn.LastSeen.After(seenBefore) {
t.Error("TouchDevice did not bump LastSeen")
}
}
func TestDeviceSnapshotAndCount(t *testing.T) {
app := NewWebApp()
if got := app.DeviceCount(); got != 0 {
t.Errorf("DeviceCount on empty app = %d; want 0", got)
}
if snap := app.DeviceSnapshot(); len(snap) != 0 {
t.Errorf("DeviceSnapshot on empty app = %v; want []", snap)
}
for i := 0; i < 5; i++ {
app.AddDevice(fmt.Sprintf("host-%d", i), newRegistryDevice(fmt.Sprintf("n%d", i)))
}
if got := app.DeviceCount(); got != 5 {
t.Errorf("DeviceCount after 5 adds = %d; want 5", got)
}
snap := app.DeviceSnapshot()
if len(snap) != 5 {
t.Errorf("DeviceSnapshot len = %d; want 5", len(snap))
}
// Spot-check that the snapshot ids match what we inserted.
seen := map[string]bool{}
for _, entry := range snap {
seen[entry.ID] = true
}
for i := 0; i < 5; i++ {
id := fmt.Sprintf("host-%d", i)
if !seen[id] {
t.Errorf("DeviceSnapshot missing %s", id)
}
}
}
// TestRegistryConcurrent exercises the registry from many goroutines
// at once. Before the introduction of devicesMu this would either
// panic with "fatal error: concurrent map read and map write" or be
// flagged by the race detector. The test runs under `go test -race`
// in CI so a future regression that re-exposes the underlying map
// without locking would be caught here.
func TestRegistryConcurrent(t *testing.T) {
app := NewWebApp()
const workers = 16
const opsPerWorker = 200
var wg sync.WaitGroup
wg.Add(workers * 4)
// Writers: insert distinct ids across workers.
for w := 0; w < workers; w++ {
go func(worker int) {
defer wg.Done()
for i := 0; i < opsPerWorker; i++ {
id := fmt.Sprintf("w%d-%d", worker, i)
app.AddDevice(id, newRegistryDevice(id))
}
}(w)
}
// Touchers: bump LastSeen on a shared id (which may or may not
// exist yet — both branches are exercised).
for w := 0; w < workers; w++ {
go func() {
defer wg.Done()
for i := 0; i < opsPerWorker; i++ {
app.TouchDevice("shared")
}
}()
}
// Readers via snapshot.
for w := 0; w < workers; w++ {
go func() {
defer wg.Done()
for i := 0; i < opsPerWorker; i++ {
_ = app.DeviceSnapshot()
}
}()
}
// Readers via direct lookup.
for w := 0; w < workers; w++ {
go func() {
defer wg.Done()
for i := 0; i < opsPerWorker; i++ {
_, _ = app.GetDevice("shared")
_ = app.DeviceCount()
}
}()
}
wg.Wait()
// Sanity check: every writer inserted opsPerWorker devices, plus
// the "shared" entry was never AddDevice'd so should be absent.
if got, want := app.DeviceCount(), workers*opsPerWorker; got != want {
t.Errorf("DeviceCount after concurrent inserts = %d; want %d", got, want)
}
if _, ok := app.GetDevice("shared"); ok {
t.Error("shared device should not exist (only TouchDevice was called for it)")
}
}