198 lines
6.3 KiB
Go
198 lines
6.3 KiB
Go
package task
|
|
|
|
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)
|
|
}
|
|
}
|