summaryrefslogtreecommitdiff
path: root/internal/store/chains_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/store/chains_test.go')
-rw-r--r--internal/store/chains_test.go165
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)
+ }
+}