mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-30 00:04:25 +00:00
548 lines
13 KiB
Go
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)
|
|
}
|