package classes_test import ( "encoding/json" "os" "strings" "testing" "git.golem15.com/golem15/sm-quizzes-plugin/classes" ) func TestParseImportSample(t *testing.T) { raw, err := os.ReadFile("../testdata/quiz.json") if err != nil { t.Fatal(err) } doc, err := classes.ParseImport(raw) if err != nil { t.Fatalf("ParseImport: %v", err) } if doc.Slug != "bezpieczenstwo-pojecia" { t.Errorf("slug = %q, want it built from the title", doc.Slug) } if len(doc.Questions) != 3 || *doc.RoundSize != 5 || *doc.PassingScore != 60 { t.Errorf("doc = %+v", doc) } } func TestParseImportRejects(t *testing.T) { cases := map[string]string{ `not json`: "invalid JSON", `{"title":"A","questions":[{"text":"Q","type":"self_check"}],"extra":1}`: "unknown field", `{"questions":[{"text":"Q","type":"self_check"}]}`: "title is required", `{"title":"A","questions":[]}`: "questions must not be empty", `{"title":"A","passing_score":101,"questions":[{"text":"Q","type":"self_check"}]}`: "passing_score", `{"title":"A","round_size":0,"questions":[{"text":"Q","type":"self_check"}]}`: "round_size", `{"title":"A","time_limit_seconds":0,"questions":[{"text":"Q","type":"self_check"}]}`: "time_limit_seconds", `{"title":"A","questions":[{"text":"","type":"self_check"}]}`: "questions[0].text", `{"title":"A","questions":[{"text":"Q","type":"essay"}]}`: "questions[0].type", `{"title":"A","questions":[{"text":"Q","type":"self_check","points":-1}]}`: "points", `{"title":"A","questions":[{"text":"Q","type":"self_check","answers":[{"text":"x"}]}]}`: "self_check question has no answers", `{"title":"A","questions":[{"text":"Q","type":"single","answers":[{"text":"x","is_correct":true}]}]}`: "single question", `{"title":"A","questions":[{"text":"Q","type":"single","answers":[{"text":"x"},{"text":"y"}]}]}`: "single question", `{"title":"A","questions":[{"text":"Q","type":"multiple","answers":[{"text":"x"},{"text":"y"}]}]}`: "multiple question", `{"title":"A","questions":[{"text":"Q","type":"multiple","answers":[{"text":"","is_correct":true},{"text":"y"}]}]}`: "answers[0].text", `{"title":"!!!","questions":[{"text":"Q","type":"self_check"}]}`: "slug is empty", } for raw, want := range cases { _, err := classes.ParseImport([]byte(raw)) if err == nil || !strings.Contains(err.Error(), want) { t.Errorf("ParseImport(%s) = %v, want an error with %q", raw, err, want) } } doc, err := classes.ParseImport([]byte(`{"title":"Ignored","slug":"Mój Slug","questions":[{"text":"Q","type":"self_check"}]}`)) if err != nil || doc.Slug != "moj-slug" { t.Errorf("explicit slug = %v %v, want moj-slug", doc, err) } } func TestParseImportTestMode(t *testing.T) { doc, err := classes.ParseImport([]byte(`{"title":"T","mode":"test","navigation":"one_way","max_attempts":2,"questions":[{"text":"Q","type":"self_check"}]}`)) if err != nil { t.Fatalf("ParseImport: %v", err) } if doc.Mode == nil || *doc.Mode != "test" || doc.Navigation == nil || *doc.Navigation != "one_way" || doc.MaxAttempts == nil || *doc.MaxAttempts != 2 { t.Errorf("doc = %+v", doc) } for raw, want := range map[string]string{ `{"title":"T","mode":"exam","questions":[{"text":"Q","type":"self_check"}]}`: "mode must be one of learning, test", `{"title":"T","navigation":"back","questions":[{"text":"Q","type":"self_check"}]}`: "navigation must be one of free, one_way", `{"title":"T","max_attempts":0,"questions":[{"text":"Q","type":"self_check"}]}`: "max_attempts must be at least 1", } { if _, err := classes.ParseImport([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("ParseImport(%s) = %v, want %q", raw, err, want) } } raw, err := os.ReadFile("../testdata/quiz.json") if err != nil { t.Fatal(err) } doc, err = classes.ParseImport(raw) if err != nil || doc.Mode != nil || doc.Navigation != nil || doc.MaxAttempts != nil { t.Errorf("sample = %+v %v, want no test-mode keys", doc, err) } } // TestCheckQuestion: the per-question rules ParseImport applies, shared // with the manage routes; ParseImport reports exactly these problems. func TestCheckQuestion(t *testing.T) { neg := -1 for name, tc := range map[string]struct { q classes.ImportQuestion want []string }{ "no text": {classes.ImportQuestion{Type: "self_check"}, []string{"questions[0].text is required"}}, "bad type": {classes.ImportQuestion{Text: "Q", Type: "essay"}, []string{"questions[0].type must be one of single, multiple, self_check"}}, "negative points": {classes.ImportQuestion{Text: "Q", Type: "self_check", Points: &neg}, []string{"questions[0].points must not be negative"}}, "single two right": {classes.ImportQuestion{Text: "Q", Type: "single", Answers: []classes.ImportAnswer{{Text: "a", IsCorrect: true}, {Text: "b", IsCorrect: true}}}, []string{"questions[0]: a single question needs at least two answers and exactly one correct"}}, "multiple one item": {classes.ImportQuestion{Text: "Q", Type: "multiple", Answers: []classes.ImportAnswer{{Text: "a", IsCorrect: true}}}, []string{"questions[0]: a multiple question needs at least two answers and one or more correct"}}, "self_check answer": {classes.ImportQuestion{Text: "Q", Type: "self_check", Answers: []classes.ImportAnswer{{Text: "a"}}}, []string{"questions[0]: a self_check question has no answers; use answer_text"}}, "blank answer": {classes.ImportQuestion{Text: "Q", Type: "single", Answers: []classes.ImportAnswer{{Text: " ", IsCorrect: true}, {Text: "b"}}}, []string{"questions[0].answers[0].text is required"}}, "valid single": {classes.ImportQuestion{Text: "Q", Type: "single", Answers: []classes.ImportAnswer{{Text: "a", IsCorrect: true}, {Text: "b"}}}, nil}, } { got := classes.CheckQuestion("questions[0]", tc.q) if strings.Join(got, "; ") != strings.Join(tc.want, "; ") { t.Errorf("%s: %q, want %q", name, got, tc.want) } doc, _ := json.Marshal(map[string]any{"title": "T", "questions": []classes.ImportQuestion{tc.q}}) _, err := classes.ParseImport(doc) switch { case tc.want == nil && err != nil: t.Errorf("%s: ParseImport: %v", name, err) case tc.want != nil && (err == nil || err.Error() != "invalid quiz: "+strings.Join(tc.want, "; ")): t.Errorf("%s: ParseImport = %v, want the same problems", name, err) } } }