package raw_test import ( "testing" validation "github.com/go-ozzo/ozzo-validation" version "github.com/hashicorp/go-version" "github.com/runatlantis/atlantis/server/core/config/raw" "github.com/runatlantis/atlantis/server/core/config/valid" . "github.com/runatlantis/atlantis/testing" ) func TestProject_UnmarshalYAML(t *testing.T) { cases := []struct { description string input string exp raw.Project }{ { description: "omit unset fields", input: "", exp: raw.Project{ Dir: nil, Workspace: nil, Workflow: nil, TerraformVersion: nil, Autoplan: nil, PlanRequirements: nil, ApplyRequirements: nil, ImportRequirements: nil, Name: nil, Branch: nil, }, }, { description: "all fields set including mergeable apply requirement", input: ` name: myname branch: mybranch dir: mydir workspace: workspace workflow: workflow terraform_version: v0.11.0 autoplan: when_modified: [] enabled: false plan_requirements: - mergeable apply_requirements: - mergeable import_requirements: - mergeable execution_order_group: 10`, exp: raw.Project{ Name: String("myname"), Branch: String("mybranch"), Dir: String("mydir"), Workspace: String("workspace"), Workflow: String("workflow"), TerraformVersion: String("v0.11.0"), Autoplan: &raw.Autoplan{ WhenModified: []string{}, Enabled: Bool(false), }, PlanRequirements: []string{"mergeable"}, ApplyRequirements: []string{"mergeable"}, ImportRequirements: []string{"mergeable"}, ExecutionOrderGroup: Int(10), }, }, } for _, c := range cases { t.Run(c.description, func(t *testing.T) { var p raw.Project err := unmarshalString(c.input, &p) Ok(t, err) Equals(t, c.exp, p) }) } } func TestProject_Validate(t *testing.T) { cases := []struct { description string input raw.Project expErr string }{ { description: "minimal fields", input: raw.Project{ Dir: String("."), }, expErr: "", }, { description: "dir empty", input: raw.Project{ Dir: nil, }, expErr: "dir: cannot be blank.", }, { description: "dir with ..", input: raw.Project{ Dir: String("../mydir"), }, expErr: "dir: cannot contain '..'.", }, { description: "not a regexp for branch", input: raw.Project{ Branch: String("text"), Dir: String("."), }, expErr: "branch: regex must begin and end with a slash '/'.", }, { description: "invalid regexp for branch", input: raw.Project{ Branch: String("/(text/"), Dir: String("."), }, expErr: "branch: parsing: /(text/: error parsing regexp: missing closing ): `(text`.", }, { description: "plan reqs with unsupported", input: raw.Project{ Dir: String("."), PlanRequirements: []string{"unsupported"}, }, expErr: "plan_requirements: \"unsupported\" is not a valid plan_requirement, only \"approved\", \"mergeable\" and \"undiverged\" are supported.", }, { description: "plan reqs with undiverged, mergeable and approved requirements", input: raw.Project{ Dir: String("."), PlanRequirements: []string{"undiverged", "mergeable", "approved"}, }, expErr: "", }, { description: "plan reqs with approved requirement", input: raw.Project{ Dir: String("."), PlanRequirements: []string{"approved"}, }, expErr: "", }, { description: "plan reqs with mergeable requirement", input: raw.Project{ Dir: String("."), PlanRequirements: []string{"mergeable"}, }, expErr: "", }, { description: "plan reqs with mergeable and approved requirements", input: raw.Project{ Dir: String("."), PlanRequirements: []string{"mergeable", "approved"}, }, expErr: "", }, { description: "apply reqs with unsupported", input: raw.Project{ Dir: String("."), ApplyRequirements: []string{"unsupported"}, }, expErr: "apply_requirements: \"unsupported\" is not a valid apply_requirement, only \"approved\", \"mergeable\" and \"undiverged\" are supported.", }, { description: "apply reqs with approved requirement", input: raw.Project{ Dir: String("."), ApplyRequirements: []string{"approved"}, }, expErr: "", }, { description: "apply reqs with mergeable requirement", input: raw.Project{ Dir: String("."), ApplyRequirements: []string{"mergeable"}, }, expErr: "", }, { description: "apply reqs with undiverged requirement", input: raw.Project{ Dir: String("."), ApplyRequirements: []string{"undiverged"}, }, expErr: "", }, { description: "apply reqs with mergeable and approved requirements", input: raw.Project{ Dir: String("."), ApplyRequirements: []string{"mergeable", "approved"}, }, expErr: "", }, { description: "apply reqs with undiverged and approved requirements", input: raw.Project{ Dir: String("."), ApplyRequirements: []string{"undiverged", "approved"}, }, expErr: "", }, { description: "apply reqs with undiverged and mergeable requirements", input: raw.Project{ Dir: String("."), ApplyRequirements: []string{"undiverged", "mergeable"}, }, expErr: "", }, { description: "apply reqs with undiverged, mergeable and approved requirements", input: raw.Project{ Dir: String("."), ApplyRequirements: []string{"undiverged", "mergeable", "approved"}, }, expErr: "", }, { description: "import reqs with unsupported", input: raw.Project{ Dir: String("."), ImportRequirements: []string{"unsupported"}, }, expErr: "import_requirements: \"unsupported\" is not a valid import_requirement, only \"approved\", \"mergeable\" and \"undiverged\" are supported.", }, { description: "import reqs with undiverged, mergeable and approved requirements", input: raw.Project{ Dir: String("."), ImportRequirements: []string{"undiverged", "mergeable", "approved"}, }, expErr: "", }, { description: "empty tf version string", input: raw.Project{ Dir: String("."), TerraformVersion: String(""), }, expErr: "terraform_version: version \"\" could not be parsed: Malformed version: .", }, { description: "tf version with v prepended", input: raw.Project{ Dir: String("."), TerraformVersion: String("v1"), }, expErr: "", }, { description: "tf version without prepended v", input: raw.Project{ Dir: String("."), TerraformVersion: String("1"), }, expErr: "", }, { description: "empty string for project name", input: raw.Project{ Dir: String("."), Name: String(""), }, expErr: "name: if set cannot be empty.", }, { description: "project name with slashes", input: raw.Project{ Dir: String("."), Name: String("my/name"), }, expErr: "", }, { description: "project name with emoji", input: raw.Project{ Dir: String("."), Name: String("😀"), }, expErr: "name: \"😀\" is not allowed: must contain only URL safe characters.", }, { description: "project name with spaces", input: raw.Project{ Dir: String("."), Name: String("name with spaces"), }, expErr: "name: \"name with spaces\" is not allowed: must contain only URL safe characters.", }, { description: "project name with +", input: raw.Project{ Dir: String("."), Name: String("namewith+"), }, expErr: "name: \"namewith+\" is not allowed: must contain only URL safe characters.", }, { description: `project name with \`, input: raw.Project{ Dir: String("."), Name: String(`namewith\`), }, expErr: `name: "namewith\\" is not allowed: must contain only URL safe characters.`, }, } validation.ErrorTag = "yaml" for _, c := range cases { t.Run(c.description, func(t *testing.T) { err := c.input.Validate() if c.expErr == "" { Ok(t, err) } else { ErrEquals(t, c.expErr, err) } }) } } func TestProject_ToValid(t *testing.T) { tfVersionPointEleven, _ := version.NewVersion("v0.11.0") repoLocksOnApply := valid.RepoLocksOnApplyMode cases := []struct { description string input raw.Project exp valid.Project }{ { description: "minimal values", input: raw.Project{ Dir: String("."), }, exp: valid.Project{ Dir: ".", BranchRegex: nil, Workspace: "default", WorkflowName: nil, TerraformVersion: nil, Autoplan: valid.Autoplan{ WhenModified: raw.DefaultAutoPlanWhenModified, Enabled: true, }, ApplyRequirements: nil, Name: nil, }, }, { description: "all set", input: raw.Project{ Dir: String("."), Workspace: String("myworkspace"), Workflow: String("myworkflow"), TerraformVersion: String("v0.11.0"), Autoplan: &raw.Autoplan{ WhenModified: []string{"hi"}, Enabled: Bool(false), }, RepoLocks: &raw.RepoLocks{ Mode: &repoLocksOnApply, }, ApplyRequirements: []string{"approved"}, Name: String("myname"), ExecutionOrderGroup: Int(10), }, exp: valid.Project{ Dir: ".", Workspace: "myworkspace", WorkflowName: String("myworkflow"), TerraformVersion: tfVersionPointEleven, Autoplan: valid.Autoplan{ WhenModified: []string{"hi"}, Enabled: false, }, RepoLocks: &valid.RepoLocks{ Mode: repoLocksOnApply, }, ApplyRequirements: []string{"approved"}, Name: String("myname"), ExecutionOrderGroup: 10, }, }, { description: "tf version without 'v'", input: raw.Project{ Dir: String("."), TerraformVersion: String("0.11.0"), }, exp: valid.Project{ Dir: ".", Workspace: "default", TerraformVersion: tfVersionPointEleven, Autoplan: valid.Autoplan{ WhenModified: raw.DefaultAutoPlanWhenModified, Enabled: true, }, }, }, // Directories. { description: "dir set to /", input: raw.Project{ Dir: String("/"), }, exp: valid.Project{ Dir: ".", Workspace: "default", Autoplan: valid.Autoplan{ WhenModified: raw.DefaultAutoPlanWhenModified, Enabled: true, }, }, }, { description: "dir starting with /", input: raw.Project{ Dir: String("/a/b/c"), }, exp: valid.Project{ Dir: "a/b/c", Workspace: "default", Autoplan: valid.Autoplan{ WhenModified: raw.DefaultAutoPlanWhenModified, Enabled: true, }, }, }, { description: "dir with trailing slash", input: raw.Project{ Dir: String("mydir/"), }, exp: valid.Project{ Dir: "mydir", Workspace: "default", Autoplan: valid.Autoplan{ WhenModified: raw.DefaultAutoPlanWhenModified, Enabled: true, }, }, }, { description: "unclean dir", input: raw.Project{ // This won't actually be allowed since it doesn't validate. Dir: String("./mydir/anotherdir/../"), }, exp: valid.Project{ Dir: "mydir", Workspace: "default", Autoplan: valid.Autoplan{ WhenModified: raw.DefaultAutoPlanWhenModified, Enabled: true, }, }, }, { description: "dir set to ./", input: raw.Project{ Dir: String("./"), }, exp: valid.Project{ Dir: ".", Workspace: "default", Autoplan: valid.Autoplan{ WhenModified: raw.DefaultAutoPlanWhenModified, Enabled: true, }, }, }, { description: "dir set to ././", input: raw.Project{ Dir: String("././"), }, exp: valid.Project{ Dir: ".", Workspace: "default", Autoplan: valid.Autoplan{ WhenModified: raw.DefaultAutoPlanWhenModified, Enabled: true, }, }, }, { description: "dir set to .", input: raw.Project{ Dir: String("."), }, exp: valid.Project{ Dir: ".", Workspace: "default", Autoplan: valid.Autoplan{ WhenModified: raw.DefaultAutoPlanWhenModified, Enabled: true, }, }, }, { description: "workspace set to empty string", input: raw.Project{ Dir: String("."), Workspace: String(""), }, exp: valid.Project{ Dir: ".", Workspace: "default", Autoplan: valid.Autoplan{ WhenModified: raw.DefaultAutoPlanWhenModified, Enabled: true, }, }, }, } for _, c := range cases { t.Run(c.description, func(t *testing.T) { Equals(t, c.exp, c.input.ToValid()) }) } }