package api // Regression tests for #86: title and language are bounded at create time. // Titles over 200 chars are truncated; language must match // ^[a-zA-Z0-9+#-]{1,40}$ or the create is rejected with a clear 400. import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" ) // pasteMeta fetches a created paste's stored metadata via the API. func pasteMeta(t *testing.T, h http.Handler, id string) map[string]any { t.Helper() rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest("GET", "/api/pastes/"+id, nil)) if rec.Code != 200 { t.Fatalf("get paste %s: got %d: %s", id, rec.Code, rec.Body.String()) } var m map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &m); err != nil { t.Fatal(err) } return m } // A 5000-char title is truncated to 200 characters at create time (#86). func TestCreatePasteTitleTruncated(t *testing.T) { s := testServer(t) h := s.routes() title := strings.Repeat("t", 5000) body, _ := json.Marshal(map[string]any{"content": "hi", "title": title}) rec := createPasteRaw(t, h, string(body)) if rec.Code != http.StatusCreated { t.Fatalf("got %d want 201: %s", rec.Code, rec.Body.String()) } var resp struct { ID string `json:"id"` } json.Unmarshal(rec.Body.Bytes(), &resp) meta := pasteMeta(t, h, resp.ID) got, _ := meta["title"].(string) if got != strings.Repeat("t", 200) { t.Fatalf("title not truncated to 200 chars: len=%d", len(got)) } } // A title within the 200-char bound is stored verbatim (minus surrounding // whitespace, which is trimmed). func TestCreatePasteTitleWithinBoundKept(t *testing.T) { s := testServer(t) h := s.routes() title := " " + strings.Repeat("x", 200) + " " body, _ := json.Marshal(map[string]any{"content": "hi", "title": title}) rec := createPasteRaw(t, h, string(body)) if rec.Code != http.StatusCreated { t.Fatalf("got %d want 201: %s", rec.Code, rec.Body.String()) } var resp struct { ID string `json:"id"` } json.Unmarshal(rec.Body.Bytes(), &resp) meta := pasteMeta(t, h, resp.ID) if got, _ := meta["title"].(string); got != strings.Repeat("x", 200) { t.Fatalf("title changed unexpectedly: len=%d", len(got)) } } // A language longer than 40 chars is rejected with a clear 400 (#86). func TestCreatePasteLanguageTooLong400(t *testing.T) { s := testServer(t) h := s.routes() body, _ := json.Marshal(map[string]any{"content": "hi", "language": strings.Repeat("a", 41)}) rec := createPasteRaw(t, h, string(body)) if rec.Code != http.StatusBadRequest { t.Fatalf("got %d want 400: %s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), "language") { t.Fatalf("unclear error message: %s", rec.Body.String()) } } // Language strings outside ^[a-zA-Z0-9+#-]{1,40}$ are rejected with 400. func TestCreatePasteLanguageBadFormat400(t *testing.T) { s := testServer(t) h := s.routes() for _, bad := range []string{ "", "java script", "c++ extra!", "py_thon", "go.lang", } { body, _ := json.Marshal(map[string]any{"content": "hi", "language": bad}) rec := createPasteRaw(t, h, string(body)) if rec.Code != http.StatusBadRequest { t.Errorf("language %q: got %d want 400: %s", bad, rec.Code, rec.Body.String()) continue } if !strings.Contains(rec.Body.String(), "language must match") { t.Errorf("language %q: unclear error: %s", bad, rec.Body.String()) } } } // Valid languages (letters, digits, #, +, -) within 40 chars are accepted. func TestCreatePasteLanguageValidAccepted(t *testing.T) { for _, ok := range []string{"go", "c#", "f#", "c++", "objective-c", "ECMAScript-2023", strings.Repeat("a", 40)} { s2 := testServer(t) // fresh rate limiter per case h2 := s2.routes() body, _ := json.Marshal(map[string]any{"content": "hi", "language": ok}) rec := createPasteRaw(t, h2, string(body)) if rec.Code != http.StatusCreated { t.Errorf("language %q: got %d want 201: %s", ok, rec.Code, rec.Body.String()) } } } // An absent or blank language still creates fine and stores NULL, and a // blank title is stored NULL rather than an empty string. func TestCreatePasteBlankMetadataOK(t *testing.T) { s := testServer(t) h := s.routes() for _, body := range []string{ `{"content":"hi"}`, `{"content":"hi","language":"","title":" "}`, } { rec := createPasteRaw(t, h, body) if rec.Code != http.StatusCreated { t.Fatalf("body %s: got %d want 201: %s", body, rec.Code, rec.Body.String()) } var resp struct { ID string `json:"id"` } json.Unmarshal(rec.Body.Bytes(), &resp) meta := pasteMeta(t, h, resp.ID) if lang, ok := meta["language"]; ok && lang != nil && lang != "" { t.Fatalf("body %s: language not null: %v", body, lang) } if title, ok := meta["title"]; ok && title != nil && title != "" { t.Fatalf("body %s: title not null: %v", body, title) } } }