diff options
Diffstat (limited to 'internal/store/chains_test.go')
| -rw-r--r-- | internal/store/chains_test.go | 165 |
1 files changed, 165 insertions, 0 deletions
diff --git a/internal/store/chains_test.go b/internal/store/chains_test.go new file mode 100644 index 0000000..6d3e450 --- /dev/null +++ b/internal/store/chains_test.go @@ -0,0 +1,165 @@ +package store + +import ( + "testing" +) + +func TestCreateChain_SeedsPositionsCorrectly(t *testing.T) { + s := newNativeTasksTestStore(t) + + chain, err := s.CreateChain("Ham Radio Track", []string{"Study Technician", "Pass Technician exam", "Study General"}) + if err != nil { + t.Fatalf("CreateChain: %v", err) + } + if chain.Status != "active" { + t.Errorf("chain.Status = %q, want active", chain.Status) + } + + tasks, err := s.GetChainTasks(chain.ID) + if err != nil { + t.Fatalf("GetChainTasks: %v", err) + } + if len(tasks) != 3 { + t.Fatalf("len(tasks) = %d, want 3", len(tasks)) + } + if !tasks[0].ChainUnlocked || tasks[0].DueDate == nil { + t.Errorf("position 0: ChainUnlocked=%v DueDate=%v, want unlocked with a due date", tasks[0].ChainUnlocked, tasks[0].DueDate) + } + for i := 1; i < 3; i++ { + if tasks[i].ChainUnlocked || tasks[i].DueDate != nil { + t.Errorf("position %d: ChainUnlocked=%v DueDate=%v, want locked with no due date", i, tasks[i].ChainUnlocked, tasks[i].DueDate) + } + } + if tasks[0].Content != "Study Technician" || tasks[1].Content != "Pass Technician exam" || tasks[2].Content != "Study General" { + t.Errorf("unexpected content order: %q, %q, %q", tasks[0].Content, tasks[1].Content, tasks[2].Content) + } +} + +func TestCompleteNativeTask_AdvancesChain(t *testing.T) { + s := newNativeTasksTestStore(t) + + chain, err := s.CreateChain("Track", []string{"Step 1", "Step 2"}) + if err != nil { + t.Fatal(err) + } + tasks, err := s.GetChainTasks(chain.ID) + if err != nil { + t.Fatal(err) + } + + if err := s.CompleteNativeTask(tasks[0].ID); err != nil { + t.Fatalf("CompleteNativeTask: %v", err) + } + + after, err := s.GetChainTasks(chain.ID) + if err != nil { + t.Fatal(err) + } + if !after[1].ChainUnlocked || after[1].DueDate == nil { + t.Errorf("position 1 after completing position 0: ChainUnlocked=%v DueDate=%v, want unlocked with a due date", after[1].ChainUnlocked, after[1].DueDate) + } + + updatedChain, err := s.GetChain(chain.ID) + if err != nil { + t.Fatal(err) + } + if updatedChain.Status != "active" { + t.Errorf("chain.Status = %q, want active (not yet done)", updatedChain.Status) + } +} + +func TestCompleteNativeTask_LastPosition_MarksChainCompleted(t *testing.T) { + s := newNativeTasksTestStore(t) + + chain, err := s.CreateChain("Track", []string{"Only step"}) + if err != nil { + t.Fatal(err) + } + tasks, err := s.GetChainTasks(chain.ID) + if err != nil { + t.Fatal(err) + } + + if err := s.CompleteNativeTask(tasks[0].ID); err != nil { + t.Fatalf("CompleteNativeTask: %v", err) + } + + updatedChain, err := s.GetChain(chain.ID) + if err != nil { + t.Fatal(err) + } + if updatedChain.Status != "completed" { + t.Errorf("chain.Status = %q, want completed", updatedChain.Status) + } +} + +func TestCompleteNativeTask_PausedChain_DoesNotAdvance(t *testing.T) { + s := newNativeTasksTestStore(t) + + chain, err := s.CreateChain("Track", []string{"Step 1", "Step 2"}) + if err != nil { + t.Fatal(err) + } + if err := s.SetChainStatus(chain.ID, "paused"); err != nil { + t.Fatal(err) + } + tasks, err := s.GetChainTasks(chain.ID) + if err != nil { + t.Fatal(err) + } + + if err := s.CompleteNativeTask(tasks[0].ID); err != nil { + t.Fatalf("CompleteNativeTask: %v", err) + } + + after, err := s.GetChainTasks(chain.ID) + if err != nil { + t.Fatal(err) + } + if after[1].ChainUnlocked { + t.Error("position 1 should still be locked while chain is paused") + } + + // Resuming re-enables advancement on the *next* completion. + if err := s.SetChainStatus(chain.ID, "active"); err != nil { + t.Fatal(err) + } + if err := s.CompleteNativeTask(tasks[1].ID); err != nil { + t.Fatalf("CompleteNativeTask after resume: %v", err) + } + updatedChain, err := s.GetChain(chain.ID) + if err != nil { + t.Fatal(err) + } + if updatedChain.Status != "completed" { + t.Errorf("chain.Status = %q, want completed after resuming and finishing the last step", updatedChain.Status) + } +} + +func TestGetUndatedNativeTasks_ExcludesLockedChainTasks(t *testing.T) { + s := newNativeTasksTestStore(t) + + chain, err := s.CreateChain("Track", []string{"Step 1", "Step 2", "Step 3"}) + if err != nil { + t.Fatal(err) + } + _ = chain + + undated, err := s.GetUndatedNativeTasks() + if err != nil { + t.Fatalf("GetUndatedNativeTasks: %v", err) + } + for _, task := range undated { + if task.ChainID != "" && !task.ChainUnlocked { + t.Errorf("locked chain task %q leaked into GetUndatedNativeTasks", task.ID) + } + } +} + +func TestGetChain_UnknownID_ReturnsErrNotFound(t *testing.T) { + s := newNativeTasksTestStore(t) + + if _, err := s.GetChain("does-not-exist"); err != ErrNativeTaskNotFound { + t.Errorf("err = %v, want ErrNativeTaskNotFound", err) + } +} |
