Files
sm-quizzes-plugin/admin_postgres_test.go

159 lines
6.6 KiB
Go

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)
}
}