package classes_test import ( "errors" "strings" "testing" "git.golem15.com/golem15/sm-quizzes-plugin/classes" "git.golem15.com/golem15/sm-quizzes-plugin/models" ) func str(s string) *string { return &s } func num(n int) *int { return &n } func TestCheckQuizInput(t *testing.T) { for name, tc := range map[string]struct { in classes.QuizInput create bool want string }{ "missing title": {classes.QuizInput{}, true, "title is required"}, "blank title": {classes.QuizInput{Title: str(" ")}, true, "title is required"}, "long title": {classes.QuizInput{Title: str(strings.Repeat("a", 256))}, true, "title must be at most 255"}, "mode exam": {classes.QuizInput{Title: str("A"), Mode: str("exam")}, true, "mode must be one of"}, "navigation back": {classes.QuizInput{Title: str("A"), Navigation: str("back")}, true, "navigation must be one of"}, "passing score 101": {classes.QuizInput{Title: str("A"), PassingScore: num(101)}, true, "passing_score must be between"}, "max attempts 0": {classes.QuizInput{Title: str("A"), MaxAttempts: num(0)}, true, "max_attempts must be at least 1"}, "time limit 0": {classes.QuizInput{Title: str("A"), TimeLimitSeconds: num(0)}, true, "time_limit_seconds must be at least 1"}, "long context": {classes.QuizInput{Title: str("A"), Context: str(strings.Repeat("c", 256))}, true, "context must be at most 255"}, "update blank title": {classes.QuizInput{Title: str("")}, false, "title is required"}, "update negative limit": {classes.QuizInput{MaxAttempts: num(-1)}, false, "max_attempts must be 0"}, } { got := strings.Join(classes.CheckQuizInput(tc.in, tc.create), "; ") if !strings.Contains(got, tc.want) { t.Errorf("%s: problems %q, want %q", name, got, tc.want) } } if got := classes.CheckQuizInput(classes.QuizInput{Title: str("Pojęcia")}, true); len(got) != 0 { t.Errorf("minimal create: %v", got) } if got := classes.CheckQuizInput(classes.QuizInput{}, false); len(got) != 0 { t.Errorf("empty update: %v", got) } clears := classes.QuizInput{MaxAttempts: num(0), TimeLimitSeconds: num(0), RoundSize: num(0), Description: str(""), Context: str("")} if got := classes.CheckQuizInput(clears, false); len(got) != 0 { t.Errorf("update clearing with 0: %v", got) } } func TestValidateDeleteConfirm(t *testing.T) { if err := classes.ValidateDeleteConfirm("a", "a"); err != nil { t.Errorf("matching confirm: %v", err) } for _, confirm := range []string{"", "b", " a"} { err := classes.ValidateDeleteConfirm("a", confirm) var invalid *classes.ValidationError if !errors.As(err, &invalid) || !strings.Contains(err.Error(), "confirm") { t.Errorf("confirm %q: %v, want a ValidationError naming confirm", confirm, err) } } } func TestBuildStats(t *testing.T) { quiz := models.Quiz{ID: 1, Slug: "q", PassingScore: 60} questions := []models.Question{ {ID: 10, Text: "single", Type: models.TypeSingle}, {ID: 11, Text: "card", Type: models.TypeSelfCheck}, {ID: 12, Text: "unanswered", Type: models.TypeMultiple}, {ID: 13, Text: "all right", Type: models.TypeSingle}, } best1, best2, last := 75.0, 50.0, 40.0 students := []classes.StudentRow{ {UserID: 5, Email: "a@x", Attempts: 2, BestScorePercent: &best1, LastScorePercent: &last}, {UserID: 6, Email: "b@x", Attempts: 1, BestScorePercent: &best2, LastScorePercent: &best2}, {UserID: 7, Email: "c@x", Attempts: 1}, } answers := []classes.AnswerRow{ {QuestionID: 10, Answered: 3, Correct: 2}, {QuestionID: 11, Answered: 4, Correct: 1}, {QuestionID: 13, Answered: 2, Correct: 2}, } wrong := []classes.WrongRow{ {QuestionID: 10, AnswerID: 102, Text: "second", N: 1}, {QuestionID: 10, AnswerID: 101, Text: "first", N: 1}, {QuestionID: 11, AnswerID: 999, Text: "ignored", N: 9}, } s := classes.BuildStats(quiz, questions, students, answers, wrong) if s.QuestionCount != 4 || len(s.Questions) != 4 || len(s.Students) != 3 { t.Fatalf("stats = %+v", s) } if !s.Students[0].Passed || s.Students[1].Passed || s.Students[2].Passed { t.Errorf("passed = %v %v %v, want true false false", s.Students[0].Passed, s.Students[1].Passed, s.Students[2].Passed) } q := s.Questions[0] if q.CorrectPercent == nil || *q.CorrectPercent != 66.7 { t.Errorf("correct_percent 2 of 3 = %v, want 66.7", q.CorrectPercent) } if q.MostChosenWrong == nil || q.MostChosenWrong.AnswerID != 101 || q.MostChosenWrong.Count != 1 { t.Errorf("most_chosen_wrong = %+v, want answer 101 (lowest id on a tie)", q.MostChosenWrong) } if s.Questions[1].MostChosenWrong != nil { t.Errorf("self_check most_chosen_wrong = %+v, want nil", s.Questions[1].MostChosenWrong) } if un := s.Questions[2]; un.ID != 12 || un.Answered != 0 || un.CorrectPercent != nil || un.MostChosenWrong != nil { t.Errorf("unanswered question = %+v", un) } if ok := s.Questions[3]; ok.CorrectPercent == nil || *ok.CorrectPercent != 100 || ok.MostChosenWrong != nil { t.Errorf("all-correct question = %+v", ok) } more := append(wrong, classes.WrongRow{QuestionID: 10, AnswerID: 103, Text: "third", N: 2}) if w := classes.BuildStats(quiz, questions, nil, answers, more).Questions[0].MostChosenWrong; w == nil || w.AnswerID != 103 { t.Errorf("most_chosen_wrong by count = %+v, want 103", w) } }