- Extend sseClient with userID, workstationID, and item filter set - Update Subscribe() to accept userID and workstationID params - Add WatchItem/UnwatchItem/IsWatchingItem methods on sseClient - Add PublishToItem, PublishToWorkstation, PublishToUser targeted delivery - Targeted events get IDs but skip history ring buffer (real-time only) - Update HandleEvents to pass auth user ID and workstation_id query param - Touch workstation last_seen on SSE connect - Existing Publish() broadcast unchanged; all current callers unaffected - Add 5 new tests for targeted delivery and item watch lifecycle Closes #162
119 lines
2.9 KiB
Go
119 lines
2.9 KiB
Go
package api
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
func TestServerStateModeNormal(t *testing.T) {
|
|
b := NewBroker(zerolog.Nop())
|
|
ss := NewServerState(zerolog.Nop(), nil, b)
|
|
|
|
if ss.Mode() != ModeNormal {
|
|
t.Fatalf("expected normal, got %s", ss.Mode())
|
|
}
|
|
if ss.IsReadOnly() {
|
|
t.Fatal("expected not read-only")
|
|
}
|
|
}
|
|
|
|
func TestServerStateModeReadOnly(t *testing.T) {
|
|
b := NewBroker(zerolog.Nop())
|
|
ss := NewServerState(zerolog.Nop(), nil, b)
|
|
|
|
ss.SetReadOnly(true)
|
|
if ss.Mode() != ModeReadOnly {
|
|
t.Fatalf("expected read-only, got %s", ss.Mode())
|
|
}
|
|
if !ss.IsReadOnly() {
|
|
t.Fatal("expected read-only")
|
|
}
|
|
|
|
ss.SetReadOnly(false)
|
|
if ss.Mode() != ModeNormal {
|
|
t.Fatalf("expected normal after clearing read-only, got %s", ss.Mode())
|
|
}
|
|
}
|
|
|
|
func TestServerStateModeDegraded(t *testing.T) {
|
|
b := NewBroker(zerolog.Nop())
|
|
// Pass a non-nil storage placeholder to simulate configured storage.
|
|
// We manipulate storageOK directly to test degraded mode.
|
|
ss := NewServerState(zerolog.Nop(), nil, b)
|
|
|
|
// Simulate storage configured but unhealthy by setting fields directly.
|
|
ss.mu.Lock()
|
|
// We need a non-nil storage pointer to trigger degraded mode check.
|
|
// Since we can't easily create a fake storage, we test the mode() logic
|
|
// by checking that without storage, mode stays normal.
|
|
ss.mu.Unlock()
|
|
|
|
// Without storage configured, mode should be normal even if storageOK is false
|
|
ss.mu.Lock()
|
|
ss.storageOK = false
|
|
ss.mu.Unlock()
|
|
if ss.Mode() != ModeNormal {
|
|
t.Fatalf("expected normal (no storage configured), got %s", ss.Mode())
|
|
}
|
|
}
|
|
|
|
func TestServerStateToggleReadOnly(t *testing.T) {
|
|
b := NewBroker(zerolog.Nop())
|
|
ss := NewServerState(zerolog.Nop(), nil, b)
|
|
|
|
ss.ToggleReadOnly()
|
|
if !ss.IsReadOnly() {
|
|
t.Fatal("expected read-only after toggle")
|
|
}
|
|
|
|
ss.ToggleReadOnly()
|
|
if ss.IsReadOnly() {
|
|
t.Fatal("expected not read-only after second toggle")
|
|
}
|
|
}
|
|
|
|
func TestServerStateBroadcastsOnTransition(t *testing.T) {
|
|
b := NewBroker(zerolog.Nop())
|
|
c := b.Subscribe("", "")
|
|
defer b.Unsubscribe(c)
|
|
|
|
ss := NewServerState(zerolog.Nop(), nil, b)
|
|
|
|
ss.SetReadOnly(true)
|
|
|
|
select {
|
|
case ev := <-c.ch:
|
|
if ev.Type != "server.state" {
|
|
t.Fatalf("expected server.state event, got %s", ev.Type)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for server.state event")
|
|
}
|
|
|
|
// Setting to same value should not broadcast
|
|
ss.SetReadOnly(true)
|
|
select {
|
|
case ev := <-c.ch:
|
|
t.Fatalf("unexpected event on no-op SetReadOnly: %+v", ev)
|
|
case <-time.After(50 * time.Millisecond):
|
|
// expected — no event
|
|
}
|
|
}
|
|
|
|
func TestServerStateReadOnlyOverridesDegraded(t *testing.T) {
|
|
b := NewBroker(zerolog.Nop())
|
|
ss := NewServerState(zerolog.Nop(), nil, b)
|
|
|
|
// Both read-only and storage down: should show read-only
|
|
ss.mu.Lock()
|
|
ss.readOnly = true
|
|
ss.storageOK = false
|
|
ss.mu.Unlock()
|
|
|
|
if ss.Mode() != ModeReadOnly {
|
|
t.Fatalf("expected read-only to override degraded, got %s", ss.Mode())
|
|
}
|
|
}
|