f1bcb8cd3a
repospawner turns JSON new-repo requests into terraform-git pull requests via kubernetes Jobs, follows those PRs to merge and optionally activates the repository in Woodpecker.
147 lines
4.6 KiB
Go
147 lines
4.6 KiB
Go
package repospec
|
|
|
|
import (
|
|
"errors"
|
|
"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: "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
|
|
}
|