180 lines
6.1 KiB
Go
180 lines
6.1 KiB
Go
package api_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"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"
|
|
)
|
|
|
|
// noDB is a Handlers whose database is never published, so a request that
|
|
// gets past authentication and validation answers 503.
|
|
func noDB() *api.Handlers {
|
|
return &api.Handlers{DB: func() *gorm.DB { return nil }}
|
|
}
|
|
|
|
func serve(t *testing.T, h http.HandlerFunc, method, target string, user *bouncer.Principal, pathValues map[string]string) (int, map[string]any) {
|
|
t.Helper()
|
|
req := httptest.NewRequest(method, target, nil)
|
|
if user != nil {
|
|
req = req.WithContext(bouncer.WithUser(req.Context(), user))
|
|
}
|
|
for k, v := range pathValues {
|
|
req.SetPathValue(k, v)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
h(rec, req)
|
|
var body map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("%s %s: body is not JSON: %v\n%s", method, target, err, rec.Body.String())
|
|
}
|
|
return rec.Code, body
|
|
}
|
|
|
|
func handlers(h *api.Handlers) map[string]http.HandlerFunc {
|
|
return map[string]http.HandlerFunc{
|
|
"dashboard": h.Dashboard,
|
|
"courses": h.Courses,
|
|
"course": h.Course,
|
|
"schedule": h.Schedule,
|
|
"meetings": h.Meetings,
|
|
"deadlines": h.Deadlines,
|
|
"readings": h.Readings,
|
|
"mark": h.MarkRead,
|
|
"unmark": h.UnmarkRead,
|
|
"materials": h.Materials,
|
|
"material": h.Material,
|
|
"feed": h.CalendarFeed,
|
|
"feed-put": h.UpdateCalendarFeed,
|
|
"rotate": h.RotateCalendarFeed,
|
|
"feed-del": h.DeleteCalendarFeed,
|
|
}
|
|
}
|
|
|
|
func TestEveryHandlerNeedsACaller(t *testing.T) {
|
|
for name, fn := range handlers(noDB()) {
|
|
code, body := serve(t, fn, http.MethodGet, "/", nil, map[string]string{"id": "1"})
|
|
if code != http.StatusUnauthorized || body["error"] != true {
|
|
t.Errorf("%s without a principal = %d %v, want 401", name, code, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEveryHandlerNeedsADatabase(t *testing.T) {
|
|
user := &bouncer.Principal{ID: 7}
|
|
for name, fn := range handlers(noDB()) {
|
|
code, body := serve(t, fn, http.MethodGet, "/", user, map[string]string{"id": "1"})
|
|
if code != http.StatusServiceUnavailable {
|
|
t.Errorf("%s without a database = %d %v, want 503", name, code, body)
|
|
}
|
|
}
|
|
// A nil DB func is the same as an unpublished database.
|
|
code, _ := serve(t, (&api.Handlers{}).Meetings, http.MethodGet, "/", user, nil)
|
|
if code != http.StatusServiceUnavailable {
|
|
t.Errorf("nil DB func = %d, want 503", code)
|
|
}
|
|
}
|
|
|
|
func TestQueryValidation(t *testing.T) {
|
|
h := noDB()
|
|
user := &bouncer.Principal{ID: 7}
|
|
cases := []struct {
|
|
name string
|
|
fn http.HandlerFunc
|
|
target string
|
|
field string
|
|
}{
|
|
{"schedule from not a date", h.Schedule, "/?from=jutro", "from"},
|
|
{"schedule from impossible date", h.Schedule, "/?from=2026-02-30", "from"},
|
|
{"schedule from not ISO", h.Schedule, "/?from=03.10.2026", "from"},
|
|
{"schedule to before from", h.Schedule, "/?from=2026-10-10&to=2026-10-01", "to"},
|
|
{"deadlines to not a date", h.Deadlines, "/?to=10/01/2026", "to"},
|
|
{"readings course not a number", h.Readings, "/?course=abc", "course"},
|
|
{"readings course zero", h.Readings, "/?course=0", "course"},
|
|
{"courses semester out of range", h.Courses, "/?semester=9", "semester"},
|
|
{"materials type unknown", h.Materials, "/?type=video", "type"},
|
|
{"materials page zero", h.Materials, "/?page=0", "page"},
|
|
{"materials per_page too large", h.Materials, "/?per_page=1000", "per_page"},
|
|
{"materials course negative", h.Materials, "/?course=-1", "course"},
|
|
{"materials tag too long", h.Materials, "/?tag=" + longTag(), "tag"},
|
|
}
|
|
for _, tc := range cases {
|
|
code, body := serve(t, tc.fn, http.MethodGet, tc.target, user, nil)
|
|
if code != http.StatusUnprocessableEntity {
|
|
t.Errorf("%s: status %d %v, want 422", tc.name, code, body)
|
|
continue
|
|
}
|
|
errs, _ := body["errors"].(map[string]any)
|
|
if msgs, _ := errs[tc.field].([]any); len(msgs) == 0 {
|
|
t.Errorf("%s: errors %v have no %s message", tc.name, errs, tc.field)
|
|
}
|
|
if msg, _ := body["message"].(string); msg == "" {
|
|
t.Errorf("%s: no message in %v", tc.name, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func longTag() string {
|
|
b := make([]byte, 101)
|
|
for i := range b {
|
|
b[i] = 'a'
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func TestValidQueriesReachTheDatabase(t *testing.T) {
|
|
h := noDB()
|
|
user := &bouncer.Principal{ID: 7}
|
|
for _, tc := range []struct {
|
|
fn http.HandlerFunc
|
|
target string
|
|
}{
|
|
{h.Schedule, "/?from=2026-10-01&to=2026-10-31"},
|
|
{h.Schedule, "/?to=2026-10-31"},
|
|
{h.Schedule, "/?from=2026-10-01&to=2026-10-01"},
|
|
{h.Deadlines, "/"},
|
|
{h.Readings, "/?course=3"},
|
|
{h.Courses, "/?semester=2"},
|
|
{h.Materials, "/?course=1&tag=geopolityka&type=pdf&page=2&per_page=10"},
|
|
} {
|
|
if code, body := serve(t, tc.fn, http.MethodGet, tc.target, user, nil); code != http.StatusServiceUnavailable {
|
|
t.Errorf("%s = %d %v, want to pass validation (503 without a database)", tc.target, code, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCalendarICSRejectsMalformedTokens(t *testing.T) {
|
|
h := noDB()
|
|
good := strings.Repeat("ab", 32)
|
|
for _, token := range []string{"", "abc", good[:63], good + "0", strings.ToUpper(good), good[:62] + "zz", good + ".ical", "../" + good} {
|
|
code, body := serve(t, h.CalendarICS, http.MethodGet, "/", nil, map[string]string{"token": token})
|
|
if code != http.StatusNotFound {
|
|
t.Errorf("token %q = %d %v, want 404 without a database lookup", token, code, body)
|
|
}
|
|
}
|
|
// A well-formed token, with or without .ics, is looked up (503 here).
|
|
for _, token := range []string{good, good + ".ics"} {
|
|
if code, _ := serve(t, h.CalendarICS, http.MethodGet, "/", nil, map[string]string{"token": token}); code != http.StatusServiceUnavailable {
|
|
t.Errorf("token %q = %d, want 503 without a database", token, code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUpdateCalendarFeedRejectsAnInvalidBody(t *testing.T) {
|
|
h := noDB()
|
|
for _, body := range []string{"[1,2]", "{", `"x"`} {
|
|
req := httptest.NewRequest(http.MethodPut, "/", strings.NewReader(body))
|
|
req = req.WithContext(bouncer.WithUser(req.Context(), &bouncer.Principal{ID: 7}))
|
|
rec := httptest.NewRecorder()
|
|
h.UpdateCalendarFeed(rec, req)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("body %s = %d %s, want 400", body, rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
}
|