Files
silo-server/internal/audiobooks/abssocket/server_test.go
T
RXWatcherandClaude Opus 4.7 7785f286e7 feat(audiobooks): port ABS Socket.io endpoint
Stage 6 of the ABS-compat port. Lifts the plugin's abssocket package
into internal/audiobooks/abssocket/, mounted under /abs/socket.io/ on
silo's main HTTP listener. The plugin's standalone-listener
workaround is no longer needed because the host plugin-proxy WS-bridge
constraint disappears once the code runs in-process.

Redis pub/sub adapter behavior is preserved when REDIS_URL is set;
single-replica in-memory fan-out otherwise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:45:11 +02:00

99 lines
2.9 KiB
Go

package abssocket_test
import (
"net/http/httptest"
"sync"
"testing"
"github.com/Silo-Server/silo-server/internal/audiobooks/abs"
"github.com/Silo-Server/silo-server/internal/audiobooks/abssocket"
)
// recordingLogger captures Warn/Debug calls so we can assert on auth-reject
// paths without depending on hclog wiring.
type recordingLogger struct {
mu sync.Mutex
logs []string
}
func (r *recordingLogger) Debug(msg string, _ ...any) {
r.mu.Lock()
defer r.mu.Unlock()
r.logs = append(r.logs, "debug:"+msg)
}
func (r *recordingLogger) Warn(msg string, _ ...any) {
r.mu.Lock()
defer r.mu.Unlock()
r.logs = append(r.logs, "warn:"+msg)
}
// TestNew_BuildsAndExposesHandler is the bare-minimum smoke: New returns a
// Server, Handler is non-nil, Close is a no-op when never used.
func TestNew_BuildsAndExposesHandler(t *testing.T) {
s := abssocket.New(
func() []byte { return nil },
nil, // nil tokenValidator → skip revocation check
nil, // nil logger → noop
nil, // default in-memory adapter
)
if s == nil {
t.Fatal("New returned nil")
}
if s.Handler() == nil {
t.Fatal("Handler() returned nil")
}
s.Close()
}
// TestHandler_ServesSocketIOOpenHandshake confirms the underlying engine
// returns the Engine.io handshake document on the initial polling GET. The
// payload starts with the Engine.io OPEN packet identifier ("0") followed
// by a JSON envelope carrying {sid, upgrades, pingInterval, pingTimeout}.
func TestHandler_ServesSocketIOOpenHandshake(t *testing.T) {
s := abssocket.New(
func() []byte { return []byte("a-32-byte-secret-for-handshakes!") },
nil,
&recordingLogger{},
nil,
)
defer s.Close()
req := httptest.NewRequest("GET", "/socket.io/?EIO=4&transport=polling", nil)
w := httptest.NewRecorder()
s.Handler().ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("handshake status = %d, want 200; body=%q", w.Code, w.Body.String())
}
body := w.Body.String()
if len(body) == 0 || body[0] != '0' {
// Engine.io v4 prefixes the open packet with literal "0". Any
// other shape means we're not actually serving Socket.io.
t.Fatalf("body must start with Engine.io OPEN ('0'); got %q", body)
}
}
// TestPublish_NoConnections_IsNoOp ensures the publish path doesn't panic
// when no sockets are connected to the user's room.
func TestPublish_NoConnections_IsNoOp(t *testing.T) {
s := abssocket.New(
func() []byte { return nil },
nil,
nil,
nil,
)
defer s.Close()
// Should not panic, should not block.
s.Publish("u-no-such-user", "user_item_progress_updated", map[string]any{"x": 1})
s.Publish("", "noop", nil) // empty user id is an explicit no-op
}
// TestEventPublisherInterfaceSatisfied is a compile-time check (executed at
// test time only). It guarantees that abssocket.Server stays usable wherever
// abs.EventPublisher is expected.
func TestEventPublisherInterfaceSatisfied(t *testing.T) {
var _ abs.EventPublisher = (*abssocket.Server)(nil)
}