// Tests for input validation: body size limits, username/password limits, // tag name validation, and LIKE search wildcard escaping. package e2e import ( "net/http" "net/url" "strings" "testing" ) func TestValidation(t *testing.T) { e := newEnv(t) admin := e.admin() cfg := e.Cfg e.createRepo(admin, "val-repo") e.seedRepo("val-repo", nil) // A baseline issue gives the comment tests a URL. issueURL := admin.post("/val-repo/issues", url.Values{ "title": {"Baseline issue"}, "body": {"ok"}, }).mustRedirect("/val-repo/issues/") // ─── Body size limits ────────────────────────────────────────────────── t.Run("issue body at limit is accepted", func(t *testing.T) { admin.post("/val-repo/issues", url.Values{ "title": {"Body at limit"}, "body": {strings.Repeat("x", cfg.MaxTextBodyBytes)}, }).mustStatus(http.StatusFound) }) t.Run("issue body over limit is rejected", func(t *testing.T) { admin.post("/val-repo/issues", url.Values{ "title": {"Body over limit"}, "body": {strings.Repeat("x", cfg.MaxTextBodyBytes+1)}, }).mustStatus(http.StatusUnprocessableEntity) }) t.Run("issue title at limit is accepted", func(t *testing.T) { admin.post("/val-repo/issues", url.Values{ "title": {strings.Repeat("x", cfg.MaxTitleBytes)}, "body": {"ok"}, }).mustStatus(http.StatusFound) }) t.Run("issue title over limit is rejected", func(t *testing.T) { admin.post("/val-repo/issues", url.Values{ "title": {strings.Repeat("x", cfg.MaxTitleBytes+1)}, "body": {"ok"}, }).mustStatus(http.StatusUnprocessableEntity) }) t.Run("issue comment body at limit is accepted", func(t *testing.T) { admin.post(issueURL+"/comments", url.Values{ "body": {strings.Repeat("x", cfg.MaxTextBodyBytes)}, }).mustStatus(http.StatusFound) }) t.Run("issue comment body over limit is rejected", func(t *testing.T) { admin.post(issueURL+"/comments", url.Values{ "body": {strings.Repeat("x", cfg.MaxTextBodyBytes+1)}, }).mustStatus(http.StatusUnprocessableEntity) }) t.Run("patch description at limit is accepted", func(t *testing.T) { // No patch file, so the business logic rejects it. The schema check // must still pass, which means anything but 422. r := admin.post("/val-repo/patches", url.Values{ "title": {"Patch ok"}, "description": {strings.Repeat("x", cfg.MaxTextBodyBytes)}, }) if r.Code == http.StatusUnprocessableEntity { t.Errorf("status = %d", r.Code) } }) t.Run("patch description over limit is rejected", func(t *testing.T) { admin.post("/val-repo/patches", url.Values{ "title": {"Patch bad"}, "description": {strings.Repeat("x", cfg.MaxTextBodyBytes+1)}, }).mustStatus(http.StatusUnprocessableEntity) }) t.Run("patch title over limit is rejected", func(t *testing.T) { admin.post("/val-repo/patches", url.Values{ "title": {strings.Repeat("x", cfg.MaxTitleBytes+1)}, }).mustStatus(http.StatusUnprocessableEntity) }) // ─── Auth limits ─────────────────────────────────────────────────────── t.Run("username over limit is rejected at registration", func(t *testing.T) { e.anon().post("/register", url.Values{ "username": {strings.Repeat("u", cfg.MaxUsernameBytes+1)}, "password": {"validpass1"}, "password2": {"validpass1"}, }).mustStatus(http.StatusUnprocessableEntity) }) t.Run("username at limit is not schema-rejected", func(t *testing.T) { // A username at exactly the limit passes the schema check. It may // still fail on uniqueness or format, so only 422 is wrong. r := e.anon().post("/register", url.Values{ "username": {strings.Repeat("a", cfg.MaxUsernameBytes)}, "password": {"validpass1"}, "password2": {"validpass1"}, }) if r.Code == http.StatusUnprocessableEntity { t.Errorf("status = %d", r.Code) } }) t.Run("password over limit is rejected at registration", func(t *testing.T) { long := strings.Repeat("p", cfg.MaxPasswordBytes+1) e.anon().post("/register", url.Values{ "username": {"newuser"}, "password": {long}, "password2": {long}, }).mustStatus(http.StatusUnprocessableEntity) }) t.Run("new_password over limit is rejected at settings/password", func(t *testing.T) { long := strings.Repeat("p", cfg.MaxPasswordBytes+1) admin.post("/settings/password", url.Values{ "current_password": {adminPass}, "new_password": {long}, "confirm_password": {long}, }).mustStatus(http.StatusUnprocessableEntity) }) // ─── Tag name validation ─────────────────────────────────────────────── for _, tag := range []string{"v1.0.0", "release-2", "1.0+build.1", "v1_alpha"} { t.Run("valid tag "+tag+" is accepted", func(t *testing.T) { // 302 on success, 200 with a form error (e.g. tag exists) is also // fine. Only a 422 schema error is wrong. r := admin.post("/val-repo/releases", url.Values{ "create_tag": {"on"}, "tag_name": {tag}, "revision": {"main"}, "name": {"Release " + tag}, }) if r.Code == http.StatusUnprocessableEntity { t.Errorf("status = %d", r.Code) } }) } for _, tag := range []string{"v1.0~1", "tag with space", "v1:2", "v1^2", "ref/head", "v1?", "v1*"} { t.Run("invalid tag "+tag+" is rejected", func(t *testing.T) { r := admin.post("/val-repo/releases", url.Values{ "create_tag": {"on"}, "tag_name": {tag}, "revision": {"main"}, "name": {"Release " + tag}, }).mustStatus(http.StatusOK) if !r.Contains("may only contain") { t.Error("inline form error missing") } }) } // ─── LIKE wildcard escaping in repo search ───────────────────────────── t.Run("search for _ returns only repos with literal underscore", func(t *testing.T) { e.createRepo(admin, "search-under_score") e.createRepo(admin, "search-nodash") body := admin.get("/?q=" + url.QueryEscape("_")).BodyString() if !strings.Contains(body, "search-under_score") { t.Error("search-under_score missing") } for _, bad := range []string{"search-nodash", "val-repo"} { if strings.Contains(body, bad) { t.Errorf("body contains %q", bad) } } }) t.Run("search for % returns no repos", func(t *testing.T) { body := admin.get("/?q=" + url.QueryEscape("%")).BodyString() for _, bad := range []string{"search-under_score", "search-nodash", "val-repo"} { if strings.Contains(body, bad) { t.Errorf("body contains %q", bad) } } }) t.Run("normal substring search still works", func(t *testing.T) { body := admin.get("/?q=search-under").BodyString() if !strings.Contains(body, "search-under_score") { t.Error("search-under_score missing") } if strings.Contains(body, "search-nodash") { t.Error("body contains search-nodash") } }) }