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>
99 lines
2.9 KiB
Go
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)
|
|
}
|