505 lines
20 KiB
Go
505 lines
20 KiB
Go
package bm_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
userclasses "git.golem15.com/golem15/sm-user-plugin/classes"
|
|
"git.golem15.com/golem15/summercms/modules/surf"
|
|
"gocloud.dev/blob"
|
|
_ "gocloud.dev/blob/memblob"
|
|
)
|
|
|
|
func TestManagementAPIUsesSharedTokenAndCurrentUserPermissions(t *testing.T) {
|
|
app, plugins, db := migrated(t)
|
|
|
|
var userID uint
|
|
if err := db.Raw(`
|
|
INSERT INTO users (email, password, must_change_password)
|
|
VALUES ('bm-shared-token@example.test', '', false)
|
|
RETURNING id`).Scan(&userID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var groupID uint
|
|
if err := db.Raw(`
|
|
INSERT INTO user_groups (name, code, permissions, created_at, updated_at)
|
|
VALUES ('BM API', 'bm-api', '{"jz.bm.access_courses":1}', NOW(), NOW())
|
|
RETURNING id`).Scan(&groupID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Exec(`INSERT INTO users_groups (user_id, user_group_id) VALUES (?, ?)`, userID, groupID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secret, token, err := userclasses.MintAPIToken("usr_", userID, "BM integration", []string{"read"}, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Create(token).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
handler, err := surf.Assemble(app, plugins)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := func(path string) *httptest.ResponseRecorder {
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
req.Header.Set("Authorization", "Bearer "+secret)
|
|
rec := httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
|
|
if rec := request("/api/v1/bm/courses"); rec.Code != http.StatusOK {
|
|
t.Fatalf("permitted courses request = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
if rec := request("/api/v1/bm/materials"); rec.Code != http.StatusForbidden {
|
|
t.Fatalf("ungranted materials request = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
if err := db.Model(&struct{}{}).Table("user_groups").Where("id = ?", groupID).Update("permissions", `{}`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec := request("/api/v1/bm/courses"); rec.Code != http.StatusForbidden || !strings.Contains(rec.Body.String(), "Forbidden") {
|
|
t.Fatalf("request after permission revocation = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestManagementMaterialsUpsertAndUpdate drives the material routes of the
|
|
// management API with a write token: the upsert identity (title and course),
|
|
// the update by id with a rename and a draft, the identity conflict, and
|
|
// the 404s.
|
|
func TestManagementMaterialsUpsertAndUpdate(t *testing.T) {
|
|
app, plugins, db := migrated(t)
|
|
|
|
var userID uint
|
|
if err := db.Raw(`
|
|
INSERT INTO users (email, password, must_change_password)
|
|
VALUES ('bm-materials-token@example.test', '', false)
|
|
RETURNING id`).Scan(&userID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var groupID uint
|
|
if err := db.Raw(`
|
|
INSERT INTO user_groups (name, code, permissions, created_at, updated_at)
|
|
VALUES ('BM materials', 'bm-materials', '{"jz.bm.access_materials":1}', NOW(), NOW())
|
|
RETURNING id`).Scan(&groupID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Exec(`INSERT INTO users_groups (user_id, user_group_id) VALUES (?, ?)`, userID, groupID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secret, token, err := userclasses.MintAPIToken("usr_", userID, "BM notes", []string{"read", "write"}, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Create(token).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
handler, err := surf.Assemble(app, plugins)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
send := func(method, path string, payload any) (*httptest.ResponseRecorder, map[string]any) {
|
|
t.Helper()
|
|
var rd io.Reader
|
|
if payload != nil {
|
|
raw, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rd = bytes.NewReader(raw)
|
|
}
|
|
req := httptest.NewRequest(method, path, rd)
|
|
req.Header.Set("Authorization", "Bearer "+secret)
|
|
if payload != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, req)
|
|
var out map[string]any
|
|
if rec.Body.Len() > 0 {
|
|
_ = json.Unmarshal(rec.Body.Bytes(), &out)
|
|
}
|
|
return rec, out
|
|
}
|
|
data := func(out map[string]any) map[string]any {
|
|
t.Helper()
|
|
d, _ := out["data"].(map[string]any)
|
|
if d == nil {
|
|
t.Fatalf("no data object in %v", out)
|
|
}
|
|
return d
|
|
}
|
|
idPath := func(id any) string { return "/api/v1/bm/materials/" + strconv.Itoa(int(id.(float64))) }
|
|
|
|
note := map[string]any{"title": "Definicje globalizacji", "type": "notatka", "body": "# Robertson\n\n> single place", "tags": []string{"Globalizacja", "Zjazd 2"}}
|
|
rec, out := send(http.MethodPost, "/api/v1/bm/materials", note)
|
|
if rec.Code != http.StatusCreated || out["created"] != true {
|
|
t.Fatalf("create = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
first := data(out)
|
|
if first["published_at"] == nil {
|
|
t.Error("a new material without published_at is not published")
|
|
}
|
|
if tags, _ := first["tags"].([]any); len(tags) != 2 || tags[1].(map[string]any)["slug"] != "zjazd-2" {
|
|
t.Errorf("tags = %v", first["tags"])
|
|
}
|
|
|
|
rec, out = send(http.MethodPost, "/api/v1/bm/materials", note)
|
|
if rec.Code != http.StatusOK || out["created"] != false || data(out)["id"] != first["id"] {
|
|
t.Fatalf("repeated upsert = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
rec, out = send(http.MethodGet, idPath(first["id"]), nil)
|
|
if rec.Code != http.StatusOK || data(out)["title"] != "Definicje globalizacji" {
|
|
t.Fatalf("show = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
if files, ok := data(out)["files"].([]any); !ok || len(files) != 0 {
|
|
t.Errorf("files = %v", data(out)["files"])
|
|
}
|
|
|
|
renamed := map[string]any{"title": "Definicje globalizacji (zjazd 2)", "type": "notatka", "body": "zmienione", "published_at": nil}
|
|
rec, out = send(http.MethodPut, idPath(first["id"]), renamed)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("update = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
updated := data(out)
|
|
if updated["title"] != renamed["title"] || updated["body"] != "zmienione" || updated["published_at"] != nil {
|
|
t.Errorf("updated = %v", updated)
|
|
}
|
|
if tags, _ := updated["tags"].([]any); len(tags) != 2 {
|
|
t.Errorf("an update without tags changed them: %v", updated["tags"])
|
|
}
|
|
|
|
rec, out = send(http.MethodPost, "/api/v1/bm/materials", note)
|
|
if rec.Code != http.StatusCreated || data(out)["id"] == first["id"] {
|
|
t.Fatalf("upsert after the rename = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
second := data(out)
|
|
|
|
rec, out = send(http.MethodPut, idPath(second["id"]), renamed)
|
|
if rec.Code != http.StatusUnprocessableEntity || !strings.Contains(rec.Body.String(), `"title"`) {
|
|
t.Fatalf("update onto a taken identity = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
for _, method := range []string{http.MethodGet, http.MethodPut} {
|
|
var payload any
|
|
if method == http.MethodPut {
|
|
payload = renamed
|
|
}
|
|
if rec, _ := send(method, "/api/v1/bm/materials/999999", payload); rec.Code != http.StatusNotFound {
|
|
t.Errorf("%s of an unknown id = %d %s", method, rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
if rec, _ := send(http.MethodDelete, idPath(first["id"]), nil); rec.Code != http.StatusNoContent {
|
|
t.Fatalf("delete = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
if rec, _ := send(http.MethodGet, idPath(first["id"]), nil); rec.Code != http.StatusNotFound {
|
|
t.Errorf("show after delete = %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
// TestManagementMaterialFiles uploads files to a material through the
|
|
// management API, refuses a disallowed type, detaches one file and deletes
|
|
// the material with the other, checking rows and blobs.
|
|
func TestManagementMaterialFiles(t *testing.T) {
|
|
app, plugins, db := migrated(t)
|
|
bucket, err := blob.OpenBucket(t.Context(), "mem://")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { bucket.Close() })
|
|
if err := app.Publish(bucket); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var userID uint
|
|
if err := db.Raw(`
|
|
INSERT INTO users (email, password, must_change_password)
|
|
VALUES ('bm-files-token@example.test', '', false)
|
|
RETURNING id`).Scan(&userID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var groupID uint
|
|
if err := db.Raw(`
|
|
INSERT INTO user_groups (name, code, permissions, created_at, updated_at)
|
|
VALUES ('BM files', 'bm-files', '{"jz.bm.access_materials":1}', NOW(), NOW())
|
|
RETURNING id`).Scan(&groupID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Exec(`INSERT INTO users_groups (user_id, user_group_id) VALUES (?, ?)`, userID, groupID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secret, token, err := userclasses.MintAPIToken("usr_", userID, "BM files", []string{"read", "write"}, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Create(token).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
handler, err := surf.Assemble(app, plugins)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
do := func(req *http.Request) (*httptest.ResponseRecorder, map[string]any) {
|
|
t.Helper()
|
|
req.Header.Set("Authorization", "Bearer "+secret)
|
|
rec := httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, req)
|
|
var out map[string]any
|
|
if rec.Body.Len() > 0 {
|
|
_ = json.Unmarshal(rec.Body.Bytes(), &out)
|
|
}
|
|
return rec, out
|
|
}
|
|
upload := func(path string, files map[string][]byte) (*httptest.ResponseRecorder, map[string]any) {
|
|
t.Helper()
|
|
var buf bytes.Buffer
|
|
mw := multipart.NewWriter(&buf)
|
|
for name, content := range files {
|
|
fw, err := mw.CreateFormFile("file", name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fw.Write(content)
|
|
}
|
|
mw.Close()
|
|
req := httptest.NewRequest(http.MethodPost, path, &buf)
|
|
req.Header.Set("Content-Type", mw.FormDataContentType())
|
|
return do(req)
|
|
}
|
|
countFiles := func() int64 {
|
|
var n int64
|
|
if err := db.Table("system_files").Where("field = 'files'").Count(&n).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return n
|
|
}
|
|
|
|
raw, _ := json.Marshal(map[string]any{"title": "Podręcznik", "type": "pdf"})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/bm/materials", bytes.NewReader(raw))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec, out := do(req)
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("create = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
id := strconv.Itoa(int(out["data"].(map[string]any)["id"].(float64)))
|
|
base := "/api/v1/bm/materials/" + id
|
|
|
|
pdf := []byte("%PDF-1.4\n1 0 obj << >> endobj\ntrailer << >>\n%%EOF\n")
|
|
rec, out = upload(base+"/files", map[string][]byte{"wojna.pdf": pdf})
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("upload = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
rec, out = upload(base+"/files", map[string][]byte{"plan.txt": []byte("plan zajęć\n")})
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("second upload = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
files, _ := out["data"].(map[string]any)["files"].([]any)
|
|
if len(files) != 2 {
|
|
t.Fatalf("files after two uploads = %v", out["data"])
|
|
}
|
|
first := files[0].(map[string]any)
|
|
if first["name"] != "wojna.pdf" || first["mime"] != "application/pdf" || !strings.HasSuffix(first["url"].(string), ".pdf") {
|
|
t.Errorf("first file = %v", first)
|
|
}
|
|
|
|
if rec, _ := upload(base+"/files", map[string][]byte{"x.exe": []byte("MZ")}); rec.Code != http.StatusUnprocessableEntity {
|
|
t.Errorf("disallowed type = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
if rec, _ := upload(base+"/files", nil); rec.Code != http.StatusUnprocessableEntity {
|
|
t.Errorf("no file = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
if rec, _ := upload("/api/v1/bm/materials/999999/files", map[string][]byte{"a.pdf": pdf}); rec.Code != http.StatusNotFound {
|
|
t.Errorf("unknown material = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
if n := countFiles(); n != 2 {
|
|
t.Fatalf("system_files rows = %d after refusals, want 2", n)
|
|
}
|
|
|
|
fileID := strconv.Itoa(int(first["id"].(float64)))
|
|
if rec, _ := do(httptest.NewRequest(http.MethodDelete, "/api/v1/bm/materials/999999/files/"+fileID, nil)); rec.Code != http.StatusNotFound {
|
|
t.Errorf("detach through another material = %d", rec.Code)
|
|
}
|
|
if rec, _ := do(httptest.NewRequest(http.MethodDelete, base+"/files/"+fileID, nil)); rec.Code != http.StatusNoContent {
|
|
t.Fatalf("detach = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
if n := countFiles(); n != 1 {
|
|
t.Errorf("system_files rows = %d after detach, want 1", n)
|
|
}
|
|
if rec, _ := do(httptest.NewRequest(http.MethodDelete, base, nil)); rec.Code != http.StatusNoContent {
|
|
t.Fatalf("delete material = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
if n := countFiles(); n != 0 {
|
|
t.Errorf("system_files rows = %d after deleting the material, want 0", n)
|
|
}
|
|
iter := bucket.List(nil)
|
|
if obj, err := iter.Next(t.Context()); err != io.EOF {
|
|
t.Errorf("a blob is left in the bucket: %v %v", obj, err)
|
|
}
|
|
}
|
|
|
|
// TestManagementRecords drives the record routes: create, partial update,
|
|
// validation (unknown column, rules, temporal order, a missing reference),
|
|
// list filters and delete, for courses, meetings, sessions, readings and
|
|
// deadlines.
|
|
func TestManagementRecords(t *testing.T) {
|
|
app, plugins, db := migrated(t)
|
|
var userID uint
|
|
if err := db.Raw(`
|
|
INSERT INTO users (email, password, must_change_password)
|
|
VALUES ('bm-records-token@example.test', '', false)
|
|
RETURNING id`).Scan(&userID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var groupID uint
|
|
if err := db.Raw(`
|
|
INSERT INTO user_groups (name, code, permissions, created_at, updated_at)
|
|
VALUES ('BM records', 'bm-records', '{"jz.bm.access_courses":1,"jz.bm.access_schedule":1}', NOW(), NOW())
|
|
RETURNING id`).Scan(&groupID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Exec(`INSERT INTO users_groups (user_id, user_group_id) VALUES (?, ?)`, userID, groupID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secret, token, err := userclasses.MintAPIToken("usr_", userID, "BM records", []string{"read", "write"}, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Create(token).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
handler, err := surf.Assemble(app, plugins)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
send := func(method, path string, payload any) (int, map[string]any) {
|
|
t.Helper()
|
|
var rd io.Reader
|
|
if payload != nil {
|
|
raw, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rd = bytes.NewReader(raw)
|
|
}
|
|
req := httptest.NewRequest(method, "/api/v1/bm"+path, rd)
|
|
req.Header.Set("Authorization", "Bearer "+secret)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, req)
|
|
var out map[string]any
|
|
if rec.Body.Len() > 0 {
|
|
_ = json.Unmarshal(rec.Body.Bytes(), &out)
|
|
}
|
|
return rec.Code, out
|
|
}
|
|
data := func(out map[string]any) map[string]any {
|
|
t.Helper()
|
|
d, _ := out["data"].(map[string]any)
|
|
if d == nil {
|
|
t.Fatalf("no data object in %v", out)
|
|
}
|
|
return d
|
|
}
|
|
id := func(d map[string]any) string { return strconv.Itoa(int(d["id"].(float64))) }
|
|
errorsOf := func(out map[string]any) map[string]any {
|
|
e, _ := out["errors"].(map[string]any)
|
|
return e
|
|
}
|
|
|
|
code, out := send(http.MethodPost, "/courses", map[string]any{"name": "Teoria", "form": "wyklad", "semester": 1, "code": "26-X"})
|
|
if code != http.StatusCreated {
|
|
t.Fatalf("create course = %d %v", code, out)
|
|
}
|
|
course := data(out)
|
|
code, out = send(http.MethodPatch, "/courses/"+id(course), map[string]any{"description": "Plan wykładów: 1. Wprowadzenie."})
|
|
if code != http.StatusOK || data(out)["description"] != "Plan wykładów: 1. Wprowadzenie." || data(out)["name"] != "Teoria" {
|
|
t.Fatalf("patch course = %d %v", code, out)
|
|
}
|
|
if code, out = send(http.MethodPatch, "/courses/"+id(course), map[string]any{"owner": 1}); code != http.StatusUnprocessableEntity || errorsOf(out)["owner"] == nil {
|
|
t.Errorf("course patch with an unknown column = %d %v", code, out)
|
|
}
|
|
if code, out = send(http.MethodPatch, "/courses/"+id(course), map[string]any{"form": "warsztat", "semester": 9}); code != http.StatusUnprocessableEntity || errorsOf(out)["form"] == nil || errorsOf(out)["semester"] == nil {
|
|
t.Errorf("course patch breaking the rules = %d %v", code, out)
|
|
}
|
|
if code, _ = send(http.MethodDelete, "/courses/"+id(course), nil); code != http.StatusMethodNotAllowed && code != http.StatusNotFound {
|
|
t.Errorf("course delete = %d, want no route", code)
|
|
}
|
|
|
|
code, out = send(http.MethodPost, "/meetings", map[string]any{"number": 1, "starts_on": "2026-10-03", "ends_on": "2026-10-04"})
|
|
if code != http.StatusCreated || data(out)["starts_on"] != "2026-10-03" {
|
|
t.Fatalf("create meeting = %d %v", code, out)
|
|
}
|
|
meeting := data(out)
|
|
if code, out = send(http.MethodPost, "/meetings", map[string]any{"number": 1, "starts_on": "2026-10-17", "ends_on": "2026-10-18"}); code != http.StatusUnprocessableEntity || errorsOf(out)["number"] == nil {
|
|
t.Errorf("duplicate meeting number = %d %v", code, out)
|
|
}
|
|
if code, out = send(http.MethodPatch, "/meetings/"+id(meeting), map[string]any{"notes": "online"}); code != http.StatusOK || data(out)["number"] != float64(1) {
|
|
t.Errorf("patch own meeting (unique on itself) = %d %v", code, out)
|
|
}
|
|
|
|
session := map[string]any{"course_id": course["id"], "meeting_id": meeting["id"], "starts_at": "2026-10-03 09:00", "ends_at": "2026-10-03 10:30", "room": "107"}
|
|
code, out = send(http.MethodPost, "/class-sessions", session)
|
|
if code != http.StatusCreated || data(out)["starts_at"] != "2026-10-03T09:00:00+02:00" {
|
|
t.Fatalf("create session = %d %v", code, out)
|
|
}
|
|
sess := data(out)
|
|
if code, out = send(http.MethodPatch, "/class-sessions/"+id(sess), map[string]any{"ends_at": "2026-10-03 08:00"}); code != http.StatusUnprocessableEntity || errorsOf(out)["ends_at"] == nil {
|
|
t.Errorf("session ending before it starts = %d %v", code, out)
|
|
}
|
|
if code, out = send(http.MethodPatch, "/class-sessions/"+id(sess), map[string]any{"room": "", "lecturer": "dr X", "is_online": true}); code != http.StatusOK || data(out)["room"] != nil || data(out)["is_online"] != true {
|
|
t.Errorf("patch session = %d %v", code, out)
|
|
}
|
|
if code, out = send(http.MethodPatch, "/class-sessions/"+id(sess), map[string]any{"room": 102}); code != http.StatusOK || data(out)["room"] != "102" {
|
|
t.Errorf("room sent as a number = %d %v", code, out)
|
|
}
|
|
if code, out = send(http.MethodPost, "/class-sessions", map[string]any{"course_id": 999999, "starts_at": "2026-10-03 09:00", "ends_at": "2026-10-03 10:00"}); code != http.StatusUnprocessableEntity || errorsOf(out)["course_id"] == nil {
|
|
t.Errorf("session of an unknown course = %d %v", code, out)
|
|
}
|
|
|
|
code, out = send(http.MethodPost, "/readings", map[string]any{"course_id": course["id"], "title": "Kuhn, Struktura rewolucji naukowych", "is_required": true})
|
|
if code != http.StatusCreated {
|
|
t.Fatalf("create reading = %d %v", code, out)
|
|
}
|
|
reading := data(out)
|
|
if code, out = send(http.MethodPost, "/readings", map[string]any{"course_id": course["id"]}); code != http.StatusUnprocessableEntity || errorsOf(out)["title"] == nil {
|
|
t.Errorf("reading without a title = %d %v", code, out)
|
|
}
|
|
if code, out = send(http.MethodGet, "/readings?course_id="+id(course), nil); code != http.StatusOK || len(out["data"].([]any)) != 1 {
|
|
t.Errorf("readings of the course = %d %v", code, out)
|
|
}
|
|
if code, out = send(http.MethodGet, "/readings?course_id=999999", nil); code != http.StatusOK || len(out["data"].([]any)) != 0 {
|
|
t.Errorf("readings of another course = %d %v", code, out)
|
|
}
|
|
|
|
code, out = send(http.MethodPost, "/deadlines", map[string]any{"title": "Egzamin", "due_at": "2027-01-30T12:00:00+01:00", "kind": "egzamin", "course_id": course["id"]})
|
|
if code != http.StatusCreated {
|
|
t.Fatalf("create deadline = %d %v", code, out)
|
|
}
|
|
deadline := data(out)
|
|
if code, out = send(http.MethodPatch, "/deadlines/"+id(deadline), map[string]any{"due_at": "jutro"}); code != http.StatusUnprocessableEntity || errorsOf(out)["due_at"] == nil {
|
|
t.Errorf("deadline with a bad time = %d %v", code, out)
|
|
}
|
|
if code, out = send(http.MethodPatch, "/deadlines/"+id(deadline), map[string]any{"course_id": nil}); code != http.StatusOK || data(out)["course_id"] != nil {
|
|
t.Errorf("deadline detached from the course = %d %v", code, out)
|
|
}
|
|
|
|
for _, path := range []string{"/readings/" + id(reading), "/deadlines/" + id(deadline), "/class-sessions/" + id(sess)} {
|
|
if code, _ := send(http.MethodDelete, path, nil); code != http.StatusNoContent {
|
|
t.Errorf("delete %s = %d", path, code)
|
|
}
|
|
if code, _ := send(http.MethodGet, path, nil); code != http.StatusNotFound {
|
|
t.Errorf("get %s after delete = %d", path, code)
|
|
}
|
|
}
|
|
}
|