mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-01-18 17:04:25 +00:00
Updated to websockspec
This commit is contained in:
3
go.mod
3
go.mod
@@ -11,6 +11,7 @@ require (
|
|||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/jackc/pgx/v5 v5.6.0
|
github.com/jackc/pgx/v5 v5.6.0
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/redis/go-redis/v9 v9.17.1
|
github.com/redis/go-redis/v9 v9.17.1
|
||||||
@@ -62,7 +63,6 @@ require (
|
|||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
@@ -102,6 +102,7 @@ require (
|
|||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
|
|||||||
596
pkg/websocketspec/connection_test.go
Normal file
596
pkg/websocketspec/connection_test.go
Normal file
@@ -0,0 +1,596 @@
|
|||||||
|
package websocketspec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper function to create a test connection with proper initialization
|
||||||
|
func createTestConnection(id string) *Connection {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
return &Connection{
|
||||||
|
ID: id,
|
||||||
|
send: make(chan []byte, 256),
|
||||||
|
subscriptions: make(map[string]*Subscription),
|
||||||
|
metadata: make(map[string]interface{}),
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewConnectionManager(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
cm := NewConnectionManager(ctx)
|
||||||
|
|
||||||
|
assert.NotNil(t, cm)
|
||||||
|
assert.NotNil(t, cm.connections)
|
||||||
|
assert.NotNil(t, cm.register)
|
||||||
|
assert.NotNil(t, cm.unregister)
|
||||||
|
assert.NotNil(t, cm.broadcast)
|
||||||
|
assert.Equal(t, 0, cm.Count())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnectionManager_Count(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
cm := NewConnectionManager(ctx)
|
||||||
|
|
||||||
|
// Start manager
|
||||||
|
go cm.Run()
|
||||||
|
defer func() {
|
||||||
|
// Cancel context without calling Shutdown which tries to close connections
|
||||||
|
cm.cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Initially empty
|
||||||
|
assert.Equal(t, 0, cm.Count())
|
||||||
|
|
||||||
|
// Add a connection
|
||||||
|
conn := createTestConnection("conn-1")
|
||||||
|
|
||||||
|
cm.Register(conn)
|
||||||
|
time.Sleep(10 * time.Millisecond) // Give time for registration
|
||||||
|
|
||||||
|
assert.Equal(t, 1, cm.Count())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnectionManager_Register(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
cm := NewConnectionManager(ctx)
|
||||||
|
|
||||||
|
// Start manager
|
||||||
|
go cm.Run()
|
||||||
|
defer cm.cancel()
|
||||||
|
|
||||||
|
conn := createTestConnection("conn-1")
|
||||||
|
|
||||||
|
cm.Register(conn)
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
// Verify connection was registered
|
||||||
|
retrievedConn, exists := cm.GetConnection("conn-1")
|
||||||
|
assert.True(t, exists)
|
||||||
|
assert.Equal(t, "conn-1", retrievedConn.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnectionManager_Unregister(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
cm := NewConnectionManager(ctx)
|
||||||
|
|
||||||
|
// Start manager
|
||||||
|
go cm.Run()
|
||||||
|
defer cm.cancel()
|
||||||
|
|
||||||
|
conn := &Connection{
|
||||||
|
ID: "conn-1",
|
||||||
|
send: make(chan []byte, 256),
|
||||||
|
subscriptions: make(map[string]*Subscription),
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.Register(conn)
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
assert.Equal(t, 1, cm.Count())
|
||||||
|
|
||||||
|
cm.Unregister(conn)
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
assert.Equal(t, 0, cm.Count())
|
||||||
|
|
||||||
|
// Verify connection was removed
|
||||||
|
_, exists := cm.GetConnection("conn-1")
|
||||||
|
assert.False(t, exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnectionManager_GetConnection(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
cm := NewConnectionManager(ctx)
|
||||||
|
|
||||||
|
// Start manager
|
||||||
|
go cm.Run()
|
||||||
|
defer cm.cancel()
|
||||||
|
|
||||||
|
// Non-existent connection
|
||||||
|
_, exists := cm.GetConnection("non-existent")
|
||||||
|
assert.False(t, exists)
|
||||||
|
|
||||||
|
// Register connection
|
||||||
|
conn := &Connection{
|
||||||
|
ID: "conn-1",
|
||||||
|
send: make(chan []byte, 256),
|
||||||
|
subscriptions: make(map[string]*Subscription),
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.Register(conn)
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
// Get existing connection
|
||||||
|
retrievedConn, exists := cm.GetConnection("conn-1")
|
||||||
|
assert.True(t, exists)
|
||||||
|
assert.Equal(t, "conn-1", retrievedConn.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnectionManager_MultipleConnections(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
cm := NewConnectionManager(ctx)
|
||||||
|
|
||||||
|
// Start manager
|
||||||
|
go cm.Run()
|
||||||
|
defer cm.cancel()
|
||||||
|
|
||||||
|
// Register multiple connections
|
||||||
|
conn1 := &Connection{ID: "conn-1", send: make(chan []byte, 256), subscriptions: make(map[string]*Subscription)}
|
||||||
|
conn2 := &Connection{ID: "conn-2", send: make(chan []byte, 256), subscriptions: make(map[string]*Subscription)}
|
||||||
|
conn3 := &Connection{ID: "conn-3", send: make(chan []byte, 256), subscriptions: make(map[string]*Subscription)}
|
||||||
|
|
||||||
|
cm.Register(conn1)
|
||||||
|
cm.Register(conn2)
|
||||||
|
cm.Register(conn3)
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
assert.Equal(t, 3, cm.Count())
|
||||||
|
|
||||||
|
// Verify all connections exist
|
||||||
|
_, exists := cm.GetConnection("conn-1")
|
||||||
|
assert.True(t, exists)
|
||||||
|
_, exists = cm.GetConnection("conn-2")
|
||||||
|
assert.True(t, exists)
|
||||||
|
_, exists = cm.GetConnection("conn-3")
|
||||||
|
assert.True(t, exists)
|
||||||
|
|
||||||
|
// Unregister one
|
||||||
|
cm.Unregister(conn2)
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
assert.Equal(t, 2, cm.Count())
|
||||||
|
|
||||||
|
// Verify conn-2 is gone but others remain
|
||||||
|
_, exists = cm.GetConnection("conn-2")
|
||||||
|
assert.False(t, exists)
|
||||||
|
_, exists = cm.GetConnection("conn-1")
|
||||||
|
assert.True(t, exists)
|
||||||
|
_, exists = cm.GetConnection("conn-3")
|
||||||
|
assert.True(t, exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnectionManager_Shutdown(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
cm := NewConnectionManager(ctx)
|
||||||
|
|
||||||
|
// Start manager
|
||||||
|
go cm.Run()
|
||||||
|
|
||||||
|
// Register connections
|
||||||
|
conn1 := &Connection{
|
||||||
|
ID: "conn-1",
|
||||||
|
send: make(chan []byte, 256),
|
||||||
|
subscriptions: make(map[string]*Subscription),
|
||||||
|
ctx: context.Background(),
|
||||||
|
}
|
||||||
|
conn1.ctx, conn1.cancel = context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
cm.Register(conn1)
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
assert.Equal(t, 1, cm.Count())
|
||||||
|
|
||||||
|
// Shutdown
|
||||||
|
cm.Shutdown()
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
// Verify context was cancelled
|
||||||
|
select {
|
||||||
|
case <-cm.ctx.Done():
|
||||||
|
// Expected
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Fatal("Context not cancelled after shutdown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnection_SetMetadata(t *testing.T) {
|
||||||
|
conn := &Connection{
|
||||||
|
metadata: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.SetMetadata("user_id", 123)
|
||||||
|
conn.SetMetadata("username", "john")
|
||||||
|
|
||||||
|
// Verify metadata was set
|
||||||
|
userID, exists := conn.GetMetadata("user_id")
|
||||||
|
assert.True(t, exists)
|
||||||
|
assert.Equal(t, 123, userID)
|
||||||
|
|
||||||
|
username, exists := conn.GetMetadata("username")
|
||||||
|
assert.True(t, exists)
|
||||||
|
assert.Equal(t, "john", username)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnection_GetMetadata(t *testing.T) {
|
||||||
|
conn := &Connection{
|
||||||
|
metadata: map[string]interface{}{
|
||||||
|
"user_id": 123,
|
||||||
|
"role": "admin",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get existing metadata
|
||||||
|
userID, exists := conn.GetMetadata("user_id")
|
||||||
|
assert.True(t, exists)
|
||||||
|
assert.Equal(t, 123, userID)
|
||||||
|
|
||||||
|
// Get non-existent metadata
|
||||||
|
_, exists = conn.GetMetadata("non_existent")
|
||||||
|
assert.False(t, exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnection_AddSubscription(t *testing.T) {
|
||||||
|
conn := &Connection{
|
||||||
|
subscriptions: make(map[string]*Subscription),
|
||||||
|
}
|
||||||
|
|
||||||
|
sub := &Subscription{
|
||||||
|
ID: "sub-1",
|
||||||
|
ConnectionID: "conn-1",
|
||||||
|
Entity: "users",
|
||||||
|
Active: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.AddSubscription(sub)
|
||||||
|
|
||||||
|
// Verify subscription was added
|
||||||
|
retrievedSub, exists := conn.GetSubscription("sub-1")
|
||||||
|
assert.True(t, exists)
|
||||||
|
assert.Equal(t, "sub-1", retrievedSub.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnection_RemoveSubscription(t *testing.T) {
|
||||||
|
sub := &Subscription{
|
||||||
|
ID: "sub-1",
|
||||||
|
ConnectionID: "conn-1",
|
||||||
|
Entity: "users",
|
||||||
|
Active: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := &Connection{
|
||||||
|
subscriptions: map[string]*Subscription{
|
||||||
|
"sub-1": sub,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify subscription exists
|
||||||
|
_, exists := conn.GetSubscription("sub-1")
|
||||||
|
assert.True(t, exists)
|
||||||
|
|
||||||
|
// Remove subscription
|
||||||
|
conn.RemoveSubscription("sub-1")
|
||||||
|
|
||||||
|
// Verify subscription was removed
|
||||||
|
_, exists = conn.GetSubscription("sub-1")
|
||||||
|
assert.False(t, exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnection_GetSubscription(t *testing.T) {
|
||||||
|
sub1 := &Subscription{ID: "sub-1", Entity: "users"}
|
||||||
|
sub2 := &Subscription{ID: "sub-2", Entity: "posts"}
|
||||||
|
|
||||||
|
conn := &Connection{
|
||||||
|
subscriptions: map[string]*Subscription{
|
||||||
|
"sub-1": sub1,
|
||||||
|
"sub-2": sub2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get existing subscription
|
||||||
|
retrievedSub, exists := conn.GetSubscription("sub-1")
|
||||||
|
assert.True(t, exists)
|
||||||
|
assert.Equal(t, "sub-1", retrievedSub.ID)
|
||||||
|
|
||||||
|
// Get non-existent subscription
|
||||||
|
_, exists = conn.GetSubscription("non-existent")
|
||||||
|
assert.False(t, exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnection_MultipleSubscriptions(t *testing.T) {
|
||||||
|
conn := &Connection{
|
||||||
|
subscriptions: make(map[string]*Subscription),
|
||||||
|
}
|
||||||
|
|
||||||
|
sub1 := &Subscription{ID: "sub-1", Entity: "users"}
|
||||||
|
sub2 := &Subscription{ID: "sub-2", Entity: "posts"}
|
||||||
|
sub3 := &Subscription{ID: "sub-3", Entity: "comments"}
|
||||||
|
|
||||||
|
conn.AddSubscription(sub1)
|
||||||
|
conn.AddSubscription(sub2)
|
||||||
|
conn.AddSubscription(sub3)
|
||||||
|
|
||||||
|
// Verify all subscriptions exist
|
||||||
|
_, exists := conn.GetSubscription("sub-1")
|
||||||
|
assert.True(t, exists)
|
||||||
|
_, exists = conn.GetSubscription("sub-2")
|
||||||
|
assert.True(t, exists)
|
||||||
|
_, exists = conn.GetSubscription("sub-3")
|
||||||
|
assert.True(t, exists)
|
||||||
|
|
||||||
|
// Remove one subscription
|
||||||
|
conn.RemoveSubscription("sub-2")
|
||||||
|
|
||||||
|
// Verify sub-2 is gone but others remain
|
||||||
|
_, exists = conn.GetSubscription("sub-2")
|
||||||
|
assert.False(t, exists)
|
||||||
|
_, exists = conn.GetSubscription("sub-1")
|
||||||
|
assert.True(t, exists)
|
||||||
|
_, exists = conn.GetSubscription("sub-3")
|
||||||
|
assert.True(t, exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBroadcastMessage_Structure(t *testing.T) {
|
||||||
|
msg := &BroadcastMessage{
|
||||||
|
Message: []byte("test message"),
|
||||||
|
Filter: func(conn *Connection) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NotNil(t, msg.Message)
|
||||||
|
assert.NotNil(t, msg.Filter)
|
||||||
|
assert.Equal(t, "test message", string(msg.Message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBroadcastMessage_Filter(t *testing.T) {
|
||||||
|
// Filter that only allows specific connection
|
||||||
|
filter := func(conn *Connection) bool {
|
||||||
|
return conn.ID == "conn-1"
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := &BroadcastMessage{
|
||||||
|
Message: []byte("test"),
|
||||||
|
Filter: filter,
|
||||||
|
}
|
||||||
|
|
||||||
|
conn1 := &Connection{ID: "conn-1"}
|
||||||
|
conn2 := &Connection{ID: "conn-2"}
|
||||||
|
|
||||||
|
assert.True(t, msg.Filter(conn1))
|
||||||
|
assert.False(t, msg.Filter(conn2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnectionManager_Broadcast(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
cm := NewConnectionManager(ctx)
|
||||||
|
|
||||||
|
// Start manager
|
||||||
|
go cm.Run()
|
||||||
|
defer cm.cancel()
|
||||||
|
|
||||||
|
// Register connections
|
||||||
|
conn1 := &Connection{ID: "conn-1", send: make(chan []byte, 256), subscriptions: make(map[string]*Subscription)}
|
||||||
|
conn2 := &Connection{ID: "conn-2", send: make(chan []byte, 256), subscriptions: make(map[string]*Subscription)}
|
||||||
|
|
||||||
|
cm.Register(conn1)
|
||||||
|
cm.Register(conn2)
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
// Broadcast message
|
||||||
|
message := []byte("test broadcast")
|
||||||
|
cm.Broadcast(message, nil)
|
||||||
|
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
// Verify both connections received the message
|
||||||
|
select {
|
||||||
|
case msg := <-conn1.send:
|
||||||
|
assert.Equal(t, message, msg)
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Fatal("conn1 did not receive message")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case msg := <-conn2.send:
|
||||||
|
assert.Equal(t, message, msg)
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Fatal("conn2 did not receive message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnectionManager_BroadcastWithFilter(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
cm := NewConnectionManager(ctx)
|
||||||
|
|
||||||
|
// Start manager
|
||||||
|
go cm.Run()
|
||||||
|
defer cm.cancel()
|
||||||
|
|
||||||
|
// Register connections with metadata
|
||||||
|
conn1 := &Connection{
|
||||||
|
ID: "conn-1",
|
||||||
|
send: make(chan []byte, 256),
|
||||||
|
subscriptions: make(map[string]*Subscription),
|
||||||
|
metadata: map[string]interface{}{"role": "admin"},
|
||||||
|
}
|
||||||
|
conn2 := &Connection{
|
||||||
|
ID: "conn-2",
|
||||||
|
send: make(chan []byte, 256),
|
||||||
|
subscriptions: make(map[string]*Subscription),
|
||||||
|
metadata: map[string]interface{}{"role": "user"},
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.Register(conn1)
|
||||||
|
cm.Register(conn2)
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
// Broadcast only to admins
|
||||||
|
filter := func(conn *Connection) bool {
|
||||||
|
role, _ := conn.GetMetadata("role")
|
||||||
|
return role == "admin"
|
||||||
|
}
|
||||||
|
|
||||||
|
message := []byte("admin message")
|
||||||
|
cm.Broadcast(message, filter)
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
// Verify only conn1 received the message
|
||||||
|
select {
|
||||||
|
case msg := <-conn1.send:
|
||||||
|
assert.Equal(t, message, msg)
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Fatal("conn1 (admin) did not receive message")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify conn2 did not receive the message
|
||||||
|
select {
|
||||||
|
case <-conn2.send:
|
||||||
|
t.Fatal("conn2 (user) should not have received admin message")
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
// Expected - no message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnection_ConcurrentMetadataAccess(t *testing.T) {
|
||||||
|
// This test verifies that concurrent metadata access doesn't cause race conditions
|
||||||
|
// Run with: go test -race
|
||||||
|
|
||||||
|
conn := &Connection{
|
||||||
|
metadata: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan bool)
|
||||||
|
|
||||||
|
// Goroutine 1: Write metadata
|
||||||
|
go func() {
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
conn.SetMetadata("key", i)
|
||||||
|
}
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Goroutine 2: Read metadata
|
||||||
|
go func() {
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
conn.GetMetadata("key")
|
||||||
|
}
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for completion
|
||||||
|
<-done
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnection_ConcurrentSubscriptionAccess(t *testing.T) {
|
||||||
|
// This test verifies that concurrent subscription access doesn't cause race conditions
|
||||||
|
// Run with: go test -race
|
||||||
|
|
||||||
|
conn := &Connection{
|
||||||
|
subscriptions: make(map[string]*Subscription),
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan bool)
|
||||||
|
|
||||||
|
// Goroutine 1: Add subscriptions
|
||||||
|
go func() {
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
sub := &Subscription{ID: "sub-" + string(rune(i)), Entity: "users"}
|
||||||
|
conn.AddSubscription(sub)
|
||||||
|
}
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Goroutine 2: Get subscriptions
|
||||||
|
go func() {
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
conn.GetSubscription("sub-" + string(rune(i)))
|
||||||
|
}
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for completion
|
||||||
|
<-done
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnectionManager_CompleteLifecycle(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
cm := NewConnectionManager(ctx)
|
||||||
|
|
||||||
|
// Start manager
|
||||||
|
go cm.Run()
|
||||||
|
defer cm.cancel()
|
||||||
|
|
||||||
|
// Create and register connection
|
||||||
|
conn := &Connection{
|
||||||
|
ID: "conn-1",
|
||||||
|
send: make(chan []byte, 256),
|
||||||
|
subscriptions: make(map[string]*Subscription),
|
||||||
|
metadata: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set metadata
|
||||||
|
conn.SetMetadata("user_id", 123)
|
||||||
|
|
||||||
|
// Add subscriptions
|
||||||
|
sub1 := &Subscription{ID: "sub-1", Entity: "users"}
|
||||||
|
sub2 := &Subscription{ID: "sub-2", Entity: "posts"}
|
||||||
|
conn.AddSubscription(sub1)
|
||||||
|
conn.AddSubscription(sub2)
|
||||||
|
|
||||||
|
// Register connection
|
||||||
|
cm.Register(conn)
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
assert.Equal(t, 1, cm.Count())
|
||||||
|
|
||||||
|
// Verify connection exists
|
||||||
|
retrievedConn, exists := cm.GetConnection("conn-1")
|
||||||
|
require.True(t, exists)
|
||||||
|
assert.Equal(t, "conn-1", retrievedConn.ID)
|
||||||
|
|
||||||
|
// Verify metadata
|
||||||
|
userID, exists := retrievedConn.GetMetadata("user_id")
|
||||||
|
assert.True(t, exists)
|
||||||
|
assert.Equal(t, 123, userID)
|
||||||
|
|
||||||
|
// Verify subscriptions
|
||||||
|
_, exists = retrievedConn.GetSubscription("sub-1")
|
||||||
|
assert.True(t, exists)
|
||||||
|
_, exists = retrievedConn.GetSubscription("sub-2")
|
||||||
|
assert.True(t, exists)
|
||||||
|
|
||||||
|
// Broadcast message
|
||||||
|
message := []byte("test message")
|
||||||
|
cm.Broadcast(message, nil)
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case msg := <-retrievedConn.send:
|
||||||
|
assert.Equal(t, message, msg)
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Fatal("Connection did not receive broadcast")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregister connection
|
||||||
|
cm.Unregister(conn)
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
assert.Equal(t, 0, cm.Count())
|
||||||
|
|
||||||
|
// Verify connection is gone
|
||||||
|
_, exists = cm.GetConnection("conn-1")
|
||||||
|
assert.False(t, exists)
|
||||||
|
}
|
||||||
@@ -121,13 +121,11 @@ func Example_withHooks() {
|
|||||||
handler.Hooks().Register(websocketspec.BeforeSubscribe, func(ctx *websocketspec.HookContext) error {
|
handler.Hooks().Register(websocketspec.BeforeSubscribe, func(ctx *websocketspec.HookContext) error {
|
||||||
// Limit subscriptions per connection
|
// Limit subscriptions per connection
|
||||||
maxSubscriptions := 10
|
maxSubscriptions := 10
|
||||||
currentCount := len(ctx.Connection.subscriptions)
|
// Note: In a real implementation, you would count subscriptions using the connection's methods
|
||||||
|
// currentCount := len(ctx.Connection.subscriptions) // subscriptions is private
|
||||||
|
|
||||||
if currentCount >= maxSubscriptions {
|
// For demonstration purposes, we'll just log
|
||||||
return fmt.Errorf("maximum subscriptions reached (%d)", maxSubscriptions)
|
log.Printf("Creating subscription (max: %d)", maxSubscriptions)
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Creating subscription %d/%d", currentCount+1, maxSubscriptions)
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -141,7 +139,7 @@ func Example_monitoring() {
|
|||||||
db, _ := gorm.Open(postgres.Open("your-connection-string"), &gorm.Config{})
|
db, _ := gorm.Open(postgres.Open("your-connection-string"), &gorm.Config{})
|
||||||
handler := websocketspec.NewHandlerWithGORM(db)
|
handler := websocketspec.NewHandlerWithGORM(db)
|
||||||
|
|
||||||
handler.Registry.RegisterModel("public.users", &User{})
|
handler.Registry().RegisterModel("public.users", &User{})
|
||||||
|
|
||||||
// Add connection tracking
|
// Add connection tracking
|
||||||
handler.Hooks().Register(websocketspec.AfterConnect, func(ctx *websocketspec.HookContext) error {
|
handler.Hooks().Register(websocketspec.AfterConnect, func(ctx *websocketspec.HookContext) error {
|
||||||
|
|||||||
823
pkg/websocketspec/handler_test.go
Normal file
823
pkg/websocketspec/handler_test.go
Normal file
@@ -0,0 +1,823 @@
|
|||||||
|
package websocketspec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockDatabase is a mock implementation of common.Database for testing
|
||||||
|
type MockDatabase struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockDatabase) NewSelect() common.SelectQuery {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(common.SelectQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockDatabase) NewInsert() common.InsertQuery {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(common.InsertQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockDatabase) NewUpdate() common.UpdateQuery {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(common.UpdateQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockDatabase) NewDelete() common.DeleteQuery {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(common.DeleteQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockDatabase) Close() error {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockDatabase) Exec(ctx context.Context, query string, args ...interface{}) (common.Result, error) {
|
||||||
|
callArgs := m.Called(ctx, query, args)
|
||||||
|
if callArgs.Get(0) == nil {
|
||||||
|
return nil, callArgs.Error(1)
|
||||||
|
}
|
||||||
|
return callArgs.Get(0).(common.Result), callArgs.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockDatabase) Query(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
|
||||||
|
callArgs := m.Called(ctx, dest, query, args)
|
||||||
|
return callArgs.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockDatabase) BeginTx(ctx context.Context) (common.Database, error) {
|
||||||
|
args := m.Called(ctx)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(common.Database), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockDatabase) CommitTx(ctx context.Context) error {
|
||||||
|
args := m.Called(ctx)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockDatabase) RollbackTx(ctx context.Context) error {
|
||||||
|
args := m.Called(ctx)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockDatabase) RunInTransaction(ctx context.Context, fn func(common.Database) error) error {
|
||||||
|
args := m.Called(ctx, fn)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockDatabase) GetUnderlyingDB() interface{} {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockSelectQuery is a mock implementation of common.SelectQuery
|
||||||
|
type MockSelectQuery struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) Model(model interface{}) common.SelectQuery {
|
||||||
|
args := m.Called(model)
|
||||||
|
return args.Get(0).(common.SelectQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) Table(table string) common.SelectQuery {
|
||||||
|
args := m.Called(table)
|
||||||
|
return args.Get(0).(common.SelectQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) Column(columns ...string) common.SelectQuery {
|
||||||
|
args := m.Called(columns)
|
||||||
|
return args.Get(0).(common.SelectQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) Where(query string, args ...interface{}) common.SelectQuery {
|
||||||
|
callArgs := m.Called(query, args)
|
||||||
|
return callArgs.Get(0).(common.SelectQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) WhereIn(column string, values interface{}) common.SelectQuery {
|
||||||
|
args := m.Called(column, values)
|
||||||
|
return args.Get(0).(common.SelectQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) Order(order string) common.SelectQuery {
|
||||||
|
args := m.Called(order)
|
||||||
|
return args.Get(0).(common.SelectQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) Limit(limit int) common.SelectQuery {
|
||||||
|
args := m.Called(limit)
|
||||||
|
return args.Get(0).(common.SelectQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) Offset(offset int) common.SelectQuery {
|
||||||
|
args := m.Called(offset)
|
||||||
|
return args.Get(0).(common.SelectQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) PreloadRelation(relation string, apply ...func(common.SelectQuery) common.SelectQuery) common.SelectQuery {
|
||||||
|
args := m.Called(relation, apply)
|
||||||
|
return args.Get(0).(common.SelectQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) Preload(relation string, conditions ...interface{}) common.SelectQuery {
|
||||||
|
args := m.Called(relation, conditions)
|
||||||
|
return args.Get(0).(common.SelectQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) ColumnExpr(query string, args ...interface{}) common.SelectQuery {
|
||||||
|
callArgs := m.Called(query, args)
|
||||||
|
return callArgs.Get(0).(common.SelectQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) WhereOr(query string, args ...interface{}) common.SelectQuery {
|
||||||
|
callArgs := m.Called(query, args)
|
||||||
|
return callArgs.Get(0).(common.SelectQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) Join(query string, args ...interface{}) common.SelectQuery {
|
||||||
|
callArgs := m.Called(query, args)
|
||||||
|
return callArgs.Get(0).(common.SelectQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) LeftJoin(query string, args ...interface{}) common.SelectQuery {
|
||||||
|
callArgs := m.Called(query, args)
|
||||||
|
return callArgs.Get(0).(common.SelectQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) JoinRelation(relation string, apply ...func(common.SelectQuery) common.SelectQuery) common.SelectQuery {
|
||||||
|
args := m.Called(relation, apply)
|
||||||
|
return args.Get(0).(common.SelectQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) OrderExpr(order string, args ...interface{}) common.SelectQuery {
|
||||||
|
callArgs := m.Called(order, args)
|
||||||
|
return callArgs.Get(0).(common.SelectQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) Group(group string) common.SelectQuery {
|
||||||
|
args := m.Called(group)
|
||||||
|
return args.Get(0).(common.SelectQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) Having(having string, args ...interface{}) common.SelectQuery {
|
||||||
|
callArgs := m.Called(having, args)
|
||||||
|
return callArgs.Get(0).(common.SelectQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) Scan(ctx context.Context, dest interface{}) error {
|
||||||
|
args := m.Called(ctx, dest)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) ScanModel(ctx context.Context) error {
|
||||||
|
args := m.Called(ctx)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) Count(ctx context.Context) (int, error) {
|
||||||
|
args := m.Called(ctx)
|
||||||
|
return args.Int(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSelectQuery) Exists(ctx context.Context) (bool, error) {
|
||||||
|
args := m.Called(ctx)
|
||||||
|
return args.Bool(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockInsertQuery is a mock implementation of common.InsertQuery
|
||||||
|
type MockInsertQuery struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockInsertQuery) Model(model interface{}) common.InsertQuery {
|
||||||
|
args := m.Called(model)
|
||||||
|
return args.Get(0).(common.InsertQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockInsertQuery) Table(table string) common.InsertQuery {
|
||||||
|
args := m.Called(table)
|
||||||
|
return args.Get(0).(common.InsertQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockInsertQuery) Value(column string, value interface{}) common.InsertQuery {
|
||||||
|
args := m.Called(column, value)
|
||||||
|
return args.Get(0).(common.InsertQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockInsertQuery) OnConflict(action string) common.InsertQuery {
|
||||||
|
args := m.Called(action)
|
||||||
|
return args.Get(0).(common.InsertQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockInsertQuery) Returning(columns ...string) common.InsertQuery {
|
||||||
|
args := m.Called(columns)
|
||||||
|
return args.Get(0).(common.InsertQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockInsertQuery) Exec(ctx context.Context) (common.Result, error) {
|
||||||
|
args := m.Called(ctx)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(common.Result), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockUpdateQuery is a mock implementation of common.UpdateQuery
|
||||||
|
type MockUpdateQuery struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockUpdateQuery) Model(model interface{}) common.UpdateQuery {
|
||||||
|
args := m.Called(model)
|
||||||
|
return args.Get(0).(common.UpdateQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockUpdateQuery) Table(table string) common.UpdateQuery {
|
||||||
|
args := m.Called(table)
|
||||||
|
return args.Get(0).(common.UpdateQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockUpdateQuery) Set(column string, value interface{}) common.UpdateQuery {
|
||||||
|
args := m.Called(column, value)
|
||||||
|
return args.Get(0).(common.UpdateQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockUpdateQuery) SetMap(values map[string]interface{}) common.UpdateQuery {
|
||||||
|
args := m.Called(values)
|
||||||
|
return args.Get(0).(common.UpdateQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockUpdateQuery) Where(query string, args ...interface{}) common.UpdateQuery {
|
||||||
|
callArgs := m.Called(query, args)
|
||||||
|
return callArgs.Get(0).(common.UpdateQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockUpdateQuery) Exec(ctx context.Context) (common.Result, error) {
|
||||||
|
args := m.Called(ctx)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(common.Result), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockDeleteQuery is a mock implementation of common.DeleteQuery
|
||||||
|
type MockDeleteQuery struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockDeleteQuery) Model(model interface{}) common.DeleteQuery {
|
||||||
|
args := m.Called(model)
|
||||||
|
return args.Get(0).(common.DeleteQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockDeleteQuery) Table(table string) common.DeleteQuery {
|
||||||
|
args := m.Called(table)
|
||||||
|
return args.Get(0).(common.DeleteQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockDeleteQuery) Where(query string, args ...interface{}) common.DeleteQuery {
|
||||||
|
callArgs := m.Called(query, args)
|
||||||
|
return callArgs.Get(0).(common.DeleteQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockDeleteQuery) Exec(ctx context.Context) (common.Result, error) {
|
||||||
|
args := m.Called(ctx)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(common.Result), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockModelRegistry is a mock implementation of common.ModelRegistry
|
||||||
|
type MockModelRegistry struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockModelRegistry) RegisterModel(key string, model interface{}) error {
|
||||||
|
args := m.Called(key, model)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockModelRegistry) GetModel(key string) (interface{}, error) {
|
||||||
|
args := m.Called(key)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockModelRegistry) GetAllModels() map[string]interface{} {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockModelRegistry) GetModelByEntity(schema, entity string) (interface{}, error) {
|
||||||
|
args := m.Called(schema, entity)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test model
|
||||||
|
type TestUser struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewHandler(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
assert.NotNil(t, handler)
|
||||||
|
assert.NotNil(t, handler.db)
|
||||||
|
assert.NotNil(t, handler.registry)
|
||||||
|
assert.NotNil(t, handler.hooks)
|
||||||
|
assert.NotNil(t, handler.connManager)
|
||||||
|
assert.NotNil(t, handler.subscriptionManager)
|
||||||
|
assert.NotNil(t, handler.upgrader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_Hooks(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
hooks := handler.Hooks()
|
||||||
|
assert.NotNil(t, hooks)
|
||||||
|
assert.IsType(t, &HookRegistry{}, hooks)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_Registry(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
registry := handler.Registry()
|
||||||
|
assert.NotNil(t, registry)
|
||||||
|
assert.Equal(t, mockRegistry, registry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_GetDatabase(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
db := handler.GetDatabase()
|
||||||
|
assert.NotNil(t, db)
|
||||||
|
assert.Equal(t, mockDB, db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_GetConnectionCount(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
count := handler.GetConnectionCount()
|
||||||
|
assert.Equal(t, 0, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_GetSubscriptionCount(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
count := handler.GetSubscriptionCount()
|
||||||
|
assert.Equal(t, 0, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_GetConnection(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
// Non-existent connection
|
||||||
|
_, exists := handler.GetConnection("non-existent")
|
||||||
|
assert.False(t, exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_HandleMessage_InvalidType(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
conn := &Connection{
|
||||||
|
ID: "conn-1",
|
||||||
|
send: make(chan []byte, 256),
|
||||||
|
subscriptions: make(map[string]*Subscription),
|
||||||
|
ctx: context.Background(),
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := &Message{
|
||||||
|
ID: "msg-1",
|
||||||
|
Type: MessageType("invalid"),
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.HandleMessage(conn, msg)
|
||||||
|
|
||||||
|
// Should send error response
|
||||||
|
select {
|
||||||
|
case data := <-conn.send:
|
||||||
|
var response map[string]interface{}
|
||||||
|
require.NoError(t, ParseMessageBytes(data, &response))
|
||||||
|
assert.False(t, response["success"].(bool))
|
||||||
|
default:
|
||||||
|
t.Fatal("Expected error response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseMessageBytes(data []byte, v interface{}) error {
|
||||||
|
return nil // Simplified for testing
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_GetOperatorSQL(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
operator string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"eq", "="},
|
||||||
|
{"neq", "!="},
|
||||||
|
{"gt", ">"},
|
||||||
|
{"gte", ">="},
|
||||||
|
{"lt", "<"},
|
||||||
|
{"lte", "<="},
|
||||||
|
{"like", "LIKE"},
|
||||||
|
{"ilike", "ILIKE"},
|
||||||
|
{"in", "IN"},
|
||||||
|
{"unknown", "="}, // default
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.operator, func(t *testing.T) {
|
||||||
|
result := handler.getOperatorSQL(tt.operator)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_GetTableName(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
schema string
|
||||||
|
entity string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "With schema",
|
||||||
|
schema: "public",
|
||||||
|
entity: "users",
|
||||||
|
expected: "public.users",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Without schema",
|
||||||
|
schema: "",
|
||||||
|
entity: "users",
|
||||||
|
expected: "users",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Different schema",
|
||||||
|
schema: "custom",
|
||||||
|
entity: "posts",
|
||||||
|
expected: "custom.posts",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := handler.getTableName(tt.schema, tt.entity, &TestUser{})
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_GetMetadata(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
metadata := handler.getMetadata("public", "users", &TestUser{})
|
||||||
|
|
||||||
|
assert.NotNil(t, metadata)
|
||||||
|
assert.Equal(t, "public", metadata["schema"])
|
||||||
|
assert.Equal(t, "users", metadata["entity"])
|
||||||
|
assert.Equal(t, "public.users", metadata["table_name"])
|
||||||
|
assert.NotNil(t, metadata["columns"])
|
||||||
|
assert.NotNil(t, metadata["primary_key"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_NotifySubscribers(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
// Create connection
|
||||||
|
conn := &Connection{
|
||||||
|
ID: "conn-1",
|
||||||
|
send: make(chan []byte, 256),
|
||||||
|
subscriptions: make(map[string]*Subscription),
|
||||||
|
handler: handler,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register connection
|
||||||
|
handler.connManager.connections["conn-1"] = conn
|
||||||
|
|
||||||
|
// Create subscription
|
||||||
|
sub := handler.subscriptionManager.Subscribe("sub-1", "conn-1", "public", "users", nil)
|
||||||
|
conn.AddSubscription(sub)
|
||||||
|
|
||||||
|
// Notify subscribers
|
||||||
|
data := map[string]interface{}{"id": 1, "name": "John"}
|
||||||
|
handler.notifySubscribers("public", "users", OperationCreate, data)
|
||||||
|
|
||||||
|
// Verify notification was sent
|
||||||
|
select {
|
||||||
|
case msg := <-conn.send:
|
||||||
|
assert.NotEmpty(t, msg)
|
||||||
|
default:
|
||||||
|
t.Fatal("Expected notification to be sent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_NotifySubscribers_NoSubscribers(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
// Notify with no subscribers - should not panic
|
||||||
|
data := map[string]interface{}{"id": 1, "name": "John"}
|
||||||
|
handler.notifySubscribers("public", "users", OperationCreate, data)
|
||||||
|
|
||||||
|
// No assertions needed - just checking it doesn't panic
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_NotifySubscribers_ConnectionNotFound(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
// Create subscription without connection
|
||||||
|
handler.subscriptionManager.Subscribe("sub-1", "conn-1", "public", "users", nil)
|
||||||
|
|
||||||
|
// Notify - should handle gracefully when connection not found
|
||||||
|
data := map[string]interface{}{"id": 1, "name": "John"}
|
||||||
|
handler.notifySubscribers("public", "users", OperationCreate, data)
|
||||||
|
|
||||||
|
// No assertions needed - just checking it doesn't panic
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_HooksIntegration(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
beforeCalled := false
|
||||||
|
afterCalled := false
|
||||||
|
|
||||||
|
// Register hooks
|
||||||
|
handler.Hooks().RegisterBefore(OperationCreate, func(ctx *HookContext) error {
|
||||||
|
beforeCalled = true
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.Hooks().RegisterAfter(OperationCreate, func(ctx *HookContext) error {
|
||||||
|
afterCalled = true
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify hooks are registered
|
||||||
|
assert.True(t, handler.Hooks().HasHooks(BeforeCreate))
|
||||||
|
assert.True(t, handler.Hooks().HasHooks(AfterCreate))
|
||||||
|
|
||||||
|
// Execute hooks
|
||||||
|
ctx := &HookContext{Context: context.Background()}
|
||||||
|
handler.Hooks().Execute(BeforeCreate, ctx)
|
||||||
|
handler.Hooks().Execute(AfterCreate, ctx)
|
||||||
|
|
||||||
|
assert.True(t, beforeCalled)
|
||||||
|
assert.True(t, afterCalled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_Shutdown(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
// Shutdown should not panic
|
||||||
|
handler.Shutdown()
|
||||||
|
|
||||||
|
// Verify context was cancelled
|
||||||
|
select {
|
||||||
|
case <-handler.connManager.ctx.Done():
|
||||||
|
// Expected
|
||||||
|
default:
|
||||||
|
t.Fatal("Connection manager context not cancelled after shutdown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_SubscriptionLifecycle(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
// Create connection
|
||||||
|
conn := &Connection{
|
||||||
|
ID: "conn-1",
|
||||||
|
send: make(chan []byte, 256),
|
||||||
|
subscriptions: make(map[string]*Subscription),
|
||||||
|
ctx: context.Background(),
|
||||||
|
handler: handler,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create subscription message
|
||||||
|
msg := &Message{
|
||||||
|
ID: "sub-msg-1",
|
||||||
|
Type: MessageTypeSubscription,
|
||||||
|
Operation: OperationSubscribe,
|
||||||
|
Schema: "public",
|
||||||
|
Entity: "users",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle subscribe
|
||||||
|
handler.handleSubscribe(conn, msg)
|
||||||
|
|
||||||
|
// Verify subscription was created
|
||||||
|
assert.Equal(t, 1, handler.GetSubscriptionCount())
|
||||||
|
assert.Equal(t, 1, len(conn.subscriptions))
|
||||||
|
|
||||||
|
// Verify response was sent
|
||||||
|
select {
|
||||||
|
case data := <-conn.send:
|
||||||
|
assert.NotEmpty(t, data)
|
||||||
|
default:
|
||||||
|
t.Fatal("Expected subscription response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_UnsubscribeLifecycle(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
// Create connection
|
||||||
|
conn := &Connection{
|
||||||
|
ID: "conn-1",
|
||||||
|
send: make(chan []byte, 256),
|
||||||
|
subscriptions: make(map[string]*Subscription),
|
||||||
|
ctx: context.Background(),
|
||||||
|
handler: handler,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create subscription
|
||||||
|
sub := handler.subscriptionManager.Subscribe("sub-1", "conn-1", "public", "users", nil)
|
||||||
|
conn.AddSubscription(sub)
|
||||||
|
|
||||||
|
assert.Equal(t, 1, handler.GetSubscriptionCount())
|
||||||
|
|
||||||
|
// Create unsubscribe message
|
||||||
|
msg := &Message{
|
||||||
|
ID: "unsub-msg-1",
|
||||||
|
Type: MessageTypeSubscription,
|
||||||
|
Operation: OperationUnsubscribe,
|
||||||
|
SubscriptionID: "sub-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle unsubscribe
|
||||||
|
handler.handleUnsubscribe(conn, msg)
|
||||||
|
|
||||||
|
// Verify subscription was removed
|
||||||
|
assert.Equal(t, 0, handler.GetSubscriptionCount())
|
||||||
|
assert.Equal(t, 0, len(conn.subscriptions))
|
||||||
|
|
||||||
|
// Verify response was sent
|
||||||
|
select {
|
||||||
|
case data := <-conn.send:
|
||||||
|
assert.NotEmpty(t, data)
|
||||||
|
default:
|
||||||
|
t.Fatal("Expected unsubscribe response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_HandlePing(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
conn := &Connection{
|
||||||
|
ID: "conn-1",
|
||||||
|
send: make(chan []byte, 256),
|
||||||
|
subscriptions: make(map[string]*Subscription),
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := &Message{
|
||||||
|
ID: "ping-1",
|
||||||
|
Type: MessageTypePing,
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.handlePing(conn, msg)
|
||||||
|
|
||||||
|
// Verify pong was sent
|
||||||
|
select {
|
||||||
|
case data := <-conn.send:
|
||||||
|
assert.NotEmpty(t, data)
|
||||||
|
default:
|
||||||
|
t.Fatal("Expected pong response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_CompleteWorkflow(t *testing.T) {
|
||||||
|
mockDB := &MockDatabase{}
|
||||||
|
mockRegistry := &MockModelRegistry{}
|
||||||
|
handler := NewHandler(mockDB, mockRegistry)
|
||||||
|
|
||||||
|
// Setup hooks (these are registered but not called in this test workflow)
|
||||||
|
handler.Hooks().RegisterBefore(OperationCreate, func(ctx *HookContext) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.Hooks().RegisterAfter(OperationCreate, func(ctx *HookContext) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create connection
|
||||||
|
conn := &Connection{
|
||||||
|
ID: "conn-1",
|
||||||
|
send: make(chan []byte, 256),
|
||||||
|
subscriptions: make(map[string]*Subscription),
|
||||||
|
ctx: context.Background(),
|
||||||
|
handler: handler,
|
||||||
|
metadata: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register connection
|
||||||
|
handler.connManager.connections["conn-1"] = conn
|
||||||
|
|
||||||
|
// Set user metadata
|
||||||
|
conn.SetMetadata("user_id", 123)
|
||||||
|
|
||||||
|
// Create subscription
|
||||||
|
subMsg := &Message{
|
||||||
|
ID: "sub-1",
|
||||||
|
Type: MessageTypeSubscription,
|
||||||
|
Operation: OperationSubscribe,
|
||||||
|
Schema: "public",
|
||||||
|
Entity: "users",
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.handleSubscribe(conn, subMsg)
|
||||||
|
assert.Equal(t, 1, handler.GetSubscriptionCount())
|
||||||
|
|
||||||
|
// Clear send channel
|
||||||
|
select {
|
||||||
|
case <-conn.send:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send ping
|
||||||
|
pingMsg := &Message{
|
||||||
|
ID: "ping-1",
|
||||||
|
Type: MessageTypePing,
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.handlePing(conn, pingMsg)
|
||||||
|
|
||||||
|
// Verify pong was sent
|
||||||
|
select {
|
||||||
|
case <-conn.send:
|
||||||
|
// Expected
|
||||||
|
default:
|
||||||
|
t.Fatal("Expected pong response")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify metadata
|
||||||
|
userID, exists := conn.GetMetadata("user_id")
|
||||||
|
assert.True(t, exists)
|
||||||
|
assert.Equal(t, 123, userID)
|
||||||
|
|
||||||
|
// Verify hooks were registered
|
||||||
|
assert.True(t, handler.Hooks().HasHooks(BeforeCreate))
|
||||||
|
assert.True(t, handler.Hooks().HasHooks(AfterCreate))
|
||||||
|
}
|
||||||
547
pkg/websocketspec/hooks_test.go
Normal file
547
pkg/websocketspec/hooks_test.go
Normal file
@@ -0,0 +1,547 @@
|
|||||||
|
package websocketspec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHookType_Constants(t *testing.T) {
|
||||||
|
assert.Equal(t, HookType("before_read"), BeforeRead)
|
||||||
|
assert.Equal(t, HookType("after_read"), AfterRead)
|
||||||
|
assert.Equal(t, HookType("before_create"), BeforeCreate)
|
||||||
|
assert.Equal(t, HookType("after_create"), AfterCreate)
|
||||||
|
assert.Equal(t, HookType("before_update"), BeforeUpdate)
|
||||||
|
assert.Equal(t, HookType("after_update"), AfterUpdate)
|
||||||
|
assert.Equal(t, HookType("before_delete"), BeforeDelete)
|
||||||
|
assert.Equal(t, HookType("after_delete"), AfterDelete)
|
||||||
|
assert.Equal(t, HookType("before_subscribe"), BeforeSubscribe)
|
||||||
|
assert.Equal(t, HookType("after_subscribe"), AfterSubscribe)
|
||||||
|
assert.Equal(t, HookType("before_unsubscribe"), BeforeUnsubscribe)
|
||||||
|
assert.Equal(t, HookType("after_unsubscribe"), AfterUnsubscribe)
|
||||||
|
assert.Equal(t, HookType("before_connect"), BeforeConnect)
|
||||||
|
assert.Equal(t, HookType("after_connect"), AfterConnect)
|
||||||
|
assert.Equal(t, HookType("before_disconnect"), BeforeDisconnect)
|
||||||
|
assert.Equal(t, HookType("after_disconnect"), AfterDisconnect)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewHookRegistry(t *testing.T) {
|
||||||
|
hr := NewHookRegistry()
|
||||||
|
assert.NotNil(t, hr)
|
||||||
|
assert.NotNil(t, hr.hooks)
|
||||||
|
assert.Empty(t, hr.hooks)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookRegistry_Register(t *testing.T) {
|
||||||
|
hr := NewHookRegistry()
|
||||||
|
|
||||||
|
hookCalled := false
|
||||||
|
hook := func(ctx *HookContext) error {
|
||||||
|
hookCalled = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hr.Register(BeforeRead, hook)
|
||||||
|
|
||||||
|
// Verify hook was registered
|
||||||
|
assert.True(t, hr.HasHooks(BeforeRead))
|
||||||
|
|
||||||
|
// Execute hook
|
||||||
|
ctx := &HookContext{Context: context.Background()}
|
||||||
|
err := hr.Execute(BeforeRead, ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, hookCalled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookRegistry_Register_MultipleHooks(t *testing.T) {
|
||||||
|
hr := NewHookRegistry()
|
||||||
|
|
||||||
|
callOrder := []int{}
|
||||||
|
|
||||||
|
hook1 := func(ctx *HookContext) error {
|
||||||
|
callOrder = append(callOrder, 1)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
hook2 := func(ctx *HookContext) error {
|
||||||
|
callOrder = append(callOrder, 2)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
hook3 := func(ctx *HookContext) error {
|
||||||
|
callOrder = append(callOrder, 3)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hr.Register(BeforeRead, hook1)
|
||||||
|
hr.Register(BeforeRead, hook2)
|
||||||
|
hr.Register(BeforeRead, hook3)
|
||||||
|
|
||||||
|
// Execute hooks
|
||||||
|
ctx := &HookContext{Context: context.Background()}
|
||||||
|
err := hr.Execute(BeforeRead, ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify hooks were called in order
|
||||||
|
assert.Equal(t, []int{1, 2, 3}, callOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookRegistry_RegisterBefore(t *testing.T) {
|
||||||
|
hr := NewHookRegistry()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
operation OperationType
|
||||||
|
hookType HookType
|
||||||
|
}{
|
||||||
|
{OperationRead, BeforeRead},
|
||||||
|
{OperationCreate, BeforeCreate},
|
||||||
|
{OperationUpdate, BeforeUpdate},
|
||||||
|
{OperationDelete, BeforeDelete},
|
||||||
|
{OperationSubscribe, BeforeSubscribe},
|
||||||
|
{OperationUnsubscribe, BeforeUnsubscribe},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(string(tt.operation), func(t *testing.T) {
|
||||||
|
hookCalled := false
|
||||||
|
hook := func(ctx *HookContext) error {
|
||||||
|
hookCalled = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hr.RegisterBefore(tt.operation, hook)
|
||||||
|
assert.True(t, hr.HasHooks(tt.hookType))
|
||||||
|
|
||||||
|
ctx := &HookContext{Context: context.Background()}
|
||||||
|
err := hr.Execute(tt.hookType, ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, hookCalled)
|
||||||
|
|
||||||
|
// Clean up for next test
|
||||||
|
hr.Clear(tt.hookType)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookRegistry_RegisterAfter(t *testing.T) {
|
||||||
|
hr := NewHookRegistry()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
operation OperationType
|
||||||
|
hookType HookType
|
||||||
|
}{
|
||||||
|
{OperationRead, AfterRead},
|
||||||
|
{OperationCreate, AfterCreate},
|
||||||
|
{OperationUpdate, AfterUpdate},
|
||||||
|
{OperationDelete, AfterDelete},
|
||||||
|
{OperationSubscribe, AfterSubscribe},
|
||||||
|
{OperationUnsubscribe, AfterUnsubscribe},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(string(tt.operation), func(t *testing.T) {
|
||||||
|
hookCalled := false
|
||||||
|
hook := func(ctx *HookContext) error {
|
||||||
|
hookCalled = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hr.RegisterAfter(tt.operation, hook)
|
||||||
|
assert.True(t, hr.HasHooks(tt.hookType))
|
||||||
|
|
||||||
|
ctx := &HookContext{Context: context.Background()}
|
||||||
|
err := hr.Execute(tt.hookType, ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, hookCalled)
|
||||||
|
|
||||||
|
// Clean up for next test
|
||||||
|
hr.Clear(tt.hookType)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookRegistry_Execute_NoHooks(t *testing.T) {
|
||||||
|
hr := NewHookRegistry()
|
||||||
|
|
||||||
|
ctx := &HookContext{Context: context.Background()}
|
||||||
|
err := hr.Execute(BeforeRead, ctx)
|
||||||
|
|
||||||
|
// Should not error when no hooks registered
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookRegistry_Execute_HookReturnsError(t *testing.T) {
|
||||||
|
hr := NewHookRegistry()
|
||||||
|
|
||||||
|
expectedErr := errors.New("hook error")
|
||||||
|
hook := func(ctx *HookContext) error {
|
||||||
|
return expectedErr
|
||||||
|
}
|
||||||
|
|
||||||
|
hr.Register(BeforeRead, hook)
|
||||||
|
|
||||||
|
ctx := &HookContext{Context: context.Background()}
|
||||||
|
err := hr.Execute(BeforeRead, ctx)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, expectedErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookRegistry_Execute_FirstHookErrors(t *testing.T) {
|
||||||
|
hr := NewHookRegistry()
|
||||||
|
|
||||||
|
hook1Called := false
|
||||||
|
hook2Called := false
|
||||||
|
|
||||||
|
hook1 := func(ctx *HookContext) error {
|
||||||
|
hook1Called = true
|
||||||
|
return errors.New("hook1 error")
|
||||||
|
}
|
||||||
|
hook2 := func(ctx *HookContext) error {
|
||||||
|
hook2Called = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hr.Register(BeforeRead, hook1)
|
||||||
|
hr.Register(BeforeRead, hook2)
|
||||||
|
|
||||||
|
ctx := &HookContext{Context: context.Background()}
|
||||||
|
err := hr.Execute(BeforeRead, ctx)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.True(t, hook1Called)
|
||||||
|
assert.False(t, hook2Called) // Should not be called after first error
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookRegistry_HasHooks(t *testing.T) {
|
||||||
|
hr := NewHookRegistry()
|
||||||
|
|
||||||
|
assert.False(t, hr.HasHooks(BeforeRead))
|
||||||
|
|
||||||
|
hr.Register(BeforeRead, func(ctx *HookContext) error { return nil })
|
||||||
|
|
||||||
|
assert.True(t, hr.HasHooks(BeforeRead))
|
||||||
|
assert.False(t, hr.HasHooks(AfterRead))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookRegistry_Clear(t *testing.T) {
|
||||||
|
hr := NewHookRegistry()
|
||||||
|
|
||||||
|
hr.Register(BeforeRead, func(ctx *HookContext) error { return nil })
|
||||||
|
hr.Register(BeforeRead, func(ctx *HookContext) error { return nil })
|
||||||
|
assert.True(t, hr.HasHooks(BeforeRead))
|
||||||
|
|
||||||
|
hr.Clear(BeforeRead)
|
||||||
|
assert.False(t, hr.HasHooks(BeforeRead))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookRegistry_ClearAll(t *testing.T) {
|
||||||
|
hr := NewHookRegistry()
|
||||||
|
|
||||||
|
hr.Register(BeforeRead, func(ctx *HookContext) error { return nil })
|
||||||
|
hr.Register(AfterRead, func(ctx *HookContext) error { return nil })
|
||||||
|
hr.Register(BeforeCreate, func(ctx *HookContext) error { return nil })
|
||||||
|
|
||||||
|
assert.True(t, hr.HasHooks(BeforeRead))
|
||||||
|
assert.True(t, hr.HasHooks(AfterRead))
|
||||||
|
assert.True(t, hr.HasHooks(BeforeCreate))
|
||||||
|
|
||||||
|
hr.ClearAll()
|
||||||
|
|
||||||
|
assert.False(t, hr.HasHooks(BeforeRead))
|
||||||
|
assert.False(t, hr.HasHooks(AfterRead))
|
||||||
|
assert.False(t, hr.HasHooks(BeforeCreate))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookContext_Structure(t *testing.T) {
|
||||||
|
ctx := &HookContext{
|
||||||
|
Context: context.Background(),
|
||||||
|
Schema: "public",
|
||||||
|
Entity: "users",
|
||||||
|
TableName: "public.users",
|
||||||
|
ID: "123",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"name": "John",
|
||||||
|
},
|
||||||
|
Options: &common.RequestOptions{
|
||||||
|
Filters: []common.FilterOption{
|
||||||
|
{Column: "status", Operator: "eq", Value: "active"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Metadata: map[string]interface{}{
|
||||||
|
"user_id": 456,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NotNil(t, ctx.Context)
|
||||||
|
assert.Equal(t, "public", ctx.Schema)
|
||||||
|
assert.Equal(t, "users", ctx.Entity)
|
||||||
|
assert.Equal(t, "public.users", ctx.TableName)
|
||||||
|
assert.Equal(t, "123", ctx.ID)
|
||||||
|
assert.NotNil(t, ctx.Data)
|
||||||
|
assert.NotNil(t, ctx.Options)
|
||||||
|
assert.NotNil(t, ctx.Metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookContext_ModifyData(t *testing.T) {
|
||||||
|
hr := NewHookRegistry()
|
||||||
|
|
||||||
|
// Hook that modifies data
|
||||||
|
hook := func(ctx *HookContext) error {
|
||||||
|
if data, ok := ctx.Data.(map[string]interface{}); ok {
|
||||||
|
data["modified"] = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hr.Register(BeforeCreate, hook)
|
||||||
|
|
||||||
|
ctx := &HookContext{
|
||||||
|
Context: context.Background(),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"name": "John",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := hr.Execute(BeforeCreate, ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify data was modified
|
||||||
|
data := ctx.Data.(map[string]interface{})
|
||||||
|
assert.True(t, data["modified"].(bool))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookContext_ModifyOptions(t *testing.T) {
|
||||||
|
hr := NewHookRegistry()
|
||||||
|
|
||||||
|
// Hook that adds a filter
|
||||||
|
hook := func(ctx *HookContext) error {
|
||||||
|
if ctx.Options == nil {
|
||||||
|
ctx.Options = &common.RequestOptions{}
|
||||||
|
}
|
||||||
|
ctx.Options.Filters = append(ctx.Options.Filters, common.FilterOption{
|
||||||
|
Column: "user_id",
|
||||||
|
Operator: "eq",
|
||||||
|
Value: 123,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hr.Register(BeforeRead, hook)
|
||||||
|
|
||||||
|
ctx := &HookContext{
|
||||||
|
Context: context.Background(),
|
||||||
|
Options: &common.RequestOptions{},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := hr.Execute(BeforeRead, ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify filter was added
|
||||||
|
assert.Len(t, ctx.Options.Filters, 1)
|
||||||
|
assert.Equal(t, "user_id", ctx.Options.Filters[0].Column)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookContext_UseMetadata(t *testing.T) {
|
||||||
|
hr := NewHookRegistry()
|
||||||
|
|
||||||
|
// Hook that stores data in metadata
|
||||||
|
hook := func(ctx *HookContext) error {
|
||||||
|
ctx.Metadata["processed"] = true
|
||||||
|
ctx.Metadata["timestamp"] = "2024-01-01"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hr.Register(BeforeCreate, hook)
|
||||||
|
|
||||||
|
ctx := &HookContext{
|
||||||
|
Context: context.Background(),
|
||||||
|
Metadata: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := hr.Execute(BeforeCreate, ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify metadata was set
|
||||||
|
assert.True(t, ctx.Metadata["processed"].(bool))
|
||||||
|
assert.Equal(t, "2024-01-01", ctx.Metadata["timestamp"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookRegistry_Authentication_Example(t *testing.T) {
|
||||||
|
hr := NewHookRegistry()
|
||||||
|
|
||||||
|
// Authentication hook
|
||||||
|
authHook := func(ctx *HookContext) error {
|
||||||
|
// Simulate getting user from connection metadata
|
||||||
|
userID := 123
|
||||||
|
ctx.Metadata["user_id"] = userID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorization hook that uses auth data
|
||||||
|
authzHook := func(ctx *HookContext) error {
|
||||||
|
userID, ok := ctx.Metadata["user_id"]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("unauthorized: not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add filter to only show user's own records
|
||||||
|
if ctx.Options == nil {
|
||||||
|
ctx.Options = &common.RequestOptions{}
|
||||||
|
}
|
||||||
|
ctx.Options.Filters = append(ctx.Options.Filters, common.FilterOption{
|
||||||
|
Column: "user_id",
|
||||||
|
Operator: "eq",
|
||||||
|
Value: userID,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hr.Register(BeforeConnect, authHook)
|
||||||
|
hr.Register(BeforeRead, authzHook)
|
||||||
|
|
||||||
|
// Simulate connection
|
||||||
|
ctx1 := &HookContext{
|
||||||
|
Context: context.Background(),
|
||||||
|
Metadata: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
err := hr.Execute(BeforeConnect, ctx1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 123, ctx1.Metadata["user_id"])
|
||||||
|
|
||||||
|
// Simulate read with authorization
|
||||||
|
ctx2 := &HookContext{
|
||||||
|
Context: context.Background(),
|
||||||
|
Metadata: map[string]interface{}{"user_id": 123},
|
||||||
|
Options: &common.RequestOptions{},
|
||||||
|
}
|
||||||
|
err = hr.Execute(BeforeRead, ctx2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, ctx2.Options.Filters, 1)
|
||||||
|
assert.Equal(t, "user_id", ctx2.Options.Filters[0].Column)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookRegistry_Validation_Example(t *testing.T) {
|
||||||
|
hr := NewHookRegistry()
|
||||||
|
|
||||||
|
// Validation hook
|
||||||
|
validationHook := func(ctx *HookContext) error {
|
||||||
|
data, ok := ctx.Data.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return errors.New("invalid data format")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Entity == "users" {
|
||||||
|
email, hasEmail := data["email"]
|
||||||
|
if !hasEmail || email == "" {
|
||||||
|
return errors.New("validation error: email is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
name, hasName := data["name"]
|
||||||
|
if !hasName || name == "" {
|
||||||
|
return errors.New("validation error: name is required")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hr.Register(BeforeCreate, validationHook)
|
||||||
|
|
||||||
|
// Test with valid data
|
||||||
|
ctx1 := &HookContext{
|
||||||
|
Context: context.Background(),
|
||||||
|
Entity: "users",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := hr.Execute(BeforeCreate, ctx1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test with missing email
|
||||||
|
ctx2 := &HookContext{
|
||||||
|
Context: context.Background(),
|
||||||
|
Entity: "users",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"name": "John Doe",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = hr.Execute(BeforeCreate, ctx2)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "email is required")
|
||||||
|
|
||||||
|
// Test with missing name
|
||||||
|
ctx3 := &HookContext{
|
||||||
|
Context: context.Background(),
|
||||||
|
Entity: "users",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"email": "john@example.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = hr.Execute(BeforeCreate, ctx3)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookRegistry_Logging_Example(t *testing.T) {
|
||||||
|
hr := NewHookRegistry()
|
||||||
|
|
||||||
|
logEntries := []string{}
|
||||||
|
|
||||||
|
// Logging hook for create operations
|
||||||
|
loggingHook := func(ctx *HookContext) error {
|
||||||
|
logEntries = append(logEntries, "Created record in "+ctx.Entity)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hr.Register(AfterCreate, loggingHook)
|
||||||
|
|
||||||
|
ctx := &HookContext{
|
||||||
|
Context: context.Background(),
|
||||||
|
Entity: "users",
|
||||||
|
Result: map[string]interface{}{"id": 1, "name": "John"},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := hr.Execute(AfterCreate, ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, logEntries, 1)
|
||||||
|
assert.Equal(t, "Created record in users", logEntries[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookRegistry_ConcurrentExecution(t *testing.T) {
|
||||||
|
hr := NewHookRegistry()
|
||||||
|
|
||||||
|
// This test verifies that concurrent hook executions don't cause race conditions
|
||||||
|
// Run with: go test -race
|
||||||
|
|
||||||
|
counter := 0
|
||||||
|
hook := func(ctx *HookContext) error {
|
||||||
|
counter++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hr.Register(BeforeRead, hook)
|
||||||
|
|
||||||
|
done := make(chan bool)
|
||||||
|
|
||||||
|
// Execute hooks concurrently
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
go func() {
|
||||||
|
ctx := &HookContext{Context: context.Background()}
|
||||||
|
hr.Execute(BeforeRead, ctx)
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all executions
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, 10, counter)
|
||||||
|
}
|
||||||
414
pkg/websocketspec/message_test.go
Normal file
414
pkg/websocketspec/message_test.go
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
package websocketspec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMessageType_Constants(t *testing.T) {
|
||||||
|
assert.Equal(t, MessageType("request"), MessageTypeRequest)
|
||||||
|
assert.Equal(t, MessageType("response"), MessageTypeResponse)
|
||||||
|
assert.Equal(t, MessageType("notification"), MessageTypeNotification)
|
||||||
|
assert.Equal(t, MessageType("subscription"), MessageTypeSubscription)
|
||||||
|
assert.Equal(t, MessageType("error"), MessageTypeError)
|
||||||
|
assert.Equal(t, MessageType("ping"), MessageTypePing)
|
||||||
|
assert.Equal(t, MessageType("pong"), MessageTypePong)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOperationType_Constants(t *testing.T) {
|
||||||
|
assert.Equal(t, OperationType("read"), OperationRead)
|
||||||
|
assert.Equal(t, OperationType("create"), OperationCreate)
|
||||||
|
assert.Equal(t, OperationType("update"), OperationUpdate)
|
||||||
|
assert.Equal(t, OperationType("delete"), OperationDelete)
|
||||||
|
assert.Equal(t, OperationType("subscribe"), OperationSubscribe)
|
||||||
|
assert.Equal(t, OperationType("unsubscribe"), OperationUnsubscribe)
|
||||||
|
assert.Equal(t, OperationType("meta"), OperationMeta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMessage_ValidRequestMessage(t *testing.T) {
|
||||||
|
jsonData := `{
|
||||||
|
"id": "msg-1",
|
||||||
|
"type": "request",
|
||||||
|
"operation": "read",
|
||||||
|
"schema": "public",
|
||||||
|
"entity": "users",
|
||||||
|
"record_id": "123",
|
||||||
|
"options": {
|
||||||
|
"filters": [
|
||||||
|
{"column": "status", "operator": "eq", "value": "active"}
|
||||||
|
],
|
||||||
|
"limit": 10
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
msg, err := ParseMessage([]byte(jsonData))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, msg)
|
||||||
|
|
||||||
|
assert.Equal(t, "msg-1", msg.ID)
|
||||||
|
assert.Equal(t, MessageTypeRequest, msg.Type)
|
||||||
|
assert.Equal(t, OperationRead, msg.Operation)
|
||||||
|
assert.Equal(t, "public", msg.Schema)
|
||||||
|
assert.Equal(t, "users", msg.Entity)
|
||||||
|
assert.Equal(t, "123", msg.RecordID)
|
||||||
|
assert.NotNil(t, msg.Options)
|
||||||
|
assert.Equal(t, 10, *msg.Options.Limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMessage_ValidSubscriptionMessage(t *testing.T) {
|
||||||
|
jsonData := `{
|
||||||
|
"id": "sub-1",
|
||||||
|
"type": "subscription",
|
||||||
|
"operation": "subscribe",
|
||||||
|
"schema": "public",
|
||||||
|
"entity": "users"
|
||||||
|
}`
|
||||||
|
|
||||||
|
msg, err := ParseMessage([]byte(jsonData))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, msg)
|
||||||
|
|
||||||
|
assert.Equal(t, "sub-1", msg.ID)
|
||||||
|
assert.Equal(t, MessageTypeSubscription, msg.Type)
|
||||||
|
assert.Equal(t, OperationSubscribe, msg.Operation)
|
||||||
|
assert.Equal(t, "public", msg.Schema)
|
||||||
|
assert.Equal(t, "users", msg.Entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMessage_InvalidJSON(t *testing.T) {
|
||||||
|
jsonData := `{invalid json}`
|
||||||
|
|
||||||
|
msg, err := ParseMessage([]byte(jsonData))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMessage_EmptyData(t *testing.T) {
|
||||||
|
msg, err := ParseMessage([]byte{})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessage_IsValid_ValidRequestMessage(t *testing.T) {
|
||||||
|
msg := &Message{
|
||||||
|
ID: "msg-1",
|
||||||
|
Type: MessageTypeRequest,
|
||||||
|
Operation: OperationRead,
|
||||||
|
Entity: "users",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, msg.IsValid())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessage_IsValid_InvalidRequestMessage_NoID(t *testing.T) {
|
||||||
|
msg := &Message{
|
||||||
|
Type: MessageTypeRequest,
|
||||||
|
Operation: OperationRead,
|
||||||
|
Entity: "users",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.False(t, msg.IsValid())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessage_IsValid_InvalidRequestMessage_NoOperation(t *testing.T) {
|
||||||
|
msg := &Message{
|
||||||
|
ID: "msg-1",
|
||||||
|
Type: MessageTypeRequest,
|
||||||
|
Entity: "users",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.False(t, msg.IsValid())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessage_IsValid_InvalidRequestMessage_NoEntity(t *testing.T) {
|
||||||
|
msg := &Message{
|
||||||
|
ID: "msg-1",
|
||||||
|
Type: MessageTypeRequest,
|
||||||
|
Operation: OperationRead,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.False(t, msg.IsValid())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessage_IsValid_ValidSubscriptionMessage(t *testing.T) {
|
||||||
|
msg := &Message{
|
||||||
|
ID: "sub-1",
|
||||||
|
Type: MessageTypeSubscription,
|
||||||
|
Operation: OperationSubscribe,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, msg.IsValid())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessage_IsValid_InvalidSubscriptionMessage_NoID(t *testing.T) {
|
||||||
|
msg := &Message{
|
||||||
|
Type: MessageTypeSubscription,
|
||||||
|
Operation: OperationSubscribe,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.False(t, msg.IsValid())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessage_IsValid_InvalidSubscriptionMessage_NoOperation(t *testing.T) {
|
||||||
|
msg := &Message{
|
||||||
|
ID: "sub-1",
|
||||||
|
Type: MessageTypeSubscription,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.False(t, msg.IsValid())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessage_IsValid_NoType(t *testing.T) {
|
||||||
|
msg := &Message{
|
||||||
|
ID: "msg-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.False(t, msg.IsValid())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessage_IsValid_ResponseMessage(t *testing.T) {
|
||||||
|
msg := &Message{
|
||||||
|
Type: MessageTypeResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response messages don't require specific fields
|
||||||
|
assert.True(t, msg.IsValid())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessage_IsValid_NotificationMessage(t *testing.T) {
|
||||||
|
msg := &Message{
|
||||||
|
Type: MessageTypeNotification,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification messages don't require specific fields
|
||||||
|
assert.True(t, msg.IsValid())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessage_ToJSON(t *testing.T) {
|
||||||
|
msg := &Message{
|
||||||
|
ID: "msg-1",
|
||||||
|
Type: MessageTypeRequest,
|
||||||
|
Operation: OperationRead,
|
||||||
|
Entity: "users",
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := msg.ToJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, jsonData)
|
||||||
|
|
||||||
|
// Parse back to verify
|
||||||
|
var parsed map[string]interface{}
|
||||||
|
err = json.Unmarshal(jsonData, &parsed)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "msg-1", parsed["id"])
|
||||||
|
assert.Equal(t, "request", parsed["type"])
|
||||||
|
assert.Equal(t, "read", parsed["operation"])
|
||||||
|
assert.Equal(t, "users", parsed["entity"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRequestMessage(t *testing.T) {
|
||||||
|
msg := NewRequestMessage("msg-1", OperationRead, "public", "users")
|
||||||
|
|
||||||
|
assert.Equal(t, "msg-1", msg.ID)
|
||||||
|
assert.Equal(t, MessageTypeRequest, msg.Type)
|
||||||
|
assert.Equal(t, OperationRead, msg.Operation)
|
||||||
|
assert.Equal(t, "public", msg.Schema)
|
||||||
|
assert.Equal(t, "users", msg.Entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewResponseMessage(t *testing.T) {
|
||||||
|
data := map[string]interface{}{"id": 1, "name": "John"}
|
||||||
|
msg := NewResponseMessage("msg-1", true, data)
|
||||||
|
|
||||||
|
assert.Equal(t, "msg-1", msg.ID)
|
||||||
|
assert.Equal(t, MessageTypeResponse, msg.Type)
|
||||||
|
assert.True(t, msg.Success)
|
||||||
|
assert.Equal(t, data, msg.Data)
|
||||||
|
assert.False(t, msg.Timestamp.IsZero())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewErrorResponse(t *testing.T) {
|
||||||
|
msg := NewErrorResponse("msg-1", "validation_error", "Email is required")
|
||||||
|
|
||||||
|
assert.Equal(t, "msg-1", msg.ID)
|
||||||
|
assert.Equal(t, MessageTypeResponse, msg.Type)
|
||||||
|
assert.False(t, msg.Success)
|
||||||
|
assert.Nil(t, msg.Data)
|
||||||
|
assert.NotNil(t, msg.Error)
|
||||||
|
assert.Equal(t, "validation_error", msg.Error.Code)
|
||||||
|
assert.Equal(t, "Email is required", msg.Error.Message)
|
||||||
|
assert.False(t, msg.Timestamp.IsZero())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewNotificationMessage(t *testing.T) {
|
||||||
|
data := map[string]interface{}{"id": 1, "name": "John"}
|
||||||
|
msg := NewNotificationMessage("sub-123", OperationCreate, "public", "users", data)
|
||||||
|
|
||||||
|
assert.Equal(t, MessageTypeNotification, msg.Type)
|
||||||
|
assert.Equal(t, OperationCreate, msg.Operation)
|
||||||
|
assert.Equal(t, "sub-123", msg.SubscriptionID)
|
||||||
|
assert.Equal(t, "public", msg.Schema)
|
||||||
|
assert.Equal(t, "users", msg.Entity)
|
||||||
|
assert.Equal(t, data, msg.Data)
|
||||||
|
assert.False(t, msg.Timestamp.IsZero())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResponseMessage_ToJSON(t *testing.T) {
|
||||||
|
resp := NewResponseMessage("msg-1", true, map[string]interface{}{"test": "data"})
|
||||||
|
|
||||||
|
jsonData, err := resp.ToJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, jsonData)
|
||||||
|
|
||||||
|
// Verify JSON structure
|
||||||
|
var parsed map[string]interface{}
|
||||||
|
err = json.Unmarshal(jsonData, &parsed)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "msg-1", parsed["id"])
|
||||||
|
assert.Equal(t, "response", parsed["type"])
|
||||||
|
assert.True(t, parsed["success"].(bool))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotificationMessage_ToJSON(t *testing.T) {
|
||||||
|
notif := NewNotificationMessage("sub-123", OperationUpdate, "public", "users", map[string]interface{}{"id": 1})
|
||||||
|
|
||||||
|
jsonData, err := notif.ToJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, jsonData)
|
||||||
|
|
||||||
|
// Verify JSON structure
|
||||||
|
var parsed map[string]interface{}
|
||||||
|
err = json.Unmarshal(jsonData, &parsed)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "notification", parsed["type"])
|
||||||
|
assert.Equal(t, "update", parsed["operation"])
|
||||||
|
assert.Equal(t, "sub-123", parsed["subscription_id"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrorInfo_Structure(t *testing.T) {
|
||||||
|
err := &ErrorInfo{
|
||||||
|
Code: "validation_error",
|
||||||
|
Message: "Invalid input",
|
||||||
|
Details: map[string]interface{}{
|
||||||
|
"field": "email",
|
||||||
|
"value": "invalid",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "validation_error", err.Code)
|
||||||
|
assert.Equal(t, "Invalid input", err.Message)
|
||||||
|
assert.NotNil(t, err.Details)
|
||||||
|
assert.Equal(t, "email", err.Details["field"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessage_WithOptions(t *testing.T) {
|
||||||
|
limit := 10
|
||||||
|
offset := 0
|
||||||
|
|
||||||
|
msg := &Message{
|
||||||
|
ID: "msg-1",
|
||||||
|
Type: MessageTypeRequest,
|
||||||
|
Operation: OperationRead,
|
||||||
|
Entity: "users",
|
||||||
|
Options: &common.RequestOptions{
|
||||||
|
Filters: []common.FilterOption{
|
||||||
|
{Column: "status", Operator: "eq", Value: "active"},
|
||||||
|
},
|
||||||
|
Columns: []string{"id", "name", "email"},
|
||||||
|
Sort: []common.SortOption{
|
||||||
|
{Column: "name", Direction: "asc"},
|
||||||
|
},
|
||||||
|
Limit: &limit,
|
||||||
|
Offset: &offset,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, msg.IsValid())
|
||||||
|
assert.NotNil(t, msg.Options)
|
||||||
|
assert.Len(t, msg.Options.Filters, 1)
|
||||||
|
assert.Equal(t, "status", msg.Options.Filters[0].Column)
|
||||||
|
assert.Len(t, msg.Options.Columns, 3)
|
||||||
|
assert.Len(t, msg.Options.Sort, 1)
|
||||||
|
assert.Equal(t, 10, *msg.Options.Limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessage_CompleteRequestFlow(t *testing.T) {
|
||||||
|
// Create a request message
|
||||||
|
req := NewRequestMessage("msg-123", OperationCreate, "public", "users")
|
||||||
|
req.Data = map[string]interface{}{
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"status": "active",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to JSON
|
||||||
|
reqJSON, err := json.Marshal(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Parse back
|
||||||
|
parsed, err := ParseMessage(reqJSON)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, parsed.IsValid())
|
||||||
|
assert.Equal(t, "msg-123", parsed.ID)
|
||||||
|
assert.Equal(t, MessageTypeRequest, parsed.Type)
|
||||||
|
assert.Equal(t, OperationCreate, parsed.Operation)
|
||||||
|
|
||||||
|
// Create success response
|
||||||
|
resp := NewResponseMessage("msg-123", true, map[string]interface{}{
|
||||||
|
"id": 1,
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"status": "active",
|
||||||
|
})
|
||||||
|
|
||||||
|
respJSON, err := resp.ToJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, respJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessage_TimestampSerialization(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
msg := &Message{
|
||||||
|
ID: "msg-1",
|
||||||
|
Type: MessageTypeResponse,
|
||||||
|
Timestamp: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := msg.ToJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Parse back
|
||||||
|
parsed, err := ParseMessage(jsonData)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Timestamps should be approximately equal (within a second due to serialization)
|
||||||
|
assert.WithinDuration(t, now, parsed.Timestamp, time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessage_WithMetadata(t *testing.T) {
|
||||||
|
msg := &Message{
|
||||||
|
ID: "msg-1",
|
||||||
|
Type: MessageTypeResponse,
|
||||||
|
Success: true,
|
||||||
|
Data: []interface{}{},
|
||||||
|
Metadata: map[string]interface{}{
|
||||||
|
"total": 100,
|
||||||
|
"count": 10,
|
||||||
|
"page": 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := msg.ToJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
parsed, err := ParseMessage(jsonData)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, parsed.Metadata)
|
||||||
|
assert.Equal(t, float64(100), parsed.Metadata["total"]) // JSON numbers are float64
|
||||||
|
assert.Equal(t, float64(10), parsed.Metadata["count"])
|
||||||
|
}
|
||||||
434
pkg/websocketspec/subscription_test.go
Normal file
434
pkg/websocketspec/subscription_test.go
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
package websocketspec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewSubscriptionManager(t *testing.T) {
|
||||||
|
sm := NewSubscriptionManager()
|
||||||
|
assert.NotNil(t, sm)
|
||||||
|
assert.NotNil(t, sm.subscriptions)
|
||||||
|
assert.NotNil(t, sm.entitySubscriptions)
|
||||||
|
assert.Equal(t, 0, sm.Count())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscriptionManager_Subscribe(t *testing.T) {
|
||||||
|
sm := NewSubscriptionManager()
|
||||||
|
|
||||||
|
// Create a subscription
|
||||||
|
sub := sm.Subscribe("sub-1", "conn-1", "public", "users", nil)
|
||||||
|
|
||||||
|
assert.NotNil(t, sub)
|
||||||
|
assert.Equal(t, "sub-1", sub.ID)
|
||||||
|
assert.Equal(t, "conn-1", sub.ConnectionID)
|
||||||
|
assert.Equal(t, "public", sub.Schema)
|
||||||
|
assert.Equal(t, "users", sub.Entity)
|
||||||
|
assert.True(t, sub.Active)
|
||||||
|
assert.Equal(t, 1, sm.Count())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscriptionManager_Subscribe_WithOptions(t *testing.T) {
|
||||||
|
sm := NewSubscriptionManager()
|
||||||
|
|
||||||
|
options := &common.RequestOptions{
|
||||||
|
Filters: []common.FilterOption{
|
||||||
|
{Column: "status", Operator: "eq", Value: "active"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sub := sm.Subscribe("sub-1", "conn-1", "public", "users", options)
|
||||||
|
|
||||||
|
assert.NotNil(t, sub)
|
||||||
|
assert.NotNil(t, sub.Options)
|
||||||
|
assert.Len(t, sub.Options.Filters, 1)
|
||||||
|
assert.Equal(t, "status", sub.Options.Filters[0].Column)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscriptionManager_Subscribe_MultipleSubscriptions(t *testing.T) {
|
||||||
|
sm := NewSubscriptionManager()
|
||||||
|
|
||||||
|
sub1 := sm.Subscribe("sub-1", "conn-1", "public", "users", nil)
|
||||||
|
sub2 := sm.Subscribe("sub-2", "conn-1", "public", "posts", nil)
|
||||||
|
sub3 := sm.Subscribe("sub-3", "conn-2", "public", "users", nil)
|
||||||
|
|
||||||
|
assert.NotNil(t, sub1)
|
||||||
|
assert.NotNil(t, sub2)
|
||||||
|
assert.NotNil(t, sub3)
|
||||||
|
assert.Equal(t, 3, sm.Count())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscriptionManager_Unsubscribe(t *testing.T) {
|
||||||
|
sm := NewSubscriptionManager()
|
||||||
|
|
||||||
|
sm.Subscribe("sub-1", "conn-1", "public", "users", nil)
|
||||||
|
assert.Equal(t, 1, sm.Count())
|
||||||
|
|
||||||
|
// Unsubscribe
|
||||||
|
ok := sm.Unsubscribe("sub-1")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, 0, sm.Count())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscriptionManager_Unsubscribe_NonExistent(t *testing.T) {
|
||||||
|
sm := NewSubscriptionManager()
|
||||||
|
|
||||||
|
ok := sm.Unsubscribe("non-existent")
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscriptionManager_Unsubscribe_MultipleSubscriptions(t *testing.T) {
|
||||||
|
sm := NewSubscriptionManager()
|
||||||
|
|
||||||
|
sm.Subscribe("sub-1", "conn-1", "public", "users", nil)
|
||||||
|
sm.Subscribe("sub-2", "conn-1", "public", "posts", nil)
|
||||||
|
sm.Subscribe("sub-3", "conn-2", "public", "users", nil)
|
||||||
|
assert.Equal(t, 3, sm.Count())
|
||||||
|
|
||||||
|
// Unsubscribe one
|
||||||
|
ok := sm.Unsubscribe("sub-2")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, 2, sm.Count())
|
||||||
|
|
||||||
|
// Verify the right subscription was removed
|
||||||
|
_, exists := sm.GetSubscription("sub-2")
|
||||||
|
assert.False(t, exists)
|
||||||
|
|
||||||
|
_, exists = sm.GetSubscription("sub-1")
|
||||||
|
assert.True(t, exists)
|
||||||
|
|
||||||
|
_, exists = sm.GetSubscription("sub-3")
|
||||||
|
assert.True(t, exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscriptionManager_GetSubscription(t *testing.T) {
|
||||||
|
sm := NewSubscriptionManager()
|
||||||
|
|
||||||
|
sm.Subscribe("sub-1", "conn-1", "public", "users", nil)
|
||||||
|
|
||||||
|
// Get existing subscription
|
||||||
|
sub, exists := sm.GetSubscription("sub-1")
|
||||||
|
assert.True(t, exists)
|
||||||
|
assert.NotNil(t, sub)
|
||||||
|
assert.Equal(t, "sub-1", sub.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscriptionManager_GetSubscription_NonExistent(t *testing.T) {
|
||||||
|
sm := NewSubscriptionManager()
|
||||||
|
|
||||||
|
sub, exists := sm.GetSubscription("non-existent")
|
||||||
|
assert.False(t, exists)
|
||||||
|
assert.Nil(t, sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscriptionManager_GetSubscriptionsByEntity(t *testing.T) {
|
||||||
|
sm := NewSubscriptionManager()
|
||||||
|
|
||||||
|
sm.Subscribe("sub-1", "conn-1", "public", "users", nil)
|
||||||
|
sm.Subscribe("sub-2", "conn-2", "public", "users", nil)
|
||||||
|
sm.Subscribe("sub-3", "conn-1", "public", "posts", nil)
|
||||||
|
|
||||||
|
// Get subscriptions for users entity
|
||||||
|
subs := sm.GetSubscriptionsByEntity("public", "users")
|
||||||
|
assert.Len(t, subs, 2)
|
||||||
|
|
||||||
|
// Verify subscription IDs
|
||||||
|
ids := make([]string, len(subs))
|
||||||
|
for i, sub := range subs {
|
||||||
|
ids[i] = sub.ID
|
||||||
|
}
|
||||||
|
assert.Contains(t, ids, "sub-1")
|
||||||
|
assert.Contains(t, ids, "sub-2")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscriptionManager_GetSubscriptionsByEntity_NoSchema(t *testing.T) {
|
||||||
|
sm := NewSubscriptionManager()
|
||||||
|
|
||||||
|
sm.Subscribe("sub-1", "conn-1", "", "users", nil)
|
||||||
|
sm.Subscribe("sub-2", "conn-2", "", "users", nil)
|
||||||
|
|
||||||
|
// Get subscriptions without schema
|
||||||
|
subs := sm.GetSubscriptionsByEntity("", "users")
|
||||||
|
assert.Len(t, subs, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscriptionManager_GetSubscriptionsByEntity_NoResults(t *testing.T) {
|
||||||
|
sm := NewSubscriptionManager()
|
||||||
|
|
||||||
|
sm.Subscribe("sub-1", "conn-1", "public", "users", nil)
|
||||||
|
|
||||||
|
// Get subscriptions for non-existent entity
|
||||||
|
subs := sm.GetSubscriptionsByEntity("public", "posts")
|
||||||
|
assert.Nil(t, subs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscriptionManager_GetSubscriptionsByConnection(t *testing.T) {
|
||||||
|
sm := NewSubscriptionManager()
|
||||||
|
|
||||||
|
sm.Subscribe("sub-1", "conn-1", "public", "users", nil)
|
||||||
|
sm.Subscribe("sub-2", "conn-1", "public", "posts", nil)
|
||||||
|
sm.Subscribe("sub-3", "conn-2", "public", "users", nil)
|
||||||
|
|
||||||
|
// Get subscriptions for connection 1
|
||||||
|
subs := sm.GetSubscriptionsByConnection("conn-1")
|
||||||
|
assert.Len(t, subs, 2)
|
||||||
|
|
||||||
|
// Verify subscription IDs
|
||||||
|
ids := make([]string, len(subs))
|
||||||
|
for i, sub := range subs {
|
||||||
|
ids[i] = sub.ID
|
||||||
|
}
|
||||||
|
assert.Contains(t, ids, "sub-1")
|
||||||
|
assert.Contains(t, ids, "sub-2")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscriptionManager_GetSubscriptionsByConnection_NoResults(t *testing.T) {
|
||||||
|
sm := NewSubscriptionManager()
|
||||||
|
|
||||||
|
sm.Subscribe("sub-1", "conn-1", "public", "users", nil)
|
||||||
|
|
||||||
|
// Get subscriptions for non-existent connection
|
||||||
|
subs := sm.GetSubscriptionsByConnection("conn-2")
|
||||||
|
assert.Empty(t, subs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscriptionManager_Count(t *testing.T) {
|
||||||
|
sm := NewSubscriptionManager()
|
||||||
|
|
||||||
|
assert.Equal(t, 0, sm.Count())
|
||||||
|
|
||||||
|
sm.Subscribe("sub-1", "conn-1", "public", "users", nil)
|
||||||
|
assert.Equal(t, 1, sm.Count())
|
||||||
|
|
||||||
|
sm.Subscribe("sub-2", "conn-1", "public", "posts", nil)
|
||||||
|
assert.Equal(t, 2, sm.Count())
|
||||||
|
|
||||||
|
sm.Unsubscribe("sub-1")
|
||||||
|
assert.Equal(t, 1, sm.Count())
|
||||||
|
|
||||||
|
sm.Unsubscribe("sub-2")
|
||||||
|
assert.Equal(t, 0, sm.Count())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscriptionManager_CountForEntity(t *testing.T) {
|
||||||
|
sm := NewSubscriptionManager()
|
||||||
|
|
||||||
|
sm.Subscribe("sub-1", "conn-1", "public", "users", nil)
|
||||||
|
sm.Subscribe("sub-2", "conn-2", "public", "users", nil)
|
||||||
|
sm.Subscribe("sub-3", "conn-1", "public", "posts", nil)
|
||||||
|
|
||||||
|
assert.Equal(t, 2, sm.CountForEntity("public", "users"))
|
||||||
|
assert.Equal(t, 1, sm.CountForEntity("public", "posts"))
|
||||||
|
assert.Equal(t, 0, sm.CountForEntity("public", "orders"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscriptionManager_UnsubscribeUpdatesEntityIndex(t *testing.T) {
|
||||||
|
sm := NewSubscriptionManager()
|
||||||
|
|
||||||
|
sm.Subscribe("sub-1", "conn-1", "public", "users", nil)
|
||||||
|
sm.Subscribe("sub-2", "conn-2", "public", "users", nil)
|
||||||
|
assert.Equal(t, 2, sm.CountForEntity("public", "users"))
|
||||||
|
|
||||||
|
// Unsubscribe one
|
||||||
|
sm.Unsubscribe("sub-1")
|
||||||
|
assert.Equal(t, 1, sm.CountForEntity("public", "users"))
|
||||||
|
|
||||||
|
// Unsubscribe the other
|
||||||
|
sm.Unsubscribe("sub-2")
|
||||||
|
assert.Equal(t, 0, sm.CountForEntity("public", "users"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscription_MatchesFilters_NoFilters(t *testing.T) {
|
||||||
|
sub := &Subscription{
|
||||||
|
ID: "sub-1",
|
||||||
|
ConnectionID: "conn-1",
|
||||||
|
Schema: "public",
|
||||||
|
Entity: "users",
|
||||||
|
Options: nil,
|
||||||
|
Active: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"id": 1,
|
||||||
|
"name": "John",
|
||||||
|
"status": "active",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should match when no filters are specified
|
||||||
|
assert.True(t, sub.MatchesFilters(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscription_MatchesFilters_WithFilters(t *testing.T) {
|
||||||
|
sub := &Subscription{
|
||||||
|
ID: "sub-1",
|
||||||
|
ConnectionID: "conn-1",
|
||||||
|
Schema: "public",
|
||||||
|
Entity: "users",
|
||||||
|
Options: &common.RequestOptions{
|
||||||
|
Filters: []common.FilterOption{
|
||||||
|
{Column: "status", Operator: "eq", Value: "active"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Active: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"id": 1,
|
||||||
|
"name": "John",
|
||||||
|
"status": "active",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current implementation returns true for all data
|
||||||
|
// This test documents the expected behavior
|
||||||
|
assert.True(t, sub.MatchesFilters(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscription_MatchesFilters_EmptyFiltersArray(t *testing.T) {
|
||||||
|
sub := &Subscription{
|
||||||
|
ID: "sub-1",
|
||||||
|
ConnectionID: "conn-1",
|
||||||
|
Schema: "public",
|
||||||
|
Entity: "users",
|
||||||
|
Options: &common.RequestOptions{
|
||||||
|
Filters: []common.FilterOption{},
|
||||||
|
},
|
||||||
|
Active: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"id": 1,
|
||||||
|
"name": "John",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should match when filters array is empty
|
||||||
|
assert.True(t, sub.MatchesFilters(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMakeEntityKey(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
schema string
|
||||||
|
entity string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "With schema",
|
||||||
|
schema: "public",
|
||||||
|
entity: "users",
|
||||||
|
expected: "public.users",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Without schema",
|
||||||
|
schema: "",
|
||||||
|
entity: "users",
|
||||||
|
expected: "users",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Different schema",
|
||||||
|
schema: "custom",
|
||||||
|
entity: "posts",
|
||||||
|
expected: "custom.posts",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := makeEntityKey(tt.schema, tt.entity)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscriptionManager_ConcurrentOperations(t *testing.T) {
|
||||||
|
sm := NewSubscriptionManager()
|
||||||
|
|
||||||
|
// This test verifies that concurrent operations don't cause race conditions
|
||||||
|
// Run with: go test -race
|
||||||
|
|
||||||
|
done := make(chan bool)
|
||||||
|
|
||||||
|
// Goroutine 1: Subscribe
|
||||||
|
go func() {
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
sm.Subscribe("sub-"+string(rune(i)), "conn-1", "public", "users", nil)
|
||||||
|
}
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Goroutine 2: Get subscriptions
|
||||||
|
go func() {
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
sm.GetSubscriptionsByEntity("public", "users")
|
||||||
|
}
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Goroutine 3: Count
|
||||||
|
go func() {
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
sm.Count()
|
||||||
|
}
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for all goroutines
|
||||||
|
<-done
|
||||||
|
<-done
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscriptionManager_CompleteLifecycle(t *testing.T) {
|
||||||
|
sm := NewSubscriptionManager()
|
||||||
|
|
||||||
|
// Create subscriptions
|
||||||
|
options := &common.RequestOptions{
|
||||||
|
Filters: []common.FilterOption{
|
||||||
|
{Column: "status", Operator: "eq", Value: "active"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sub1 := sm.Subscribe("sub-1", "conn-1", "public", "users", options)
|
||||||
|
require.NotNil(t, sub1)
|
||||||
|
assert.Equal(t, 1, sm.Count())
|
||||||
|
|
||||||
|
sub2 := sm.Subscribe("sub-2", "conn-1", "public", "posts", nil)
|
||||||
|
require.NotNil(t, sub2)
|
||||||
|
assert.Equal(t, 2, sm.Count())
|
||||||
|
|
||||||
|
// Get by entity
|
||||||
|
userSubs := sm.GetSubscriptionsByEntity("public", "users")
|
||||||
|
assert.Len(t, userSubs, 1)
|
||||||
|
assert.Equal(t, "sub-1", userSubs[0].ID)
|
||||||
|
|
||||||
|
// Get by connection
|
||||||
|
connSubs := sm.GetSubscriptionsByConnection("conn-1")
|
||||||
|
assert.Len(t, connSubs, 2)
|
||||||
|
|
||||||
|
// Get specific subscription
|
||||||
|
sub, exists := sm.GetSubscription("sub-1")
|
||||||
|
assert.True(t, exists)
|
||||||
|
assert.Equal(t, "sub-1", sub.ID)
|
||||||
|
assert.NotNil(t, sub.Options)
|
||||||
|
|
||||||
|
// Count by entity
|
||||||
|
assert.Equal(t, 1, sm.CountForEntity("public", "users"))
|
||||||
|
assert.Equal(t, 1, sm.CountForEntity("public", "posts"))
|
||||||
|
|
||||||
|
// Unsubscribe
|
||||||
|
ok := sm.Unsubscribe("sub-1")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, 1, sm.Count())
|
||||||
|
assert.Equal(t, 0, sm.CountForEntity("public", "users"))
|
||||||
|
|
||||||
|
// Verify subscription is gone
|
||||||
|
_, exists = sm.GetSubscription("sub-1")
|
||||||
|
assert.False(t, exists)
|
||||||
|
|
||||||
|
// Unsubscribe second subscription
|
||||||
|
ok = sm.Unsubscribe("sub-2")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, 0, sm.Count())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user