package repospec import ( "errors" "strconv" "strings" "testing" ) func TestValidate(t *testing.T) { base := Request{Name: "widget", Description: "does widgets", StatusChecks: []string{"ci/woodpecker/pr/test"}} cases := []struct { name string req Request fields []string wantErr bool }{ {name: "valid", req: base}, {name: "valid with digits and dashes", req: with(base, func(r *Request) { r.Name = "arr-proxy2" })}, {name: "missing name", req: with(base, func(r *Request) { r.Name = "" }), fields: []string{"name"}, wantErr: true}, {name: "uppercase name", req: with(base, func(r *Request) { r.Name = "Widget" }), fields: []string{"name"}, wantErr: true}, {name: "underscore name", req: with(base, func(r *Request) { r.Name = "wid_get" }), fields: []string{"name"}, wantErr: true}, {name: "leading dash", req: with(base, func(r *Request) { r.Name = "-widget" }), fields: []string{"name"}, wantErr: true}, {name: "trailing dash", req: with(base, func(r *Request) { r.Name = "widget-" }), fields: []string{"name"}, wantErr: true}, {name: "path traversal", req: with(base, func(r *Request) { r.Name = "../etc/passwd" }), fields: []string{"name"}, wantErr: true}, {name: "over long", req: with(base, func(r *Request) { r.Name = strings.Repeat("a", maxNameLen+1) }), fields: []string{"name"}, wantErr: true}, {name: "missing description", req: with(base, func(r *Request) { r.Description = "" }), fields: []string{"description"}, wantErr: true}, {name: "no checks", req: with(base, func(r *Request) { r.StatusChecks = nil }), fields: []string{"status_checks"}, wantErr: true}, { name: "over long description", req: with(base, func(r *Request) { r.Description = strings.Repeat("d", maxDescriptionLen+1) }), fields: []string{"description"}, wantErr: true, }, { name: "description at the cap", req: with(base, func(r *Request) { r.Description = strings.Repeat("d", maxDescriptionLen) }), }, { name: "check containing a comma", req: with(base, func(r *Request) { r.StatusChecks = []string{"ci/woodpecker/pr/test,ci/woodpecker/pr/build"} }), fields: []string{"status_checks"}, wantErr: true, }, { name: "check containing a quote", req: with(base, func(r *Request) { r.StatusChecks = []string{`ci/"test"`} }), fields: []string{"status_checks"}, wantErr: true, }, { name: "over long check", req: with(base, func(r *Request) { r.StatusChecks = []string{strings.Repeat("c", maxCheckLen+1)} }), fields: []string{"status_checks"}, wantErr: true, }, { name: "check at the cap", req: with(base, func(r *Request) { r.StatusChecks = []string{strings.Repeat("c", maxCheckLen)} }), }, { name: "too many checks", req: with(base, func(r *Request) { r.StatusChecks = manyChecks(maxChecks + 1) }), fields: []string{"status_checks"}, wantErr: true, }, {name: "checks at the cap", req: with(base, func(r *Request) { r.StatusChecks = manyChecks(maxChecks) })}, { name: "every field bad at once", req: Request{}, fields: []string{"name", "description", "status_checks"}, wantErr: true, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { err := tc.req.Validate() if tc.wantErr == (err == nil) { t.Fatalf("Validate() = %v, wantErr %v", err, tc.wantErr) } if err == nil { return } var fe FieldErrors if !errors.As(err, &fe) { t.Fatalf("error %v is not FieldErrors", err) } if len(fe) != len(tc.fields) { t.Fatalf("fields = %v, want exactly %v", fe, tc.fields) } for _, f := range tc.fields { if _, ok := fe[f]; !ok { t.Errorf("missing field error for %q; got %v", f, fe) } } }) } } func TestNormalize(t *testing.T) { r := Request{ Name: " widget \n", Description: " does widgets ", StatusChecks: []string{" a ", "", "b", "a", " "}, }.Normalize() if r.Name != "widget" { t.Errorf("Name = %q", r.Name) } if r.Description != "does widgets" { t.Errorf("Description = %q", r.Description) } want := []string{"a", "b"} if len(r.StatusChecks) != len(want) { t.Fatalf("StatusChecks = %v, want %v", r.StatusChecks, want) } for i := range want { if r.StatusChecks[i] != want[i] { t.Fatalf("StatusChecks = %v, want %v", r.StatusChecks, want) } } } func TestPathsAndBranch(t *testing.T) { r := Request{Name: "widget"} if got, want := r.ConfigPath(), "config/git.unkin.net/unkin/repository/widget.yaml"; got != want { t.Errorf("ConfigPath() = %q, want %q", got, want) } if got, want := r.BranchName(), "repospawner/widget"; got != want { t.Errorf("BranchName() = %q, want %q", got, want) } } // golden pins the file terraform-git receives; a drift here changes estate // policy for every repository repospawner creates. const golden = `description: "Keyboard-centric widget service" private: false default_branch: "main" default_delete_branch_after_merge: true default_merge_style: "squash" branch_protection: - rule_name: "main" merge_whitelist_teams: - "Owners" enable_push: false status_check_contexts: - "ci/woodpecker/pr/build" - "ci/woodpecker/pr/test" - "ci/woodpecker/pr/pre-commit" approval_whitelist_users: - "benvin" ` func TestRenderYAMLGolden(t *testing.T) { got := Request{ Name: "widget", Description: "Keyboard-centric widget service", StatusChecks: []string{ "ci/woodpecker/pr/build", "ci/woodpecker/pr/test", "ci/woodpecker/pr/pre-commit", }, }.RenderYAML() if got != golden { t.Errorf("RenderYAML() mismatch\n got:\n%s\nwant:\n%s", got, golden) } } func TestRenderYAMLQuotesDescription(t *testing.T) { got := Request{ Name: "widget", Description: `a "quoted" \ description`, StatusChecks: []string{"x"}, }.RenderYAML() want := `description: "a \"quoted\" \\ description"` if !strings.Contains(got, want) { t.Errorf("RenderYAML() did not escape the description; got first line %q", strings.SplitN(got, "\n", 2)[0]) } } func with(r Request, f func(*Request)) Request { f(&r) return r } func manyChecks(n int) []string { out := make([]string, 0, n) for i := range n { out = append(out, "ci/woodpecker/pr/check"+strconv.Itoa(i)) } return out }