Files
sm-bm-plugin/controllers/api/material_pins_test.go
Jakub Zych 9436fb62d6 feat: personal pins on materials
A per-user jz_bm_material_pins table and PUT/DELETE /materials/{id}/pin
on the student API and its token twins: 204 for a material the caller
may see, idempotent, 404 otherwise. List rows, course rows and the
detail carry pinned. The plain list lists the caller's pins first
(newest pin first); with q the relevance order stays. A pin writes only
the pins table and never changes the material's revision.
2026-11-09 13:13:54 +01:00

65 lines
2.3 KiB
Go

package api_test
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.golem15.com/golem15/summercms/modules/bouncer"
"git.golem15.com/jakub/sm-bm-plugin/controllers/api"
"gorm.io/gorm"
)
// pinnedFirstQueries runs GET /materials?course=7&type=notatka as user on a
// dry-run database and returns its count and page queries.
func pinnedFirstQueries(t *testing.T, user uint) (string, string) {
t.Helper()
db, rec := dryDB(t)
h := &api.Handlers{DB: func() *gorm.DB { return db }}
req := httptest.NewRequest(http.MethodGet, "/materials?course=7&type=notatka", nil)
req = req.WithContext(bouncer.WithUser(req.Context(), &bouncer.Principal{ID: user}))
w := httptest.NewRecorder()
h.Materials(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status %d: %s", w.Code, w.Body.String())
}
var count, page string
for _, q := range rec.queries() {
switch {
case strings.HasPrefix(q, "SELECT count(*)"):
count = q
case strings.HasPrefix(q, "SELECT") && strings.Contains(q, `FROM "jz_bm_materials"`):
page = q
}
}
if count == "" || page == "" {
t.Fatalf("queries = %q, want a count and a page", rec.queries())
}
return count, page
}
// TestMaterialsPinnedFirstTrace checks the plain list's order: the caller's
// own pins first, then newest first with id as the tie-breaker, inside the
// same filters, while the count ignores pins.
func TestMaterialsPinnedFirstTrace(t *testing.T) {
count, page := pinnedFirstQueries(t, 5)
for name, q := range map[string]string{"count": count, "page": page} {
for _, want := range []string{"jz_bm_materials.course_id = 7", "jz_bm_materials.type = 'notatka'", "jz_bm_materials.is_private = FALSE"} {
if !strings.Contains(q, want) {
t.Errorf("%s query lacks %q:\n%s", name, want, q)
}
}
}
if want := "p.user_id = 5) DESC NULLS LAST, jz_bm_materials.published_at DESC, jz_bm_materials.id DESC"; !strings.Contains(page, want) {
t.Errorf("page query is not pinned first for user 5, want %q:\n%s", want, page)
}
if strings.Contains(count, "jz_bm_material_pins") {
t.Errorf("count query reads pins:\n%s", count)
}
_, other := pinnedFirstQueries(t, 6)
if !strings.Contains(other, "p.user_id = 6)") || strings.Contains(other, "p.user_id = 5") {
t.Errorf("page query of user 6 does not use only their pins:\n%s", other)
}
}