diff --git a/pkg/server/ptypes/project_template.go b/pkg/server/ptypes/project_template.go new file mode 100644 index 00000000000..6b162acf0a1 --- /dev/null +++ b/pkg/server/ptypes/project_template.go @@ -0,0 +1,121 @@ +package ptypes + +import ( + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/imdario/mergo" + "github.com/mitchellh/go-testing-interface" + "github.com/stretchr/testify/require" + "regexp" + + "github.com/hashicorp/waypoint/internal/pkg/validationext" + pb "github.com/hashicorp/waypoint/pkg/server/gen" +) + +const ( + PROJECT_TEMPLATE_ID_LENGTH = 64 + PROJECT_TEMPLATE_NAME_LENGTH = 64 + PROJECT_TEMPLATE_TAG_LENGTH = 64 + PROJECT_TEMPLATE_SUMMARY_LENGTH = 64 + PROJECT_TEMPLATE_EXPANDED_SUMMARY_LENGTH = 512 + PROJECT_TEMPLATE_README_LENGTH = 1024 ^ 2 + PROJECT_TEMPLATE_WAYPOINT_HCL_LENGTH = 1024 ^ 2 + + TERRAFORM_NOCODE_MODULE_SOURCE_LENGTH = 256 + TERRAFORM_NOCODE_MODULE_VERSION_LENGTH = 256 +) + +func TestProjectTemplate(t testing.T, src *pb.ProjectTemplate) *pb.ProjectTemplate { + t.Helper() + + if src == nil { + src = &pb.ProjectTemplate{} + } + + require.NoError(t, mergo.Merge(src, &pb.Project{ + Name: "test", + })) + + return src +} + +func ValidateCreateProjectTemplateRequest(req *pb.CreateProjectTemplateRequest) error { + return validationext.Error(validation.ValidateStruct(req, + validation.Field(&req.ProjectTemplate, validation.Required), + validationext.StructField(&req.ProjectTemplate, func() []*validation.FieldRules { + return append( + // Rules specific to creating a project template + []*validation.FieldRules{ + validation.Field(&req.ProjectTemplate.Name, + validation.Required, + validation.Match(regexp.MustCompile(`\S+`)), // Disallow only whitespace + ), + }, + + // General project template validation rules + validateProjectTemplateRules(req.ProjectTemplate)..., + ) + }), + )) +} + +func ValidateUpdateProjectTemplateRequest(req *pb.UpdateProjectTemplateRequest) error { + return validationext.Error(validation.ValidateStruct(req, + validation.Field(&req.ProjectTemplate, validation.Required), + validationext.StructField(&req.ProjectTemplate, func() []*validation.FieldRules { + return append( + // Rules specific to creating a project template + []*validation.FieldRules{ + // Require either Name or ID + validation.Field(&req.ProjectTemplate.Id, validation.Required.When(req.ProjectTemplate.Name == "").Error("Either Name or ID is required.")), + validation.Field(&req.ProjectTemplate.Name, validation.Required.When(req.ProjectTemplate.Id == "").Error("Either Name or ID is required.")), + }, + + // General project template validation rules + validateProjectTemplateRules(req.ProjectTemplate)..., + ) + }), + )) +} + +// validateProjectTemplateRules validates the rules that must be true of any project template in any +// request or response. +func validateProjectTemplateRules(pt *pb.ProjectTemplate) []*validation.FieldRules { + return []*validation.FieldRules{ + validation.Field(&pt.Id, validation.Length(0, PROJECT_TEMPLATE_ID_LENGTH)), + validation.Field(&pt.Name, validation.Length(0, PROJECT_TEMPLATE_NAME_LENGTH)), + + validationext.StructField(&pt.TerraformNocodeModule, func() []*validation.FieldRules { + return validateTerraformNocodeModule(pt.TerraformNocodeModule) + }), + + validation.Field(&pt.Summary, validation.Length(0, PROJECT_TEMPLATE_SUMMARY_LENGTH)), + validation.Field(&pt.ExpandedSummary, validation.Length(0, PROJECT_TEMPLATE_EXPANDED_SUMMARY_LENGTH)), + validation.Field(&pt.ReadmeMarkdownTemplate, validation.Length(0, PROJECT_TEMPLATE_README_LENGTH)), + + validation.Field(&pt.Tags, validation.Each( + validation.Length(1, PROJECT_TEMPLATE_TAG_LENGTH), + )), + + validationext.StructField(&pt.WaypointProject, func() []*validation.FieldRules { + return []*validation.FieldRules{ + validation.Field( + &pt.WaypointProject.WaypointHclTemplate, + validation.Length(1, PROJECT_TEMPLATE_WAYPOINT_HCL_LENGTH), + ), + } + }), + } +} + +func validateTerraformNocodeModule(t *pb.ProjectTemplate_TerraformNocodeModule) []*validation.FieldRules { + return []*validation.FieldRules{ + validation.Field(&t.Source, + validation.Required, + validation.Length(1, TERRAFORM_NOCODE_MODULE_SOURCE_LENGTH), + ), + validation.Field(&t.Version, + validation.Required, + validation.Length(1, TERRAFORM_NOCODE_MODULE_VERSION_LENGTH), + ), + } +} diff --git a/pkg/server/ptypes/project_template_test.go b/pkg/server/ptypes/project_template_test.go new file mode 100644 index 00000000000..294352d6bb5 --- /dev/null +++ b/pkg/server/ptypes/project_template_test.go @@ -0,0 +1,168 @@ +package ptypes + +import ( + validation "github.com/go-ozzo/ozzo-validation/v4" + "testing" + + pb "github.com/hashicorp/waypoint/pkg/server/gen" +) + +func TestValidateCreateProjectTemplateRequest(t *testing.T) { + tests := []struct { + name string + req *pb.CreateProjectTemplateRequest + wantErr bool + }{ + { + name: "minimum valid request", + req: &pb.CreateProjectTemplateRequest{ + ProjectTemplate: &pb.ProjectTemplate{ + Name: "test", + }, + }, + wantErr: false, + }, + { + name: "enforces name", + req: &pb.CreateProjectTemplateRequest{ + ProjectTemplate: &pb.ProjectTemplate{}, + }, + wantErr: true, + }, + { + name: "Inherits base validator rules (example: name length)", + req: &pb.CreateProjectTemplateRequest{ + ProjectTemplate: &pb.ProjectTemplate{ + Name: string(make([]byte, PROJECT_TEMPLATE_NAME_LENGTH+1)), + }, + }, + wantErr: true, + }, + { + name: "enforces name to not be empty", + req: &pb.CreateProjectTemplateRequest{ + ProjectTemplate: &pb.ProjectTemplate{ + Name: " "}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateCreateProjectTemplateRequest(tt.req) + if err == nil && tt.wantErr { + t.Errorf("expected error in ValidateCreateProjectTemplateRequest() but got none") + } + + if err != nil && !tt.wantErr { + t.Errorf("ValidateCreateProjectTemplateRequest() error = %v, wantErr %v", err, tt.wantErr) + } + + }) + } +} + +func TestValidateUpdateProjectTemplateRequest(t *testing.T) { + tests := []struct { + name string + req *pb.UpdateProjectTemplateRequest + wantErr bool + }{ + { + name: "Fails if no name or ID is given", + req: &pb.UpdateProjectTemplateRequest{ + ProjectTemplate: &pb.ProjectTemplate{}, + }, + wantErr: true, + }, + { + name: "OK with just name", + req: &pb.UpdateProjectTemplateRequest{ + ProjectTemplate: &pb.ProjectTemplate{ + Name: "test", + }, + }, + wantErr: false, + }, + { + name: "OK with just ID", + req: &pb.UpdateProjectTemplateRequest{ + ProjectTemplate: &pb.ProjectTemplate{ + Id: "test", + }, + }, + wantErr: false, + }, + { + name: "Also runs base project template validator", + req: &pb.UpdateProjectTemplateRequest{ + ProjectTemplate: &pb.ProjectTemplate{ + Id: string(make([]byte, PROJECT_TEMPLATE_ID_LENGTH+1)), + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateUpdateProjectTemplateRequest(tt.req) + + if err == nil && tt.wantErr { + t.Errorf("expected error in ValidateUpdateProjectTemplateRequest() but got none") + } + + if err != nil && !tt.wantErr { + t.Errorf("ValidateUpdateProjectTemplateRequest() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestValidateProjectTemplate(t *testing.T) { + tests := []struct { + name string + pt *pb.ProjectTemplate + wantErr bool + }{ + { + name: "Fine with empty values", + pt: &pb.ProjectTemplate{}, + wantErr: false, + }, + { + name: "If name is set, enforces length limits", + pt: &pb.ProjectTemplate{ + Name: string(make([]byte, PROJECT_TEMPLATE_NAME_LENGTH+1)), + }, + wantErr: true, + }, + { + name: "Validates nexted TFC-related lengths", + pt: &pb.ProjectTemplate{ + TerraformNocodeModule: &pb.ProjectTemplate_TerraformNocodeModule{ + Source: "", // Empty string shouldn't be allowed + Version: "0.0.1", + }, + }, + wantErr: true, + }, + { + name: "Tag lengths", + pt: &pb.ProjectTemplate{ + Tags: []string{string(make([]byte, PROJECT_TEMPLATE_TAG_LENGTH+1))}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validation.ValidateStruct(tt.pt, validateProjectTemplateRules(tt.pt)...) + if err == nil && tt.wantErr { + t.Errorf("expected error in validation but got none") + } + if err != nil && !tt.wantErr { + t.Errorf("validation error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}