package api import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "testing" "git.golem15.com/golem15/summercms/modules/bouncer" "gorm.io/gorm" ) func embedHandlers() *Handlers { return &Handlers{DB: func() *gorm.DB { return nil }, ChatGroup: "studenci", ChatModeratorGroup: "moderatorzy"} } func serveEmbed(t *testing.T, fn http.HandlerFunc, method, target, body string, userID uint) (int, map[string]any) { t.Helper() req := httptest.NewRequest(method, target, strings.NewReader(body)) if userID != 0 { req = req.WithContext(bouncer.WithUser(req.Context(), &bouncer.Principal{ID: userID})) } rec := httptest.NewRecorder() fn(rec, req) var out map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil { t.Fatalf("%s %s: body is not JSON: %v\n%s", method, target, err, rec.Body.String()) } return rec.Code, out } // TestChatEmbedRefs: the resolver accepts deadline, class and reading refs // with a positive id, collapses duplicates and takes at most 50 distinct // refs; materials and quizzes are not embed types (D-14). func TestChatEmbedRefs(t *testing.T) { ok := []embedRef{{"deadline", 1}, {"class", 2}, {"reading", 3}, {"deadline", 1}} got, errs := parseEmbedRefs(ok) if len(errs) != 0 { t.Fatalf("valid refs refused: %v", errs) } if want := []embedRef{{"deadline", 1}, {"class", 2}, {"reading", 3}}; fmt.Sprint(got) != fmt.Sprint(want) { t.Errorf("refs = %v, want %v (duplicates collapse, order kept)", got, want) } for name, refs := range map[string][]embedRef{ "material": {{"material", 1}}, "quiz": {{"quiz", 1}}, "empty": {{"", 1}}, "id 0": {{"deadline", 0}}, "51 refs": manyRefs(51), } { if _, errs := parseEmbedRefs(refs); len(errs) == 0 { t.Errorf("%s: accepted, want a validation error", name) } } if _, errs := parseEmbedRefs(manyRefs(50)); len(errs) != 0 { t.Errorf("50 refs refused: %v", errs) } dup := append(manyRefs(50), manyRefs(50)...) if got, errs := parseEmbedRefs(dup); len(errs) != 0 || len(got) != 50 { t.Errorf("50 distinct refs sent twice = %d refs, %v; want 50 and no error", len(got), errs) } if embedKey("deadline", 12) != "deadline:12" || embedKey("class", 3) != "class:3" { t.Errorf("embedKey = %q / %q", embedKey("deadline", 12), embedKey("class", 3)) } if embedSearchLimit != 20 { t.Errorf("embedSearchLimit = %d, want 20", embedSearchLimit) } } func manyRefs(n int) []embedRef { out := make([]embedRef, n) for i := range out { out[i] = embedRef{Type: "reading", ID: uint(i + 1)} } return out } // TestEscapeLike: %, _ and the backslash escape character are matched // literally in an ILIKE pattern. func TestEscapeLike(t *testing.T) { for in, want := range map[string]string{ `50%_a\b`: `50\%\_a\\b`, "plain": "plain", "": "", "Wstęp_100": `Wstęp\_100`, } { if got := escapeLike(in); got != want { t.Errorf("escapeLike(%q) = %q, want %q", in, got, want) } } } func TestChatEmbedsUnauthenticated(t *testing.T) { code, body := serveEmbed(t, embedHandlers().ChatEmbeds, http.MethodGet, "/?type=deadline", "", 0) if code != http.StatusUnauthorized || body["error"] != true { t.Errorf("GET /chat/embeds without a principal = %d %v, want 401", code, body) } } func TestResolveChatEmbedsUnauthenticated(t *testing.T) { code, body := serveEmbed(t, embedHandlers().ResolveChatEmbeds, http.MethodPost, "/", `{"refs":[{"type":"deadline","id":1}]}`, 0) if code != http.StatusUnauthorized || body["error"] != true { t.Errorf("POST /chat/embeds/resolve without a principal = %d %v, want 401", code, body) } } // TestChatEmbedsInvalid: the search validates type and q before the // database (422 without one). func TestChatEmbedsInvalid(t *testing.T) { h := embedHandlers() for target, field := range map[string]string{ "/": "type", "/?type=material": "type", "/?type=deadline&q=" + strings.Repeat("ą", 101): "q", } { code, body := serveEmbed(t, h.ChatEmbeds, http.MethodGet, target, "", 7) if code != http.StatusUnprocessableEntity { t.Errorf("%s = %d %v, want 422", target, code, body) continue } if errs, _ := body["errors"].(map[string]any); errs[field] == nil { t.Errorf("%s: errors %v have no %s", target, body["errors"], field) } } code, _ := serveEmbed(t, h.ChatEmbeds, http.MethodGet, "/?type=reading&q="+strings.Repeat("ą", 100), "", 7) if code != http.StatusServiceUnavailable { t.Errorf("100-rune q = %d, want to pass validation (503 without a database)", code) } } // TestResolveChatEmbedsInvalid: an unsupported type, id 0, too many refs, // an unknown field or a malformed body is 422 before the database. func TestResolveChatEmbedsInvalid(t *testing.T) { h := embedHandlers() many, _ := json.Marshal(map[string]any{"refs": manyRefs(51)}) for name, body := range map[string]string{ "material": `{"refs":[{"type":"material","id":1}]}`, "id 0": `{"refs":[{"type":"deadline","id":0}]}`, "51 refs": string(many), "unknown field": `{"refs":[],"extra":1}`, "not an object": `[1]`, "malformed": `{`, "empty body": ``, } { code, out := serveEmbed(t, h.ResolveChatEmbeds, http.MethodPost, "/", body, 7) if code != http.StatusUnprocessableEntity { t.Errorf("%s = %d %v, want 422", name, code, out) } } } // TestResolveChatEmbedsNoDatabase: valid refs pass validation and need the // database (503 without one). func TestResolveChatEmbedsNoDatabase(t *testing.T) { code, body := serveEmbed(t, embedHandlers().ResolveChatEmbeds, http.MethodPost, "/", `{"refs":[{"type":"deadline","id":1},{"type":"class","id":2},{"type":"reading","id":3}]}`, 7) if code != http.StatusServiceUnavailable { t.Errorf("valid refs without a database = %d %v, want 503", code, body) } }