kra-oa/app/system/biz/task_test.go

198 lines
6.3 KiB
Go

package biz
import (
"context"
"encoding/json"
"errors"
"sync"
"testing"
"time"
)
type applicationTaskRepo struct {
mu sync.Mutex
deleted bool
deleteErr error
createdID uint
toggleCalls int
updateCalls int
createdCalls int
nameExists bool
nameExistsErr error
deleteObserved chan struct{}
}
func (r *applicationTaskRepo) CreateTask(_ context.Context, value *TimedTask) error {
r.mu.Lock()
defer r.mu.Unlock()
r.createdCalls++
if value.ID == 0 {
value.ID = r.createdID
}
return nil
}
func (r *applicationTaskRepo) UpdateTask(context.Context, *TimedTask) error {
r.mu.Lock()
defer r.mu.Unlock()
r.updateCalls++
return nil
}
func (r *applicationTaskRepo) DeleteTask(context.Context, uint) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.deleteErr != nil {
return r.deleteErr
}
r.deleted = true
if r.deleteObserved != nil {
close(r.deleteObserved)
}
return nil
}
func (r *applicationTaskRepo) FindTask(context.Context, uint) (*TimedTask, error) {
return nil, errors.New("not implemented")
}
func (r *applicationTaskRepo) ListTasks(context.Context, int, int, *TimedTask) ([]*TimedTask, int64, error) {
return nil, 0, nil
}
func (r *applicationTaskRepo) ToggleTask(context.Context, uint, bool) error {
r.mu.Lock()
defer r.mu.Unlock()
r.toggleCalls++
return nil
}
func (r *applicationTaskRepo) RecordTaskLog(context.Context, *TimedTaskLog) error { return nil }
func (r *applicationTaskRepo) ListTaskLogs(context.Context, int, int, uint, string) ([]*TimedTaskLog, int64, error) {
return nil, 0, nil
}
func (r *applicationTaskRepo) CleanupLogs(context.Context) error { return nil }
func (r *applicationTaskRepo) TaskNameExists(context.Context, string, uint) (bool, error) {
return r.nameExists, r.nameExistsErr
}
type applicationTaskRuntime struct {
mu sync.Mutex
scheduleErr error
reloadErr error
scheduleCalls int
reloadCalls int
removeCalls int
removeBeforeDelete bool
repo *applicationTaskRepo
reloadContextActive bool
}
func (r *applicationTaskRuntime) ScheduleID(context.Context, uint) error {
r.mu.Lock()
defer r.mu.Unlock()
r.scheduleCalls++
return r.scheduleErr
}
func (r *applicationTaskRuntime) Remove(uint) {
r.mu.Lock()
defer r.mu.Unlock()
r.removeCalls++
if r.repo != nil {
r.repo.mu.Lock()
r.removeBeforeDelete = !r.repo.deleted
r.repo.mu.Unlock()
}
}
func (r *applicationTaskRuntime) TriggerID(context.Context, uint) error { return nil }
func (r *applicationTaskRuntime) NextRuns() map[uint]time.Time { return nil }
func (r *applicationTaskRuntime) Reload(ctx context.Context) error {
r.mu.Lock()
defer r.mu.Unlock()
r.reloadCalls++
r.reloadContextActive = ctx.Err() == nil
return r.reloadErr
}
func (r *applicationTaskRuntime) Subscribe(uint) chan []byte { return make(chan []byte) }
func (r *applicationTaskRuntime) Unsubscribe(uint, chan []byte) {}
func validApplicationTask(id uint) *TimedTask {
const methodName = "biz-test-application-task"
RegisterTaskMethod(methodName, "test", func(context.Context, json.RawMessage) error { return nil })
return &TimedTask{ID: id, Name: "test", Spec: "0 0 * * *", ExecutorType: TaskExecutorMethod, MethodName: methodName, Enabled: true}
}
func TestTaskApplicationRepairsRuntimeAfterScheduleFailure(t *testing.T) {
repo := &applicationTaskRepo{createdID: 41}
runtime := &applicationTaskRuntime{scheduleErr: errors.New("schedule failed")}
uc := NewTaskApplicationUsecase(NewTaskUsecase(repo), runtime)
ctx, cancel := context.WithCancel(context.Background())
cancel()
if err := uc.Create(ctx, validApplicationTask(0)); err != nil {
t.Fatalf("Create() error = %v, want successful reload compensation", err)
}
if runtime.scheduleCalls != 1 || runtime.reloadCalls != 1 {
t.Fatalf("runtime calls = schedule:%d reload:%d, want 1 and 1", runtime.scheduleCalls, runtime.reloadCalls)
}
if !runtime.reloadContextActive {
t.Fatal("reload compensation inherited the canceled request context")
}
}
func TestTaskApplicationReturnsScheduleErrorWhenRepairFails(t *testing.T) {
repo := &applicationTaskRepo{createdID: 42}
runtime := &applicationTaskRuntime{scheduleErr: errors.New("schedule failed"), reloadErr: errors.New("reload failed")}
uc := NewTaskApplicationUsecase(NewTaskUsecase(repo), runtime)
err := uc.Create(context.Background(), validApplicationTask(0))
if !errors.Is(err, ErrTaskSchedule) {
t.Fatalf("Create() error = %v, want ErrTaskSchedule", err)
}
}
func TestTaskApplicationUpdateAndToggleUseRuntimeRepair(t *testing.T) {
for _, test := range []struct {
name string
run func(*TaskApplicationUsecase) error
}{
{name: "update", run: func(uc *TaskApplicationUsecase) error {
return uc.Update(context.Background(), validApplicationTask(7))
}},
{name: "toggle", run: func(uc *TaskApplicationUsecase) error { return uc.Toggle(context.Background(), 7, true) }},
} {
t.Run(test.name, func(t *testing.T) {
repo := &applicationTaskRepo{}
runtime := &applicationTaskRuntime{scheduleErr: errors.New("schedule failed")}
uc := NewTaskApplicationUsecase(NewTaskUsecase(repo), runtime)
if err := test.run(uc); err != nil {
t.Fatalf("operation error = %v", err)
}
if runtime.scheduleCalls != 1 || runtime.reloadCalls != 1 {
t.Fatalf("runtime calls = schedule:%d reload:%d, want 1 and 1", runtime.scheduleCalls, runtime.reloadCalls)
}
})
}
}
func TestTaskApplicationDeleteRemovesRuntimeAfterDatabase(t *testing.T) {
repo := &applicationTaskRepo{}
runtime := &applicationTaskRuntime{repo: repo}
uc := NewTaskApplicationUsecase(NewTaskUsecase(repo), runtime)
if err := uc.Delete(context.Background(), 8); err != nil {
t.Fatalf("Delete() error = %v", err)
}
if runtime.removeBeforeDelete {
t.Fatal("runtime entry was removed before the database row")
}
if runtime.removeCalls != 1 {
t.Fatalf("Remove() calls = %d, want 1", runtime.removeCalls)
}
}
func TestTaskApplicationDeleteKeepsRuntimeWhenDatabaseFails(t *testing.T) {
repo := &applicationTaskRepo{deleteErr: errors.New("delete failed")}
runtime := &applicationTaskRuntime{repo: repo}
uc := NewTaskApplicationUsecase(NewTaskUsecase(repo), runtime)
if err := uc.Delete(context.Background(), 8); err == nil {
t.Fatal("Delete() error = nil, want database error")
}
if runtime.removeCalls != 0 {
t.Fatalf("Remove() calls = %d, want 0", runtime.removeCalls)
}
}