Files
ResolveSpec/pkg/websocketspec/hooks_test.go
2025-12-23 17:27:29 +02:00

548 lines
13 KiB
Go

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)
}