Files
2026-08-14 00:45:22 +08:00

121 lines
3.4 KiB
Go

package ws
import (
"errors"
"testing"
"silk-server-go/internal/service"
)
type fakeAuthorizer struct {
allowed map[string]bool
}
func (f fakeAuthorizer) CanReadDevice(userID, deviceKey string) (bool, error) {
if f.allowed == nil {
return false, nil
}
return f.allowed[userID+"|"+deviceKey], nil
}
func newTestHub() *Hub {
return NewHub(
"test-secret",
fakeAuthorizer{allowed: map[string]bool{"user-1|device-a": true}},
[]string{"http://localhost:5174"},
)
}
func TestWebSocketOriginRejectsUnknownOrigin(t *testing.T) {
hub := newTestHub()
if hub.originAllowed("http://evil.example") {
t.Fatal("unknown origin should be rejected")
}
if !hub.originAllowed("http://localhost:5174") {
t.Fatal("configured origin should be allowed")
}
}
func TestWebSocketTicketIsOneTimeAndExpires(t *testing.T) {
hub := newTestHub()
ticket := hub.IssueTicket("user-1", "admin", "User One")
if ticket == "" {
t.Fatal("expected non-empty ticket")
}
info, ok := hub.consumeTicket(ticket)
if !ok || info.userID != "user-1" {
t.Fatalf("first ticket consume failed: ok=%v info=%+v", ok, info)
}
if _, ok := hub.consumeTicket(ticket); ok {
t.Fatal("ticket should be single-use")
}
}
func TestWebSocketRejectsLongLivedJWTQuery(t *testing.T) {
hub := newTestHub()
if hub.ticketFromQuery("") != "" {
t.Fatal("missing ticket should not be accepted")
}
if hub.ticketFromQuery("token=eyJhbGciOiJIUzI1NiJ9.abc") != "" {
t.Fatal("long-lived token query must not be accepted")
}
}
func TestWebSocketDeviceAuthorizerRejectsUnauthorized(t *testing.T) {
hub := newTestHub()
if err := hub.authorizeSubscription("user-1", "device-a"); err != nil {
t.Fatalf("authorized device rejected: %v", err)
}
if err := hub.authorizeSubscription("user-1", "device-b"); err == nil {
t.Fatal("unauthorized device should be rejected")
}
}
func TestBroadcastTelemetryDoesNotLeakGlobal(t *testing.T) {
hub := newTestHub()
subscribed := &client{rooms: map[string]bool{"device:device-a": true}, send: make(chan []byte, 1)}
unsubscribed := &client{rooms: map[string]bool{}, send: make(chan []byte, 1)}
hub.clients[subscribed] = true
hub.clients[unsubscribed] = true
hub.BroadcastTelemetry("device-a", map[string]interface{}{"value": 1})
if len(subscribed.send) == 0 {
t.Fatal("subscribed client should receive telemetry")
}
if len(unsubscribed.send) != 0 {
t.Fatal("unsubscribed client must not receive global telemetry")
}
}
func TestBroadcastAlarmDoesNotLeakGlobal(t *testing.T) {
hub := newTestHub()
subscribed := &client{rooms: map[string]bool{"device:device-a": true}, send: make(chan []byte, 1)}
unsubscribed := &client{rooms: map[string]bool{}, send: make(chan []byte, 1)}
hub.clients[subscribed] = true
hub.clients[unsubscribed] = true
hub.BroadcastAlarm(service.AlarmEvent{DeviceKey: "device-a", Code: "high"})
if len(unsubscribed.send) != 0 {
t.Fatal("unsubscribed client must not receive global alarm")
}
}
type brokenAuthorizer struct{}
func (brokenAuthorizer) CanReadDevice(userID, deviceKey string) (bool, error) {
return false, errors.New("authorizer unavailable")
}
func TestWebSocketAuthorizerErrorIsDenied(t *testing.T) {
hub := NewHub("test-secret", brokenAuthorizer{}, nil)
if err := hub.authorizeSubscription("user-1", "device-a"); err == nil {
t.Fatal("authorizer error should deny subscription")
}
if _, ok := hub.consumeTicket("missing"); ok {
t.Fatal("missing ticket should not be consumed")
}
}