package quizzes_test import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" "git.golem15.com/golem15/sm-quizzes-plugin/models" "git.golem15.com/golem15/summercms/modules/bouncer" "git.golem15.com/golem15/summercms/modules/surf" "gorm.io/gorm" ) const adminPrefix = "/backend/api/v1" // adminToken creates a backend user whose role grants permissions and // signs them in through the admin API. func adminToken(t *testing.T, gdb *gorm.DB, h http.Handler, permissions ...string) string { t.Helper() suffix := fmt.Sprint(time.Now().UnixNano()) login, password := "quizzes-"+suffix, "secret-"+suffix hash, err := bouncer.HashPassword(4, password) must(t, err) grants := map[string]int{} for _, p := range permissions { grants[p] = 1 } raw, _ := json.Marshal(grants) var roleID uint must(t, gdb.Raw(`INSERT INTO backend_user_roles (name, code, permissions, is_system, created_at, updated_at) VALUES (?, ?, ?, FALSE, NOW(), NOW()) RETURNING id`, "Role "+suffix, "role-"+suffix, string(raw)).Scan(&roleID).Error) must(t, gdb.Exec(`INSERT INTO backend_users (login, email, password, is_activated, is_superuser, role_id, created_at, updated_at) VALUES (?, ?, ?, TRUE, FALSE, ?, NOW(), NOW())`, login, login+"@example.test", hash, roleID).Error) body, _ := json.Marshal(map[string]string{"login": login, "password": password}) req := httptest.NewRequest(http.MethodPost, adminPrefix+"/auth/login", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("admin login = %d %s", rec.Code, rec.Body.String()) } var out struct { Data struct { AccessToken string `json:"access_token"` } `json:"data"` } must(t, json.Unmarshal(rec.Body.Bytes(), &out)) return out.Data.AccessToken } func adminSend(t *testing.T, h http.Handler, method, path, token string, body any) (int, string) { t.Helper() var reader *bytes.Reader if body != nil { raw, _ := json.Marshal(body) reader = bytes.NewReader(raw) } else { reader = bytes.NewReader(nil) } req := httptest.NewRequest(method, adminPrefix+path, reader) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Accept-Language", "pl") req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) return rec.Code, rec.Body.String() } func TestAdminAgainstDatabase(t *testing.T) { app, plugins, gdb := migrated(t) h, err := surf.Assemble(app, plugins) must(t, err) f := seedQuiz(t, gdb, "admin", nil) other := seedQuiz(t, gdb, "drugi", func(q *models.Quiz) { q.Title = "Drugi" }) must(t, gdb.Model(&models.Question{}).Where("id = ?", other.selfQ.ID).Update("category", "Teoria stosunków").Error) u, _ := newUser(t, gdb, "ala@example.com") must(t, gdb.Create(&models.Attempt{QuizID: f.quiz.ID, UserID: u.ID, StartedAt: time.Now()}).Error) editor := adminToken(t, gdb, h, "golem15.quizzes.access_quizzes") viewer := adminToken(t, gdb, h, "golem15.quizzes.access_attempts") // Questions: filter by quiz and by a category with spaces and diacritics. code, body := adminSend(t, h, "GET", fmt.Sprintf("/golem15/quizzes/questions?filter[quiz]=%d", f.quiz.ID), editor, nil) if code != 200 || strings.Count(body, `"quiz_id"`) > 0 && !strings.Contains(body, "Stolica Polski") { t.Fatalf("questions filtered by quiz = %d %s", code, body) } var list struct { Data []map[string]any `json:"data"` } must(t, json.Unmarshal([]byte(body), &list)) if len(list.Data) != 3 { t.Errorf("questions of one quiz = %d rows: %s", len(list.Data), body) } code, body = adminSend(t, h, "GET", "/golem15/quizzes/questions/filters/category/options", editor, nil) if code != 200 || !strings.Contains(body, "Teoria stosunków") { t.Fatalf("category options = %d %s", code, body) } enc := models.EncodeCategoryValue("Teoria stosunków") code, body = adminSend(t, h, "GET", "/golem15/quizzes/questions?filter[category]="+url.QueryEscape(enc), editor, nil) must(t, json.Unmarshal([]byte(body), &list)) if code != 200 || len(list.Data) != 1 { t.Errorf("questions in 'Teoria stosunków' = %d %s", code, body) } code, body = adminSend(t, h, "GET", "/golem15/quizzes/questions/filters/quiz/options", editor, nil) if code != 200 || !strings.Contains(body, "Drugi") { t.Errorf("quiz options = %d %s", code, body) } code, body = adminSend(t, h, "GET", fmt.Sprintf("/golem15/quizzes/answers?filter[quiz]=%d", other.quiz.ID), editor, nil) must(t, json.Unmarshal([]byte(body), &list)) if code != 200 || len(list.Data) != 5 { t.Errorf("answers of one quiz = %d %s", code, body) } // Creating a quiz fills the slug from the title; a duplicate is a 422. code, body = adminSend(t, h, "POST", "/golem15/quizzes/quizzes", editor, map[string]any{"title": "Nowy Quiz Łódź", "passing_score": 60}) if code != 201 && code != 200 { t.Fatalf("create quiz = %d %s", code, body) } var q models.Quiz must(t, gdb.Where("title = ?", "Nowy Quiz Łódź").Take(&q).Error) if q.Slug != "nowy-quiz-lodz" || q.PassingScore != 60 { t.Errorf("created quiz = %+v", q) } code, body = adminSend(t, h, "POST", "/golem15/quizzes/quizzes", editor, map[string]any{"title": "Nowy quiz łódź", "passing_score": 60}) if code != 422 || !strings.Contains(body, "slug") { t.Errorf("duplicate slug = %d %s", code, body) } code, body = adminSend(t, h, "POST", "/golem15/quizzes/questions", editor, map[string]any{"quiz": q.ID, "text": "Q?", "type": "essay", "points": 1}) if code != 422 || !strings.Contains(body, "type") { t.Errorf("question with an unknown type = %d %s", code, body) } // Attempts are read-only and need their own permission. if code, _ := adminSend(t, h, "GET", "/golem15/quizzes/attempts", editor, nil); code != 403 { t.Errorf("attempts for a quiz editor = %d, want 403", code) } code, body = adminSend(t, h, "GET", "/golem15/quizzes/attempts", viewer, nil) if code != 200 || !strings.Contains(body, "ala@example.com") { t.Errorf("attempts list = %d %s", code, body) } code, body = adminSend(t, h, "GET", fmt.Sprintf("/golem15/quizzes/attempts?filter[quiz]=%d", other.quiz.ID), viewer, nil) must(t, json.Unmarshal([]byte(body), &list)) if code != 200 || len(list.Data) != 0 { t.Errorf("attempts of a quiz without attempts = %d %s", code, body) } if code, body := adminSend(t, h, "POST", "/golem15/quizzes/attempts", viewer, map[string]any{}); code == 200 || code == 201 { t.Errorf("creating an attempt through the admin = %d %s", code, body) } if code, _ := adminSend(t, h, "GET", "/golem15/quizzes/quizzes", viewer, nil); code != 403 { t.Errorf("quizzes for an attempts viewer = %d, want 403", code) } }