Files
OpenFlare/backend/core/events_test.go
T

397 lines
9.9 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package core_test
import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"Wavelet/core"
)
type UserRegisteredEvent struct {
UserID string `json:"user_id"`
Username string `json:"username"`
}
type OrderCreatedEvent struct {
OrderID string `json:"order_id"`
Amount float64 `json:"amount"`
}
func TestEventBusPublishSubscribe(t *testing.T) {
bus := core.NewEventBus()
var receivedID string
disposer := bus.On("user:registered", func(ctx context.Context, e UserRegisteredEvent) error {
receivedID = e.UserID
return nil
})
require.NotNil(t, disposer)
assert.Equal(t, []string{"user:registered"}, bus.Topics())
err := bus.Emit(context.Background(), "user:registered", UserRegisteredEvent{UserID: "u_999", Username: "alice"})
assert.NoError(t, err)
assert.Equal(t, "u_999", receivedID)
// Emit to empty topic returns nil error
err = bus.Emit(nil, "empty:topic", nil)
assert.NoError(t, err)
}
func TestEventBusGenericSubscribe(t *testing.T) {
bus := core.NewEventBus()
var receivedOrder string
assert.Panics(t, func() {
core.Subscribe[OrderCreatedEvent](nil, "order:created", func(ctx context.Context, e OrderCreatedEvent) error {
return nil
})
})
disposer := core.Subscribe(bus, "order:created", func(ctx context.Context, e OrderCreatedEvent) error {
receivedOrder = e.OrderID
return nil
})
require.NotNil(t, disposer)
err := bus.Emit(context.Background(), "order:created", OrderCreatedEvent{OrderID: "ord_123", Amount: 99.5})
assert.NoError(t, err)
assert.Equal(t, "ord_123", receivedOrder)
// Test unsubscribe via disposer
err = disposer()
assert.NoError(t, err)
receivedOrder = ""
err = bus.Emit(context.Background(), "order:created", OrderCreatedEvent{OrderID: "ord_456", Amount: 100})
assert.NoError(t, err)
assert.Empty(t, receivedOrder, "handler should not be called after disposal")
}
func TestEventBusHandlerSignatures(t *testing.T) {
bus := core.NewEventBus()
var (
calledWithCtxPayloadErr atomic.Bool
calledWithCtxPayload atomic.Bool
calledWithPayloadErr atomic.Bool
calledWithPayload atomic.Bool
calledWithCtxErr atomic.Bool
calledWithCtx atomic.Bool
calledWithNoArgsErr atomic.Bool
calledWithNoArgs atomic.Bool
)
bus.On("test:sig", func(ctx context.Context, e UserRegisteredEvent) error {
calledWithCtxPayloadErr.Store(true)
assert.Equal(t, "u_1", e.UserID)
return nil
})
bus.On("test:sig", func(ctx context.Context, e UserRegisteredEvent) {
calledWithCtxPayload.Store(true)
assert.Equal(t, "u_1", e.UserID)
})
bus.On("test:sig", func(e UserRegisteredEvent) error {
calledWithPayloadErr.Store(true)
assert.Equal(t, "u_1", e.UserID)
return nil
})
bus.On("test:sig", func(e UserRegisteredEvent) {
calledWithPayload.Store(true)
assert.Equal(t, "u_1", e.UserID)
})
bus.On("test:sig", func(ctx context.Context) error {
calledWithCtxErr.Store(true)
return nil
})
bus.On("test:sig", func(ctx context.Context) {
calledWithCtx.Store(true)
})
bus.On("test:sig", func() error {
calledWithNoArgsErr.Store(true)
return nil
})
bus.On("test:sig", func() {
calledWithNoArgs.Store(true)
})
err := bus.Emit(context.Background(), "test:sig", UserRegisteredEvent{UserID: "u_1", Username: "test"})
assert.NoError(t, err)
assert.True(t, calledWithCtxPayloadErr.Load())
assert.True(t, calledWithCtxPayload.Load())
assert.True(t, calledWithPayloadErr.Load())
assert.True(t, calledWithPayload.Load())
assert.True(t, calledWithCtxErr.Load())
assert.True(t, calledWithCtx.Load())
assert.True(t, calledWithNoArgsErr.Load())
assert.True(t, calledWithNoArgs.Load())
}
func TestEventBusPointerAndValueConversion(t *testing.T) {
bus := core.NewEventBus()
var (
receivedFromValueToPtr atomic.Bool
receivedFromPtrToValue atomic.Bool
receivedFromNilPtr atomic.Bool
)
// Handler expects pointer, payload emitted as value
bus.On("test:ptr", func(ctx context.Context, e *UserRegisteredEvent) error {
if e != nil && e.UserID == "u_ptr" {
receivedFromValueToPtr.Store(true)
}
return nil
})
err := bus.Emit(context.Background(), "test:ptr", UserRegisteredEvent{UserID: "u_ptr"})
assert.NoError(t, err)
assert.True(t, receivedFromValueToPtr.Load())
// Handler expects value, payload emitted as pointer
bus.On("test:val", func(ctx context.Context, e UserRegisteredEvent) error {
if e.UserID == "u_val" {
receivedFromPtrToValue.Store(true)
}
return nil
})
err = bus.Emit(context.Background(), "test:val", &UserRegisteredEvent{UserID: "u_val"})
assert.NoError(t, err)
assert.True(t, receivedFromPtrToValue.Load())
// Handler expects value, payload is nil pointer
var nilEvent *UserRegisteredEvent
bus.On("test:nil_ptr", func(ctx context.Context, e UserRegisteredEvent) error {
assert.Equal(t, "", e.UserID)
receivedFromNilPtr.Store(true)
return nil
})
err = bus.Emit(context.Background(), "test:nil_ptr", nilEvent)
assert.NoError(t, err)
assert.True(t, receivedFromNilPtr.Load())
// Convertible type test (int to int64)
var receivedConvert int64
bus.On("test:conv", func(e int64) {
receivedConvert = e
})
err = bus.Emit(context.Background(), "test:conv", int(42))
assert.NoError(t, err)
assert.Equal(t, int64(42), receivedConvert)
}
func TestEventBusErrorCollectionAndPanicRecovery(t *testing.T) {
bus := core.NewEventBus()
errHandler1 := errors.New("handler 1 failed")
errHandler2 := errors.New("handler 2 failed")
bus.On("test:err", func() error {
return errHandler1
})
bus.On("test:err", func() {
panic("something went horribly wrong")
})
bus.On("test:err", func() error {
return errHandler2
})
err := bus.Emit(context.Background(), "test:err", nil)
require.Error(t, err)
assert.True(t, errors.Is(err, errHandler1) || errors.Is(err, errHandler2))
assert.Contains(t, err.Error(), "handler 1 failed")
assert.Contains(t, err.Error(), "handler 2 failed")
assert.Contains(t, err.Error(), "panic")
}
func TestEventBusInvalidHandlerPanics(t *testing.T) {
bus := core.NewEventBus()
assert.Panics(t, func() {
bus.On("test:invalid", nil)
})
assert.Panics(t, func() {
bus.On("test:invalid", "not-a-func")
})
assert.Panics(t, func() {
// More than 2 arguments
bus.On("test:invalid", func(a, b, c string) {})
})
assert.Panics(t, func() {
// 2 args, but first is not context
bus.On("test:invalid", func(a string, b int) {})
})
assert.Panics(t, func() {
// More than 2 return values
bus.On("test:invalid", func() (int, string, error) { return 0, "", nil })
})
assert.Panics(t, func() {
// 2 return values, but second is not error
bus.On("test:invalid", func() (int, string) { return 0, "" })
})
}
func TestEventBusListenersCountAndDisposerIdempotence(t *testing.T) {
bus := core.NewEventBus()
assert.Equal(t, 0, bus.Listeners("topic1"))
d1 := bus.On("topic1", func() {})
d2 := bus.On("topic1", func() {})
assert.Equal(t, 2, bus.Listeners("topic1"))
_ = d1()
assert.Equal(t, 1, bus.Listeners("topic1"))
// Calling disposer again should be no-op
_ = d1()
assert.Equal(t, 1, bus.Listeners("topic1"))
_ = d2()
assert.Equal(t, 0, bus.Listeners("topic1"))
}
func TestEventBusConcurrentAccess(t *testing.T) {
bus := core.NewEventBus()
var wg sync.WaitGroup
var receivedCount atomic.Int64
// Concurrently subscribe and emit
for i := 0; i < 50; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
topic := fmt.Sprintf("topic:%d", idx%5)
disposer := bus.On(topic, func(ctx context.Context, e UserRegisteredEvent) error {
receivedCount.Add(1)
return nil
})
// Emit some events
_ = bus.Emit(context.Background(), topic, UserRegisteredEvent{UserID: fmt.Sprintf("u_%d", idx)})
// Randomly dispose
if idx%2 == 0 {
_ = disposer()
}
}(i)
}
for i := 0; i < 50; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
topic := fmt.Sprintf("topic:%d", idx%5)
_ = bus.Emit(context.Background(), topic, UserRegisteredEvent{UserID: fmt.Sprintf("u_%d", idx)})
}(i)
}
wg.Wait()
assert.Greater(t, receivedCount.Load(), int64(0))
}
func TestEventBusWaterfall(t *testing.T) {
bus := core.NewEventBus()
// Handler 1: appends "-first"
bus.On("pipeline:transform", func(ctx context.Context, s string) string {
return s + "-first"
})
// Handler 2: appends "-second" with error return
bus.On("pipeline:transform", func(s string) (string, error) {
return s + "-second", nil
})
res, err := bus.Waterfall(context.Background(), "pipeline:transform", "init")
assert.NoError(t, err)
assert.Equal(t, "init-first-second", res)
// Test short-circuit on error
expectedErr := errors.New("waterfall step failed")
bus.On("pipeline:error", func(s string) (string, error) {
return s, expectedErr
})
bus.On("pipeline:error", func(s string) string {
return s + "-should-not-run"
})
res, err = bus.Waterfall(context.Background(), "pipeline:error", "start")
assert.ErrorIs(t, err, expectedErr)
assert.Nil(t, res)
}
func TestEventBusParallel(t *testing.T) {
bus := core.NewEventBus()
var counter atomic.Int64
err1 := errors.New("parallel err 1")
bus.On("test:parallel", func(ctx context.Context, val int) error {
counter.Add(int64(val))
return nil
})
bus.On("test:parallel", func(val int) error {
counter.Add(int64(val))
return err1
})
err := bus.Parallel(context.Background(), "test:parallel", 10)
assert.ErrorIs(t, err, err1)
assert.Equal(t, int64(20), counter.Load())
}
func TestEventBusSerial(t *testing.T) {
bus := core.NewEventBus()
var executed []int
errStop := errors.New("serial stop")
bus.On("test:serial", func() error {
executed = append(executed, 1)
return nil
})
bus.On("test:serial", func() error {
executed = append(executed, 2)
return errStop
})
bus.On("test:serial", func() error {
executed = append(executed, 3)
return nil
})
err := bus.Serial(context.Background(), "test:serial", nil)
assert.ErrorIs(t, err, errStop)
assert.Equal(t, []int{1, 2}, executed)
}