571 lines
12 KiB
Go
571 lines
12 KiB
Go
package taskq
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// TestNewServlet tests Servlet creation
|
|
func TestNewServlet(t *testing.T) {
|
|
s := NewServlet()
|
|
assert.NotNil(t, s)
|
|
assert.NotNil(t, s.handlers)
|
|
assert.NotNil(t, s.queues)
|
|
assert.NotNil(t, s.exit)
|
|
assert.Equal(t, 0, len(s.handlers))
|
|
assert.Equal(t, 0, len(s.queues))
|
|
}
|
|
|
|
// TestConfigureMissingRedis tests Configure without Redis client
|
|
func TestConfigureMissingRedis(t *testing.T) {
|
|
s := NewServlet()
|
|
cfg := Config{
|
|
Redis: nil,
|
|
Tasks: []*Task{},
|
|
}
|
|
err := s.Configure(cfg)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "taskq: redis client is required", err.Error())
|
|
}
|
|
|
|
// TestConfigureValidTask tests successful configuration with valid task
|
|
func TestConfigureValidTask(t *testing.T) {
|
|
rdb := getTestRedis(t)
|
|
defer rdb.Close()
|
|
|
|
s := NewServlet()
|
|
task := &Task{
|
|
Name: "test_task",
|
|
Queue: "default",
|
|
Priority: 1,
|
|
MaxRetries: 3,
|
|
Handler: func(ctx context.Context, data struct{ Value string }) error {
|
|
return nil
|
|
},
|
|
}
|
|
|
|
cfg := Config{
|
|
Redis: rdb,
|
|
Tasks: []*Task{task},
|
|
}
|
|
|
|
err := s.Configure(cfg)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, s.client)
|
|
assert.NotNil(t, s.redisClient)
|
|
assert.Equal(t, 1, len(s.handlers))
|
|
assert.Equal(t, 1, len(s.queues))
|
|
}
|
|
|
|
// TestRegisterTaskEmptyQueue tests task registration with empty queue name
|
|
func TestRegisterTaskEmptyQueue(t *testing.T) {
|
|
rdb := getTestRedis(t)
|
|
defer rdb.Close()
|
|
|
|
s := NewServlet()
|
|
task := &Task{
|
|
Name: "test_task",
|
|
Queue: "",
|
|
Handler: func() error { return nil },
|
|
}
|
|
|
|
cfg := Config{
|
|
Redis: rdb,
|
|
Tasks: []*Task{task},
|
|
}
|
|
|
|
err := s.Configure(cfg)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "taskq: queue name cannot be empty", err.Error())
|
|
}
|
|
|
|
// TestRegisterTaskInvalidPriority tests task registration with invalid priority
|
|
func TestRegisterTaskInvalidPriority(t *testing.T) {
|
|
rdb := getTestRedis(t)
|
|
defer rdb.Close()
|
|
|
|
s := NewServlet()
|
|
task := &Task{
|
|
Name: "test_task",
|
|
Queue: "default",
|
|
Priority: 256,
|
|
Handler: func() error { return nil },
|
|
}
|
|
|
|
cfg := Config{
|
|
Redis: rdb,
|
|
Tasks: []*Task{task},
|
|
}
|
|
|
|
err := s.Configure(cfg)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "priority must be between 0 and 255")
|
|
}
|
|
|
|
// TestRegisterTaskNegativeRetry tests task registration with negative retry count
|
|
func TestRegisterTaskNegativeRetry(t *testing.T) {
|
|
rdb := getTestRedis(t)
|
|
defer rdb.Close()
|
|
|
|
s := NewServlet()
|
|
task := &Task{
|
|
Name: "test_task",
|
|
Queue: "default",
|
|
MaxRetries: -1,
|
|
Handler: func() error { return nil },
|
|
}
|
|
|
|
cfg := Config{
|
|
Redis: rdb,
|
|
Tasks: []*Task{task},
|
|
}
|
|
|
|
err := s.Configure(cfg)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "taskq: retry count must be non-negative", err.Error())
|
|
}
|
|
|
|
// TestRegisterTaskNilHandler tests task registration with nil handler
|
|
func TestRegisterTaskNilHandler(t *testing.T) {
|
|
rdb := getTestRedis(t)
|
|
defer rdb.Close()
|
|
|
|
s := NewServlet()
|
|
task := &Task{
|
|
Name: "test_task",
|
|
Queue: "default",
|
|
Handler: nil,
|
|
}
|
|
|
|
cfg := Config{
|
|
Redis: rdb,
|
|
Tasks: []*Task{task},
|
|
}
|
|
|
|
err := s.Configure(cfg)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "taskq: handler cannot be nil", err.Error())
|
|
}
|
|
|
|
// TestRegisterTaskNotFunction tests task registration with non-function handler
|
|
func TestRegisterTaskNotFunction(t *testing.T) {
|
|
rdb := getTestRedis(t)
|
|
defer rdb.Close()
|
|
|
|
s := NewServlet()
|
|
task := &Task{
|
|
Name: "test_task",
|
|
Queue: "default",
|
|
Handler: "not_a_function",
|
|
}
|
|
|
|
cfg := Config{
|
|
Redis: rdb,
|
|
Tasks: []*Task{task},
|
|
}
|
|
|
|
err := s.Configure(cfg)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "taskq: handler must be a function", err.Error())
|
|
}
|
|
|
|
// TestRegisterTaskInvalidReturn tests handler with invalid return value
|
|
func TestRegisterTaskInvalidReturn(t *testing.T) {
|
|
rdb := getTestRedis(t)
|
|
defer rdb.Close()
|
|
|
|
s := NewServlet()
|
|
task := &Task{
|
|
Name: "test_task",
|
|
Queue: "default",
|
|
Handler: func() string { return "invalid" },
|
|
}
|
|
|
|
cfg := Config{
|
|
Redis: rdb,
|
|
Tasks: []*Task{task},
|
|
}
|
|
|
|
err := s.Configure(cfg)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "must return either error or nothing")
|
|
}
|
|
|
|
// TestRegisterTaskTooManyParams tests handler with more than 2 parameters
|
|
func TestRegisterTaskTooManyParams(t *testing.T) {
|
|
rdb := getTestRedis(t)
|
|
defer rdb.Close()
|
|
|
|
s := NewServlet()
|
|
task := &Task{
|
|
Name: "test_task",
|
|
Queue: "default",
|
|
Handler: func(ctx context.Context, a string, b string) error { return nil },
|
|
}
|
|
|
|
cfg := Config{
|
|
Redis: rdb,
|
|
Tasks: []*Task{task},
|
|
}
|
|
|
|
err := s.Configure(cfg)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "taskq: handler function can have at most 2 parameters", err.Error())
|
|
}
|
|
|
|
// TestRegisterTaskContextNotFirst tests handler with context not as first parameter
|
|
func TestRegisterTaskContextNotFirst(t *testing.T) {
|
|
rdb := getTestRedis(t)
|
|
defer rdb.Close()
|
|
|
|
s := NewServlet()
|
|
task := &Task{
|
|
Name: "test_task",
|
|
Queue: "default",
|
|
Handler: func(a struct{ Value string }, ctx context.Context) error { return nil },
|
|
}
|
|
|
|
cfg := Config{
|
|
Redis: rdb,
|
|
Tasks: []*Task{task},
|
|
}
|
|
|
|
err := s.Configure(cfg)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "taskq: context.Context must be the first parameter", err.Error())
|
|
}
|
|
|
|
// TestRegisterTaskHandlerSignatures tests various valid handler signatures
|
|
func TestRegisterTaskHandlerSignatures(t *testing.T) {
|
|
rdb := getTestRedis(t)
|
|
defer rdb.Close()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
handler interface{}
|
|
valid bool
|
|
}{
|
|
{
|
|
name: "no_params",
|
|
handler: func() error { return nil },
|
|
valid: true,
|
|
},
|
|
{
|
|
name: "context_only",
|
|
handler: func(ctx context.Context) error { return nil },
|
|
valid: true,
|
|
},
|
|
{
|
|
name: "data_only",
|
|
handler: func(data struct{ Value string }) error { return nil },
|
|
valid: true,
|
|
},
|
|
{
|
|
name: "context_and_data",
|
|
handler: func(ctx context.Context, data struct{ Value string }) error { return nil },
|
|
valid: true,
|
|
},
|
|
{
|
|
name: "no_error_return",
|
|
handler: func() {},
|
|
valid: true,
|
|
},
|
|
{
|
|
name: "invalid_param",
|
|
handler: func(a int) error { return nil },
|
|
valid: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
s := NewServlet()
|
|
task := &Task{
|
|
Name: "test_task",
|
|
Queue: "default",
|
|
Handler: tc.handler,
|
|
}
|
|
|
|
cfg := Config{
|
|
Redis: rdb,
|
|
Tasks: []*Task{task},
|
|
}
|
|
|
|
err := s.Configure(cfg)
|
|
if tc.valid {
|
|
assert.NoError(t, err)
|
|
} else {
|
|
assert.Error(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestQueuePriority tests queue priority assignment
|
|
func TestQueuePriority(t *testing.T) {
|
|
rdb := getTestRedis(t)
|
|
defer rdb.Close()
|
|
|
|
s := NewServlet()
|
|
task1 := &Task{
|
|
Name: "task1",
|
|
Queue: "high",
|
|
Priority: 10,
|
|
Handler: func() error { return nil },
|
|
}
|
|
task2 := &Task{
|
|
Name: "task2",
|
|
Queue: "low",
|
|
Priority: 1,
|
|
Handler: func() error { return nil },
|
|
}
|
|
|
|
cfg := Config{
|
|
Redis: rdb,
|
|
Tasks: []*Task{task1, task2},
|
|
}
|
|
|
|
err := s.Configure(cfg)
|
|
assert.NoError(t, err)
|
|
|
|
queues := s.Queues()
|
|
assert.Equal(t, 10, queues["high"])
|
|
assert.Equal(t, 1, queues["low"])
|
|
}
|
|
|
|
// TestClient tests Client method
|
|
func TestClient(t *testing.T) {
|
|
rdb := getTestRedis(t)
|
|
defer rdb.Close()
|
|
|
|
s := NewServlet()
|
|
cfg := Config{
|
|
Redis: rdb,
|
|
Tasks: []*Task{},
|
|
}
|
|
|
|
err := s.Configure(cfg)
|
|
assert.NoError(t, err)
|
|
|
|
client := s.Client()
|
|
assert.NotNil(t, client)
|
|
}
|
|
|
|
// TestRedisClient tests RedisClient method
|
|
func TestRedisClient(t *testing.T) {
|
|
rdb := getTestRedis(t)
|
|
defer rdb.Close()
|
|
|
|
s := NewServlet()
|
|
cfg := Config{
|
|
Redis: rdb,
|
|
Tasks: []*Task{},
|
|
}
|
|
|
|
err := s.Configure(cfg)
|
|
assert.NoError(t, err)
|
|
|
|
redisClient := s.RedisClient()
|
|
assert.NotNil(t, redisClient)
|
|
assert.Equal(t, rdb, redisClient)
|
|
}
|
|
|
|
// TestQueues tests Queues method returns a copy
|
|
func TestQueues(t *testing.T) {
|
|
rdb := getTestRedis(t)
|
|
defer rdb.Close()
|
|
|
|
s := NewServlet()
|
|
task := &Task{
|
|
Name: "task1",
|
|
Queue: "default",
|
|
Priority: 5,
|
|
Handler: func() error { return nil },
|
|
}
|
|
|
|
cfg := Config{
|
|
Redis: rdb,
|
|
Tasks: []*Task{task},
|
|
}
|
|
|
|
err := s.Configure(cfg)
|
|
assert.NoError(t, err)
|
|
|
|
queues1 := s.Queues()
|
|
queues1["modified"] = 999
|
|
|
|
queues2 := s.Queues()
|
|
assert.NotContains(t, queues2, "modified")
|
|
}
|
|
|
|
// TestInitWithoutPlugins tests Init without plugins
|
|
func TestInitWithoutPlugins(t *testing.T) {
|
|
rdb := getTestRedis(t)
|
|
defer rdb.Close()
|
|
|
|
s := NewServlet()
|
|
cfg := Config{
|
|
Redis: rdb,
|
|
Tasks: []*Task{},
|
|
}
|
|
|
|
err := s.Configure(cfg)
|
|
assert.NoError(t, err)
|
|
|
|
ctx := context.Background()
|
|
err = s.Init(ctx)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
// TestInitWithPlugins tests Init with plugins
|
|
func TestInitWithPlugins(t *testing.T) {
|
|
rdb := getTestRedis(t)
|
|
defer rdb.Close()
|
|
|
|
mockPlugin := &MockPlugin{}
|
|
s := NewServlet()
|
|
cfg := Config{
|
|
Redis: rdb,
|
|
Tasks: []*Task{},
|
|
Plugins: []Plugin{mockPlugin},
|
|
}
|
|
|
|
err := s.Configure(cfg)
|
|
assert.NoError(t, err)
|
|
|
|
ctx := context.Background()
|
|
err = s.Init(ctx)
|
|
assert.NoError(t, err)
|
|
assert.True(t, mockPlugin.initCalled)
|
|
}
|
|
|
|
// TestStartStop tests Start and Stop lifecycle
|
|
func TestStartStop(t *testing.T) {
|
|
// Skip this test as Start creates blocking goroutines
|
|
t.Skip("Start creates blocking goroutines that don't clean up properly")
|
|
}
|
|
|
|
// TestStartWithPlugins tests Start with plugins
|
|
func TestStartWithPlugins(t *testing.T) {
|
|
// Skip this test as Start creates blocking goroutines
|
|
t.Skip("Start creates blocking goroutines that don't clean up properly")
|
|
}
|
|
|
|
// TestMultipleTasks tests registering multiple tasks
|
|
func TestMultipleTasks(t *testing.T) {
|
|
rdb := getTestRedis(t)
|
|
defer rdb.Close()
|
|
|
|
s := NewServlet()
|
|
tasks := []*Task{
|
|
{
|
|
Name: "task1",
|
|
Queue: "default",
|
|
Handler: func() error { return nil },
|
|
},
|
|
{
|
|
Name: "task2",
|
|
Queue: "high",
|
|
Priority: 10,
|
|
Handler: func(ctx context.Context) error { return nil },
|
|
},
|
|
{
|
|
Name: "task3",
|
|
Queue: "low",
|
|
Priority: 1,
|
|
Handler: func(data struct{ Value string }) error { return nil },
|
|
},
|
|
}
|
|
|
|
cfg := Config{
|
|
Redis: rdb,
|
|
Tasks: tasks,
|
|
}
|
|
|
|
err := s.Configure(cfg)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 3, len(s.handlers))
|
|
assert.Equal(t, 3, len(s.queues))
|
|
}
|
|
|
|
// MockPlugin for testing plugin lifecycle
|
|
type MockPlugin struct {
|
|
initCalled bool
|
|
startCalled bool
|
|
stopCalled bool
|
|
}
|
|
|
|
func (mp *MockPlugin) Name() string {
|
|
return "MockPlugin"
|
|
}
|
|
|
|
func (mp *MockPlugin) Init(ctx *Context) error {
|
|
mp.initCalled = true
|
|
return nil
|
|
}
|
|
|
|
func (mp *MockPlugin) Start(ctx *Context) error {
|
|
mp.startCalled = true
|
|
return nil
|
|
}
|
|
|
|
func (mp *MockPlugin) Stop() error {
|
|
mp.stopCalled = true
|
|
return nil
|
|
}
|
|
|
|
// FailingPlugin for testing error handling
|
|
type FailingPlugin struct {
|
|
stage string // "init" or "start"
|
|
}
|
|
|
|
func (fp *FailingPlugin) Name() string {
|
|
return "FailingPlugin"
|
|
}
|
|
|
|
func (fp *FailingPlugin) Init(ctx *Context) error {
|
|
if fp.stage == "init" {
|
|
return errors.New("init failed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (fp *FailingPlugin) Start(ctx *Context) error {
|
|
if fp.stage == "start" {
|
|
return errors.New("start failed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (fp *FailingPlugin) Stop() error {
|
|
return nil
|
|
}
|
|
|
|
// getTestRedis returns a real Redis client for testing
|
|
func getTestRedis(t *testing.T) redis.UniversalClient {
|
|
rdb := redis.NewClient(&redis.Options{
|
|
Addr: "localhost:6379",
|
|
DB: 15, // Use database 15 for testing to avoid data loss
|
|
})
|
|
|
|
// Test connection
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
if err := rdb.Ping(ctx).Err(); err != nil {
|
|
t.Skipf("Redis is not running on localhost:6379: %v", err)
|
|
}
|
|
|
|
// Clean up test database before test
|
|
if err := rdb.FlushDB(context.Background()).Err(); err != nil {
|
|
t.Fatalf("Failed to flush test database: %v", err)
|
|
}
|
|
|
|
return rdb
|
|
}
|