package classes_test import ( "testing" "time" "git.golem15.com/golem15/sm-quizzes-plugin/classes" "git.golem15.com/golem15/sm-quizzes-plugin/models" "git.golem15.com/golem15/summercms/modules/compass" ) func TestIsTestAndWithheld(t *testing.T) { if classes.IsTest(nil) { t.Error("IsTest(nil) = true") } for mode, want := range map[string]bool{"": false, models.ModeLearning: false, "exam": false, models.ModeTest: true} { if got := classes.IsTest(&models.Quiz{Mode: mode}); got != want { t.Errorf("IsTest(mode %q) = %v, want %v", mode, got, want) } } done := time.Now() test, learning := &models.Quiz{Mode: models.ModeTest}, &models.Quiz{Mode: models.ModeLearning} cases := []struct { name string a *models.Attempt want bool }{ {"open test", &models.Attempt{Quiz: test}, true}, {"completed test", &models.Attempt{Quiz: test, CompletedAt: &done}, false}, {"open learning", &models.Attempt{Quiz: learning}, false}, {"zero mode", &models.Attempt{Quiz: &models.Quiz{}}, false}, {"nil attempt", nil, false}, {"nil quiz", &models.Attempt{}, false}, } for _, tc := range cases { if got := classes.Withheld(tc.a); got != tc.want { t.Errorf("Withheld(%s) = %v, want %v", tc.name, got, tc.want) } } } func TestTestRules(t *testing.T) { limit, two := 60, 2 start := time.Date(2026, 11, 9, 10, 0, 0, 0, time.UTC) test := &models.Quiz{Mode: models.ModeTest, TimeLimitSeconds: &limit} learning := &models.Quiz{Mode: models.ModeLearning, TimeLimitSeconds: &limit} grace := 5 * time.Second open := &models.Attempt{StartedAt: start, Quiz: test} deadline := start.Add(60 * time.Second) if d := classes.AnswerDeadline(open, grace); d == nil || !d.Equal(deadline.Add(grace)) { t.Errorf("test AnswerDeadline = %v, want deadline + grace", d) } if d := classes.AnswerDeadline(&models.Attempt{StartedAt: start, Quiz: learning}, grace); d == nil || !d.Equal(deadline) { t.Errorf("learning AnswerDeadline = %v, want the deadline itself", d) } if classes.AnswerDeadline(&models.Attempt{StartedAt: start, Quiz: &models.Quiz{Mode: models.ModeTest}}, grace) != nil { t.Error("AnswerDeadline without a time limit is set") } if classes.Expired(open, deadline.Add(4*time.Second), grace) { t.Error("test attempt expired inside the grace") } if !classes.Expired(open, deadline.Add(6*time.Second), grace) { t.Error("test attempt past deadline + grace did not expire") } if classes.Expired(&models.Attempt{StartedAt: start, Quiz: learning}, deadline.Add(time.Hour), grace) { t.Error("learning attempt expired") } done := deadline if classes.Expired(&models.Attempt{StartedAt: start, Quiz: test, CompletedAt: &done}, deadline.Add(time.Hour), grace) { t.Error("completed attempt expired") } if classes.Expired(&models.Attempt{StartedAt: start, Quiz: &models.Quiz{Mode: models.ModeTest}}, deadline.Add(time.Hour), grace) { t.Error("attempt without a time limit expired") } if got := classes.SecondsLeft(open, start.Add(500*time.Millisecond)); got == nil || *got != 60 { t.Errorf("SecondsLeft at +0.5s = %v, want 60 (rounded up)", got) } if got := classes.SecondsLeft(open, start.Add(59*time.Second)); got == nil || *got != 1 { t.Errorf("SecondsLeft at +59s = %v, want 1", got) } if got := classes.SecondsLeft(open, deadline.Add(time.Minute)); got == nil || *got != 0 { t.Errorf("SecondsLeft after the deadline = %v, want 0", got) } if classes.SecondsLeft(&models.Attempt{StartedAt: start, Quiz: &models.Quiz{}}, start) != nil { t.Error("SecondsLeft without a time limit is set") } if classes.SecondsLeft(&models.Attempt{StartedAt: start, Quiz: test, CompletedAt: &done}, start) != nil { t.Error("SecondsLeft of a completed attempt is set") } if classes.AttemptsLeft(&models.Quiz{Mode: models.ModeLearning, MaxAttempts: &two}, 1) != nil { t.Error("AttemptsLeft of a learning quiz is set") } if classes.AttemptsLeft(&models.Quiz{Mode: models.ModeTest}, 1) != nil { t.Error("AttemptsLeft of an unlimited test is set") } for used, want := range map[int]int{0: 2, 1: 1, 2: 0, 3: 0} { if got := classes.AttemptsLeft(&models.Quiz{Mode: models.ModeTest, MaxAttempts: &two}, used); got == nil || *got != want { t.Errorf("AttemptsLeft(max 2, used %d) = %v, want %d", used, got, want) } } for _, tc := range []struct { q *models.Quiz want bool }{ {&models.Quiz{Mode: models.ModeTest, Navigation: models.NavigationOneWay}, true}, {&models.Quiz{Mode: models.ModeTest, Navigation: models.NavigationFree}, false}, {&models.Quiz{Mode: models.ModeLearning, Navigation: models.NavigationOneWay}, false}, {nil, false}, } { if got := classes.OneWay(tc.q); got != tc.want { t.Errorf("OneWay(%+v) = %v, want %v", tc.q, got, tc.want) } } if got := classes.AttemptLockKey(3, 7); got != "golem15.quizzes.attempt:3:7" { t.Errorf("AttemptLockKey = %q", got) } } func TestTestGraceFrom(t *testing.T) { if got := classes.TestGraceFrom(nil); got != 5*time.Second || classes.DefaultTestGrace != 5*time.Second { t.Errorf("nil config = %v, want 5s", got) } dir := t.TempDir() cfg, err := compass.Open(compass.Options{Dir: dir, Env: "testing", Environ: []string{}}) if err != nil { t.Fatal(err) } if got := classes.TestGraceFrom(cfg); got != 5*time.Second { t.Errorf("missing key = %v, want 5s", got) } for _, tc := range []struct { value any want time.Duration }{{0, 0}, {30, 30 * time.Second}, {"x", 5 * time.Second}, {900, 120 * time.Second}, {-1, 0}, {"10", 10 * time.Second}} { if err := cfg.Set("golem15.quizzes.test.grace_seconds", tc.value); err != nil { t.Fatal(err) } if got := classes.TestGraceFrom(cfg); got != tc.want { t.Errorf("grace_seconds %v = %v, want %v", tc.value, got, tc.want) } } }