Files
sm-quizzes-plugin/classes/review_test.go
Jakub Zych 692a647650 feat: set scores, confirm in bulk and regrade open answers over MCP
- SetScore (0..points in 0.5 steps, comment shown to the student),
  ConfirmBulk per quiz or attempt and Regrade through the quiz's grader
- close() announces an attempt reviewed while it was open
- quizzes_review_set_score, quizzes_review_confirm_bulk,
  quizzes_review_regrade; 409 already_reviewed and grader_manual
2026-11-09 18:06:02 +01:00

116 lines
4.4 KiB
Go

package classes_test
import (
"math"
"slices"
"strings"
"testing"
"git.golem15.com/golem15/sm-quizzes-plugin/classes"
)
// TestCheckReviewStatus: "" and open list the three unreviewed statuses,
// a grading status lists itself, all lists everything (nil), and any
// other value is a problem naming the valid values.
func TestCheckReviewStatus(t *testing.T) {
open := []string{"pending", "graded", "failed"}
for in, want := range map[string][]string{
"": open,
"open": open,
" open ": open,
"pending": {"pending"},
"graded": {"graded"},
"failed": {"failed"},
"reviewed": {"reviewed"},
"all": nil,
} {
got, problems := classes.CheckReviewStatus(in)
if len(problems) != 0 {
t.Errorf("%q: problems %v", in, problems)
}
if !slices.Equal(got, want) || (want == nil) != (got == nil) {
t.Errorf("%q = %v, want %v", in, got, want)
}
}
for _, in := range []string{"done", "Open", "pending,graded"} {
got, problems := classes.CheckReviewStatus(in)
if got != nil || len(problems) != 1 || !strings.Contains(problems[0], "open, pending, graded, failed, reviewed, all") {
t.Errorf("%q = %v %v, want one problem listing the valid values", in, got, problems)
}
}
}
// TestCheckReviewPoints: 0..points in steps of 0.5, finite.
func TestCheckReviewPoints(t *testing.T) {
for _, tc := range []struct {
p float64
max int
want bool
}{
{0, 2, true}, {1.5, 2, true}, {2, 2, true}, {0, 0, true}, {0.5, 1, true},
{-0.5, 2, false}, {2.5, 2, false}, {1.25, 2, false}, {0.1, 2, false},
{math.NaN(), 2, false}, {math.Inf(1), 2, false}, {math.Inf(-1), 2, false},
{0.5, 0, false}, {0, -1, true}, {1, -1, false},
} {
if got := classes.CheckReviewPoints(tc.p, tc.max); got != tc.want {
t.Errorf("CheckReviewPoints(%v, %d) = %v, want %v", tc.p, tc.max, got, tc.want)
}
}
}
// TestCheckReviewComment: trimmed, blank is no comment, NUL dropped,
// at most 2000 runes.
func TestCheckReviewComment(t *testing.T) {
str := func(s string) *string { return &s }
if got, p := classes.CheckReviewComment(nil); got != nil || p != nil {
t.Errorf("nil = %v %v", got, p)
}
if got, p := classes.CheckReviewComment(str(" \n ")); got != nil || p != nil {
t.Errorf("spaces = %v %v, want no comment", got, p)
}
if got, p := classes.CheckReviewComment(str(" Dobrze, ale\x00 brak przykładu. ")); got == nil || *got != "Dobrze, ale brak przykładu." || p != nil {
t.Errorf("trimmed = %v %v", got, p)
}
max := strings.Repeat("ł", classes.MaxReviewComment)
if got, p := classes.CheckReviewComment(&max); got == nil || *got != max || p != nil {
t.Errorf("2000 runes refused: %v", p)
}
over := max + "x"
if got, p := classes.CheckReviewComment(&over); got != nil || len(p) != 1 || p[0] != "comment must be at most 2000 characters" {
t.Errorf("2001 runes = %v %v, want one problem", got, p)
}
}
// TestCheckReviewScope: exactly one key; answer_id only for regrade.
func TestCheckReviewScope(t *testing.T) {
regrade := "exactly one of answer_id, attempt_id and quiz is required"
bulk := "exactly one of attempt_id and quiz is required"
for name, tc := range map[string]struct {
scope classes.ReviewScope
allow bool
want string
}{
"regrade answer": {classes.ReviewScope{AnswerID: 1}, true, ""},
"regrade attempt": {classes.ReviewScope{AttemptID: 1}, true, ""},
"regrade quiz": {classes.ReviewScope{QuizSlug: "q"}, true, ""},
"regrade none": {classes.ReviewScope{}, true, regrade},
"regrade blank quiz": {classes.ReviewScope{QuizSlug: " "}, true, regrade},
"regrade two": {classes.ReviewScope{AnswerID: 1, QuizSlug: "q"}, true, regrade},
"regrade three": {classes.ReviewScope{AnswerID: 1, AttemptID: 2, QuizSlug: "q"}, true, regrade},
"bulk attempt": {classes.ReviewScope{AttemptID: 1}, false, ""},
"bulk quiz": {classes.ReviewScope{QuizSlug: "q"}, false, ""},
"bulk none": {classes.ReviewScope{}, false, bulk},
"bulk two": {classes.ReviewScope{AttemptID: 1, QuizSlug: "q"}, false, bulk},
"bulk answer": {classes.ReviewScope{AnswerID: 1}, false, bulk},
"bulk answer and quiz": {classes.ReviewScope{AnswerID: 1, QuizSlug: "q"}, false, bulk},
} {
got := classes.CheckReviewScope(tc.scope, tc.allow)
switch {
case tc.want == "" && got != nil:
t.Errorf("%s = %v, want no problem", name, got)
case tc.want != "" && (len(got) != 1 || got[0] != tc.want):
t.Errorf("%s = %v, want %q", name, got, tc.want)
}
}
}