diff --git a/Taskfile.yml b/Taskfile.yml index 7c1ac434..16116292 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -56,6 +56,12 @@ tasks: cmds: - go test -race -coverprofile=coverage.txt -covermode=atomic -v ./cmd/... {{ .CLI_ARGS }} + test-e2e: + desc: Run end to end Integration tests + dir: "{{.SRC_DIR}}" + cmds: + - go test -v ./e2e/... {{ .CLI_ARGS }} + update-opslevel-go: desc: Update opslevel-go version to latest release dir: "{{.SRC_DIR}}" diff --git a/src/cmd/action.go b/src/cmd/action.go index f26e48aa..a0f610e7 100644 --- a/src/cmd/action.go +++ b/src/cmd/action.go @@ -17,7 +17,16 @@ var exampleActionCmd = &cobra.Command{ Short: "Example action", Long: `Example action`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.CustomActionsWebhookActionCreateInput]()) + fmt.Println(getExample(opslevel.CustomActionsWebhookActionCreateInput{ + Name: "example_name", + Description: opslevel.RefOf("example_description"), + WebhookUrl: "example_webhook_url", + HttpMethod: opslevel.CustomActionsHttpMethodEnumPost, + Headers: &opslevel.JSON{ + "example_header": "example_value", + }, + LiquidTemplate: opslevel.RefOf("example_liquid_template"), + })) }, } diff --git a/src/cmd/alias.go b/src/cmd/alias.go index b26c1270..cd3c530f 100644 --- a/src/cmd/alias.go +++ b/src/cmd/alias.go @@ -16,7 +16,10 @@ var exampleAliasCmd = &cobra.Command{ Short: "Example alias", Long: `Example alias`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.AliasCreateInput]()) + fmt.Println(getExample(opslevel.AliasCreateInput{ + OwnerId: opslevel.ID("Z2lkOi8vc2VydmljZS8xMjM0NTY3ODk"), + Alias: "example_alias", + })) }, } diff --git a/src/cmd/dependency.go b/src/cmd/dependency.go index 3fe55c91..b7c6a715 100644 --- a/src/cmd/dependency.go +++ b/src/cmd/dependency.go @@ -20,7 +20,13 @@ var exampleServiceDependencyCmd = &cobra.Command{ Short: "Example service dependency", Long: `Example service dependency`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.ServiceDependencyCreateInput]()) + fmt.Println(getExample(opslevel.ServiceDependencyCreateInput{ + DependencyKey: opslevel.ServiceDependencyKey{ + SourceIdentifier: opslevel.NewIdentifier("example_source"), + DestinationIdentifier: opslevel.NewIdentifier("example_destination"), + }, + Notes: opslevel.RefOf("example_notes"), + })) }, } diff --git a/src/cmd/domain.go b/src/cmd/domain.go index 1b64e48e..277a75a2 100644 --- a/src/cmd/domain.go +++ b/src/cmd/domain.go @@ -17,7 +17,12 @@ var exampleDomainCmd = &cobra.Command{ Short: "Example Domain", Long: `Example Domain`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.DomainInput]()) + fmt.Println(getExample(opslevel.DomainInput{ + Name: opslevel.RefOf("example_name"), + Description: opslevel.RefOf("example_description"), + OwnerId: opslevel.RefOf(opslevel.ID("Z2lkOi8vc2VydmljZS8xMjM0NTY3ODk")), + Note: opslevel.RefOf("example_note"), + })) }, } diff --git a/src/cmd/example.go b/src/cmd/example.go index 92fd8d15..9effd75e 100644 --- a/src/cmd/example.go +++ b/src/cmd/example.go @@ -3,7 +3,6 @@ package cmd import ( "encoding/json" - "github.com/opslevel/opslevel-go/v2025" "github.com/spf13/cobra" "github.com/spf13/viper" "gopkg.in/yaml.v2" @@ -17,35 +16,16 @@ var exampleCmd = &cobra.Command{ Long: "Examples of OpsLevel resources in different formats", } -func getExample[T any]() string { +func getExample[T any](v T) string { + var out []byte + var err error if exampleIsJson { - return getJson[T]() + out, err = json.Marshal(v) + } else { + out, err = yaml.Marshal(v) } - return getYaml[T]() -} - -func getJson[T any]() string { - var ( - out []byte - err error - ) - t := opslevel.NewExampleOf[T]() - out, err = json.Marshal(t) - if err != nil { - panic("unexpected error getting example json") - } - return string(out) -} - -func getYaml[T any]() string { - var ( - out []byte - err error - ) - t := opslevel.NewExampleOf[T]() - out, err = yaml.Marshal(t) if err != nil { - panic("unexpected error getting example yaml") + panic("unexpected error getting example") } return string(out) } diff --git a/src/cmd/filter.go b/src/cmd/filter.go index 840e3bda..2c3c91dd 100644 --- a/src/cmd/filter.go +++ b/src/cmd/filter.go @@ -15,7 +15,17 @@ var exampleFilterCmd = &cobra.Command{ Short: "Example filter", Long: `Example filter`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.FilterCreateInput]()) + fmt.Println(getExample(opslevel.FilterCreateInput{ + Name: "example_name", + Predicates: &[]opslevel.FilterPredicateInput{ + { + Key: opslevel.PredicateKeyEnumAliases, + Type: opslevel.PredicateTypeEnumEquals, + Value: opslevel.RefOf("example_value"), + CaseSensitive: opslevel.RefOf(false), + }, + }, + })) }, } diff --git a/src/cmd/infra.go b/src/cmd/infra.go index 588e0d96..21547737 100644 --- a/src/cmd/infra.go +++ b/src/cmd/infra.go @@ -20,7 +20,22 @@ var exampleInfraCmd = &cobra.Command{ Short: "Example infrastructure resource", Long: `Example infrastructure resource`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.InfrastructureResourceInput]()) + fmt.Println(getExample(opslevel.InfraInput{ + Schema: "example_schema", + Owner: opslevel.NewID("Z2lkOi8vc2VydmljZS8xMjM0NTY3ODk"), + Provider: &opslevel.InfraProviderInput{ + Account: "example_account", + Name: "example_provider_name", + Type: "example_provider_type", + URL: "example_external_url", + }, + Data: &opslevel.JSON{ + "name": "my-big-query", + "endpoint": "https://google.com", + "engine": "BigQuery", + "replica": false, + }, + })) }, } diff --git a/src/cmd/integration.go b/src/cmd/integration.go index 84e89c52..e36d243b 100644 --- a/src/cmd/integration.go +++ b/src/cmd/integration.go @@ -39,9 +39,9 @@ type IntegrationInputType struct { type IntegrationInput interface { opslevel.AWSIntegrationInput | - opslevel.AzureResourcesIntegrationInput | - EventIntegrationInputDTO | - opslevel.GoogleCloudIntegrationInput + opslevel.AzureResourcesIntegrationInput | + EventIntegrationInputDTO | + opslevel.GoogleCloudIntegrationInput } func validateIntegrationInput() (*IntegrationInputType, error) { diff --git a/src/cmd/property.go b/src/cmd/property.go index 9735e9f0..f37f72fc 100644 --- a/src/cmd/property.go +++ b/src/cmd/property.go @@ -14,13 +14,31 @@ import ( "github.com/spf13/cobra" ) +func buildExamplePropertyInput() string { + return getExample(opslevel.PropertyInput{ + Definition: *opslevel.NewIdentifier("example_definition"), + Owner: *opslevel.NewIdentifier("example_owner"), + Value: opslevel.JsonString("example_value"), + }) +} + +func buildExamplePropertyDefinitionInput() string { + return getExample(opslevel.PropertyDefinitionInput{ + Name: opslevel.RefOf("example_name"), + Description: opslevel.RefOf("example_description"), + Schema: &opslevel.JSONSchema{ + "type": "string", + }, + }) +} + var examplePropertyCmd = &cobra.Command{ Use: "property", Aliases: []string{"prop"}, Short: "Example Property", Long: `Example Property`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.PropertyInput]()) + fmt.Println(buildExamplePropertyInput()) }, } @@ -104,7 +122,7 @@ EOF cat << EOF | opslevel assign property -f - %s -EOF`, getYaml[opslevel.PropertyInput]()), +EOF`, buildExamplePropertyInput()), Run: func(cmd *cobra.Command, args []string) { input, err := readResourceInput[opslevel.PropertyInput]() cobra.CheckErr(err) @@ -149,7 +167,7 @@ var examplePropertyDefinitionCmd = &cobra.Command{ Short: "Example Property Definition", Long: `Example Property Definition`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.PropertyDefinitionInput]()) + fmt.Println(buildExamplePropertyDefinitionInput()) }, } @@ -161,7 +179,7 @@ var createPropertyDefinitionCmd = &cobra.Command{ Example: fmt.Sprintf(` cat << EOF | opslevel create property-definition -f - %s -EOF`, getYaml[opslevel.PropertyDefinitionInput]()), +EOF`, buildExamplePropertyDefinitionInput()), Run: func(cmd *cobra.Command, args []string) { input, err := readPropertyDefinitionInput() cobra.CheckErr(err) @@ -180,7 +198,7 @@ var updatePropertyDefinitionCmd = &cobra.Command{ Example: fmt.Sprintf(` cat << EOF | opslevel update property-definition propdef3 -f - %s -EOF`, getYaml[opslevel.PropertyDefinitionInput]()), +EOF`, buildExamplePropertyDefinitionInput()), Args: cobra.ExactArgs(1), ArgAliases: []string{"ID", "ALIAS"}, Run: func(cmd *cobra.Command, args []string) { diff --git a/src/cmd/rubric.go b/src/cmd/rubric.go index 4db95af7..7cf16763 100644 --- a/src/cmd/rubric.go +++ b/src/cmd/rubric.go @@ -16,7 +16,9 @@ var exampleCategoryCmd = &cobra.Command{ Short: "Example rubric category", Long: `Example rubric category`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.CategoryCreateInput]()) + fmt.Println(getExample(opslevel.CategoryCreateInput{ + Name: "example_name", + })) }, } @@ -92,7 +94,9 @@ var exampleLevelCmd = &cobra.Command{ Short: "Example rubric level", Long: `Example rubric level`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.LevelCreateInput]()) + fmt.Println(getExample(opslevel.LevelCreateInput{ + Name: "example_name", + })) }, } diff --git a/src/cmd/scorecard.go b/src/cmd/scorecard.go index ec62b515..0742cb5d 100644 --- a/src/cmd/scorecard.go +++ b/src/cmd/scorecard.go @@ -16,7 +16,12 @@ var exampleScorecardCmd = &cobra.Command{ Short: "Example Scorecard", Long: `Example Scorecard`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.ScorecardInput]()) + fmt.Println(getExample(opslevel.ScorecardInput{ + Name: "example_name", + Description: opslevel.RefOf("example_description"), + OwnerId: opslevel.ID("Z2lkOi8vc2VydmljZS8xMjM0NTY3ODk"), + AffectsOverallServiceLevels: opslevel.RefOf(false), + })) }, } diff --git a/src/cmd/secret.go b/src/cmd/secret.go index 231a6eb9..98d15e0d 100644 --- a/src/cmd/secret.go +++ b/src/cmd/secret.go @@ -18,7 +18,10 @@ var exampleSecretCmd = &cobra.Command{ Short: "Example Secret", Long: `Example Secret`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.SecretInput]()) + fmt.Println(getExample(opslevel.SecretInput{ + Owner: opslevel.NewIdentifier("example_owner"), + Value: opslevel.RefOf("example_value"), + })) }, } diff --git a/src/cmd/service.go b/src/cmd/service.go index d9e9f61b..bbbf8c42 100644 --- a/src/cmd/service.go +++ b/src/cmd/service.go @@ -21,7 +21,18 @@ var exampleServiceCmd = &cobra.Command{ Short: "Example service", Long: `Example service`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.ServiceCreateInput]()) + fmt.Println(getExample(opslevel.ServiceCreateInput{ + Name: "example_name", + Description: opslevel.RefOf("example_description"), + Framework: opslevel.RefOf("example_framework"), + Language: opslevel.RefOf("example_language"), + LifecycleAlias: opslevel.RefOf("example_lifecycle"), + OwnerAlias: opslevel.RefOf("example_owner"), + Parent: opslevel.NewIdentifier("example_parent"), + Product: opslevel.RefOf("example_product"), + TierAlias: opslevel.RefOf("example_tier"), + Type: opslevel.NewIdentifier("example_type"), + })) }, } @@ -158,6 +169,14 @@ type: EOF`, Run: func(cmd *cobra.Command, args []string) { input, err := readResourceInput[opslevel.ServiceUpdateInput]() + if len(args) == 1 { + key := args[0] + if opslevel.IsID(key) { + input.Id = opslevel.RefOf(opslevel.ID(key)) + } else { + input.Alias = opslevel.RefOf(key) + } + } cobra.CheckErr(err) service, err := getClientGQL().UpdateService(*input) cobra.CheckErr(err) diff --git a/src/cmd/system.go b/src/cmd/system.go index fd06fcb2..88bd581e 100644 --- a/src/cmd/system.go +++ b/src/cmd/system.go @@ -18,7 +18,13 @@ var exampleSystemCmd = &cobra.Command{ Short: "Example system", Long: `Example system`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.SystemInput]()) + fmt.Println(getExample(opslevel.SystemInput{ + Name: opslevel.RefOf("example_name"), + Description: opslevel.RefOf("example_description"), + OwnerId: opslevel.RefOf(opslevel.ID("Z2lkOi8vc2VydmljZS8xMjM0NTY3ODk")), + Parent: opslevel.NewIdentifier("domain-alias"), + Note: opslevel.RefOf("example_note"), + })) }, } diff --git a/src/cmd/tag.go b/src/cmd/tag.go index 347cd3b9..dd7eb300 100644 --- a/src/cmd/tag.go +++ b/src/cmd/tag.go @@ -19,7 +19,10 @@ var exampleTagCmd = &cobra.Command{ Short: "Example tag to assign to a resource", Long: `Example tag to assign to a resource`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.TagInput]()) + fmt.Println(getExample(opslevel.TagInput{ + Key: "example_key", + Value: "example_value", + })) }, } diff --git a/src/cmd/team.go b/src/cmd/team.go index f702e92a..1ca6faff 100644 --- a/src/cmd/team.go +++ b/src/cmd/team.go @@ -16,7 +16,14 @@ var exampleTeamCmd = &cobra.Command{ Short: "Example team", Long: `Example team`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.TeamCreateInput]()) + fmt.Println(getExample(opslevel.TeamCreateInput{ + Contacts: &[]opslevel.ContactInput{}, + ManagerEmail: opslevel.RefOf("example_manager_email"), + Members: &[]opslevel.TeamMembershipUserInput{}, + Name: "example_name", + ParentTeam: opslevel.NewIdentifier("example_parent_team"), + Responsibilities: opslevel.RefOf("example_responsibilities"), + })) }, } @@ -31,12 +38,12 @@ parentTeam: alias: "parent-team" responsibilities: "all the things" EOF`, - Args: cobra.ExactArgs(1), ArgAliases: []string{"NAME"}, Run: func(cmd *cobra.Command, args []string) { - key := args[0] input, err := readResourceInput[opslevel.TeamCreateInput]() - input.Name = key + if len(args) == 1 { + input.Name = args[0] + } cobra.CheckErr(err) team, err := getClientGQL().CreateTeam(*input) cobra.CheckErr(err) @@ -49,7 +56,11 @@ var exampleMemberCmd = &cobra.Command{ Short: "Example member", Long: `Example member`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.TeamMembershipUserInput]()) + fmt.Println(getExample(opslevel.TeamMembershipUserInput{ + Email: opslevel.RefOf("example_email"), + Role: opslevel.RefOf("example_role"), + User: opslevel.NewUserIdentifier("example_user"), + })) }, } @@ -92,7 +103,10 @@ var exampleContactCmd = &cobra.Command{ Short: "Example contact to a team", Long: `Example contact to a team`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.ContactInput]()) + fmt.Println(getExample(opslevel.ContactInput{ + DisplayName: opslevel.RefOf("example_display_name"), + Address: "example_address", + })) }, } diff --git a/src/cmd/trigger_definition.go b/src/cmd/trigger_definition.go index 8ba3ba60..d346cd3f 100644 --- a/src/cmd/trigger_definition.go +++ b/src/cmd/trigger_definition.go @@ -16,7 +16,10 @@ var exampleTriggerDefinitionCmd = &cobra.Command{ Short: "Example Scorecard", Long: `Example Scorecard`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.CustomActionsTriggerDefinitionCreateInput]()) + fmt.Println(getExample(opslevel.CustomActionsTriggerDefinitionCreateInput{ + Name: "example_name", + OwnerId: opslevel.ID("Z2lkOi8vc2VydmljZS8xMjM0NTY3ODk"), + })) }, } diff --git a/src/cmd/user.go b/src/cmd/user.go index 4bb8ad8c..df0c4c48 100644 --- a/src/cmd/user.go +++ b/src/cmd/user.go @@ -20,7 +20,9 @@ var exampleUserCmd = &cobra.Command{ Short: "Example User", Long: `Example User`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getExample[opslevel.UserInput]()) + fmt.Println(getExample(opslevel.UserInput{ + Name: opslevel.RefOf("example_name"), + })) }, } diff --git a/src/e2e/domain_test.go b/src/e2e/domain_test.go new file mode 100644 index 00000000..05b98455 --- /dev/null +++ b/src/e2e/domain_test.go @@ -0,0 +1,83 @@ +package e2e + +import ( + "strings" + "testing" +) + +func TestDomainHappyPath(t *testing.T) { + tc := CLITest{ + Steps: []Step{ + Example{ + Cmd: "example domain", + Yaml: ` +description: example_description +name: example_name +note: example_note +ownerId: Z2lkOi8vc2VydmljZS8xMjM0NTY3ODk +`, + }, + Create{ + Cmd: "create domain", + Input: ` +name: "Integration Test Domain" +description: "Created by integration test" +`, + }, + Get{ + Cmd: "get domain", + Validate: func(u *Utility, out string) { + if !strings.Contains(out, "Integration Test Domain") { + u.Fatalf("get after create failed: %s", out) + } + }, + }, + List{ + Cmd: "list domain", + Validate: func(u *Utility, out string) { + if !strings.Contains(out, "Integration Test Domain") { + u.Fatalf("list missing domain: %s", out) + } + }, + }, + Update{ + Cmd: "update domain", + Input: ` +name: "Integration Test Domain Updated" +description: "Updated by integration test" +`, + }, + Get{ + Cmd: "get domain", + Validate: func(u *Utility, out string) { + if !strings.Contains(out, "Integration Test Domain Updated") || !strings.Contains(out, "Updated by integration test") { + u.Fatalf("update1 failed\nout: %s", out) + } + }, + }, + // TODO: description cannot be unset yest + // Update{ + // Cmd: "update domain", + // Input: ` + //name: "Integration Test Domain Updated Again" + //description: null + //`, + // }, + // Get{ + // Cmd: "get domain", + // Validate: func(u *Utility, out string) { + // if !strings.Contains(out, "Integration Test Domain Updated Again") || strings.Contains(out, "Updated by integration test") { + // u.Fatalf("update2 failed (description should be unset)\nout: %s", out) + // } + // }, + // }, + Delete{ + Cmd: "delete domain", + }, + Missing{ + Cmd: "get domain", + }, + }, + } + tc.Run(t) +} diff --git a/src/e2e/helpers.go b/src/e2e/helpers.go new file mode 100644 index 00000000..fa1e54e5 --- /dev/null +++ b/src/e2e/helpers.go @@ -0,0 +1,194 @@ +package e2e + +import ( + "bytes" + "os" + "os/exec" + "strconv" + "strings" + "testing" +) + +type Utility struct { + *testing.T + ID string // The resource ID created by a step, if needed +} + +type Step interface { + Run(u *Utility) + Name() string + Deferred() bool +} + +// CLITest uses []Step interface now +type CLITest struct { + Steps []Step +} + +func (tc *CLITest) Run(t *testing.T) { + util := &Utility{T: t} + // Run non-deferred steps + for i, step := range tc.Steps { + if step.Deferred() { + continue + } + t.Run(step.Name()+"_"+strconv.Itoa(i), func(t *testing.T) { + util.T = t + step.Run(util) + }) + } + // Run deferred steps + for i, step := range tc.Steps { + if !step.Deferred() { + continue + } + t.Run(step.Name()+"_"+strconv.Itoa(i), func(t *testing.T) { + util.T = t + step.Run(util) + }) + } +} + +// Run executes the CLI using 'go run main.go' from the ./src directory with the given arguments and optional stdin, returning combined output and error. +func (u *Utility) Run(args string, stdin ...string) (string, error) { + // TODO: need to allow using the pre-built binary + // cmd := exec.Command("opslevel", strings.Split(args, " ")...) + + cmd := exec.Command("go", append([]string{"run", "main.go"}, strings.Split(args, " ")...)...) + cmd.Dir = ".." + cmd.Env = os.Environ() + var out bytes.Buffer + var errBuf bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &errBuf + if len(stdin) > 0 { + cmd.Stdin = strings.NewReader(stdin[0]) + } + err := cmd.Run() + return out.String() + errBuf.String(), err +} + +// Create step +type Create struct { + Cmd string + Input string +} + +func (s Create) Run(u *Utility) { + out, err := u.Run(s.Cmd+" -f -", s.Input) + if err != nil { + panic("create failed: " + err.Error() + "\nout: " + out) + } + u.ID = strings.TrimRight(strings.TrimLeft(strings.TrimSpace(out), "\""), "\"") + if u.ID == "" { + panic("expected ID, got: " + out) + } +} + +func (s Create) Name() string { return "Create" } +func (s Create) Deferred() bool { return false } + +// Get step +type Get struct { + Cmd string + Validate func(u *Utility, out string) +} + +func (s Get) Run(u *Utility) { + out, err := u.Run(s.Cmd + " " + u.ID) + if err != nil { + u.Fatalf("get failed: %v\nout: %s", err, out) + } + s.Validate(u, out) +} + +func (s Get) Name() string { return "Get" } +func (s Get) Deferred() bool { return false } + +// List step +type List struct { + Cmd string + Validate func(u *Utility, out string) +} + +func (s List) Run(u *Utility) { + out, err := u.Run(s.Cmd) + if err != nil { + u.Fatalf("list failed: %v\nout: %s", err, out) + } + if s.Validate != nil { + s.Validate(u, out) + } +} + +func (s List) Name() string { return "List" } +func (s List) Deferred() bool { return false } + +// Update step +type Update struct { + Cmd string + Input string + Validate func(u *Utility, out string) +} + +func (s Update) Run(u *Utility) { + out, err := u.Run(s.Cmd+" -f - "+u.ID, s.Input) + if err != nil { + u.Fatalf("update failed: %v\nout: %s", err, out) + } + if s.Validate != nil { + s.Validate(u, out) + } +} + +func (s Update) Name() string { return "Update" } +func (s Update) Deferred() bool { return false } + +type Delete struct { + Cmd string +} + +func (s Delete) Run(u *Utility) { + out, err := u.Run(s.Cmd + " " + u.ID) + if err != nil { + u.Fatalf("delete failed: %v\nout: %s", err, out) + } +} + +func (s Delete) Name() string { return "Delete" } +func (s Delete) Deferred() bool { return true } + +type Missing struct { + Cmd string +} + +func (s Missing) Run(u *Utility) { + out, err := u.Run(s.Cmd + " " + u.ID) + lower := strings.ToLower(out) + if err == nil || !(strings.Contains(lower, "not found") || strings.Contains(lower, "missing") || strings.Contains(lower, "does not exist on this account")) { + u.Fatalf("expected get after delete to fail with not found, got: %v\nout: %s", err, out) + } +} + +func (s Missing) Name() string { return "Missing" } +func (s Missing) Deferred() bool { return true } + +type Example struct { + Cmd string + Yaml string +} + +func (s Example) Run(u *Utility) { + out, err := u.Run(s.Cmd + " --yaml") + if err != nil { + panic("example failed: " + err.Error() + "\nout: " + out) + } + wip := strings.TrimSpace(out) + expected := strings.TrimSpace(s.Yaml) + if wip != expected { + u.Fatalf("example mismatch for '%s'\nExpected:\n%s\nGot:\n%s", s.Cmd, expected, wip) + } +} + +func (s Example) Name() string { return "Example" } +func (s Example) Deferred() bool { return false } diff --git a/src/e2e/infrastructure_test.go b/src/e2e/infrastructure_test.go new file mode 100644 index 00000000..ee935844 --- /dev/null +++ b/src/e2e/infrastructure_test.go @@ -0,0 +1,85 @@ +package e2e + +import ( + "strings" + "testing" +) + +func TestInfrastructureHappyPath(t *testing.T) { + tc := CLITest{ + Steps: []Step{ + Example{ + Cmd: "example infra", + Yaml: ` +schema: example_schema +owner: Z2lkOi8vc2VydmljZS8xMjM0NTY3ODk +provider: + account: example_account + name: example_provider_name + type: example_provider_type + url: example_external_url +data: + endpoint: https://google.com + engine: BigQuery + name: my-big-query + replica: false +`, + }, + Create{ + Cmd: "create infra", + Input: ` +schema: "Database" +provider: + account: "Dev - 123456789" + name: "GCP" + type: "BigQuery" + url: "https://google.com" +data: + name: "my-big-query" + endpoint: "https://google.com" + engine: "BigQuery" + replica: false +`, + }, + Get{ + Cmd: "get infra", + Validate: func(u *Utility, out string) { + if !strings.Contains(out, "my-big-query") { + u.Fatalf("get after create failed: %s", out) + } + }, + }, + List{ + Cmd: "list infra", + Validate: func(u *Utility, out string) { + if !strings.Contains(out, "my-big-query") { + u.Fatalf("list missing infra: %s", out) + } + }, + }, + Update{ + Cmd: "update infra", + Input: ` +schema: "Database" +data: + name: "my-big-query-updated" +`, + }, + Get{ + Cmd: "get infra", + Validate: func(u *Utility, out string) { + if !strings.Contains(out, "my-big-query-updated") { + u.Fatalf("update1 failed\nout: %s", out) + } + }, + }, + Delete{ + Cmd: "delete infra", + }, + Missing{ + Cmd: "get infra", + }, + }, + } + tc.Run(t) +} diff --git a/src/e2e/service_test.go b/src/e2e/service_test.go new file mode 100644 index 00000000..6e73789f --- /dev/null +++ b/src/e2e/service_test.go @@ -0,0 +1,91 @@ +package e2e + +import ( + "strings" + "testing" +) + +func TestServiceHappyPath(t *testing.T) { + tc := CLITest{ + Steps: []Step{ + Example{ + Cmd: "example service", + Yaml: ` +description: example_description +framework: example_framework +language: example_language +lifecycleAlias: example_lifecycle +name: example_name +ownerAlias: example_owner +parent: + alias: example_parent +product: example_product +tierAlias: example_tier +type: + alias: example_type +`, + }, + Create{ + Cmd: "create service", + Input: ` +name: "TestService" +description: "Created by integration test" +`, + }, + Get{ + Cmd: "get service", + Validate: func(u *Utility, out string) { + if !strings.Contains(out, "TestService") { + u.Fatalf("get after create failed: %s", out) + } + }, + }, + List{ + Cmd: "list service", + Validate: func(u *Utility, out string) { + if !strings.Contains(out, "TestService") { + u.Fatalf("list missing service: %s", out) + } + }, + }, + Update{ + Cmd: "update service", + Input: ` +name: "TestServiceUpdated" +description: "Updated by integration test" +`, + }, + Get{ + Cmd: "get service", + Validate: func(u *Utility, out string) { + if !strings.Contains(out, "TestServiceUpdated") || !strings.Contains(out, "Updated by integration test") { + u.Fatalf("update1 failed\nout: %s", out) + } + }, + }, + // TODO: description cannot be unset yet + // Update{ + // Cmd: "update service", + // Input: ` + //name: "TestServiceUpdatedAgain" + //description: null + //`, + // }, + // Get{ + // Cmd: "get service", + // Validate: func(u *Utility, out string) { + // if !strings.Contains(out, "TestServiceUpdatedAgain") || strings.Contains(out, "Updated by integration test") { + // u.Fatalf("update2 failed (description should be unset)\nout: %s", out) + // } + // }, + // }, + Delete{ + Cmd: "delete service", + }, + Missing{ + Cmd: "get service", + }, + }, + } + tc.Run(t) +} diff --git a/src/e2e/system_test.go b/src/e2e/system_test.go new file mode 100644 index 00000000..0b4b02be --- /dev/null +++ b/src/e2e/system_test.go @@ -0,0 +1,85 @@ +package e2e + +import ( + "strings" + "testing" +) + +func TestSystemHappyPath(t *testing.T) { + tc := CLITest{ + Steps: []Step{ + Example{ + Cmd: "example system", + Yaml: ` +description: example_description +name: example_name +note: example_note +ownerId: Z2lkOi8vc2VydmljZS8xMjM0NTY3ODk +parent: + alias: domain-alias +`, + }, + Create{ + Cmd: "create system", + Input: ` +name: "Integration Test System" +description: "Created by integration test" +`, + }, + Get{ + Cmd: "get system", + Validate: func(u *Utility, out string) { + if !strings.Contains(out, "Integration Test System") { + u.Fatalf("get after create failed: %s", out) + } + }, + }, + List{ + Cmd: "list system", + Validate: func(u *Utility, out string) { + if !strings.Contains(out, "Integration Test System") { + u.Fatalf("list missing system: %s", out) + } + }, + }, + Update{ + Cmd: "update system", + Input: ` +name: "Integration Test System Updated" +description: "Updated by integration test" +`, + }, + Get{ + Cmd: "get system", + Validate: func(u *Utility, out string) { + if !strings.Contains(out, "Integration Test System Updated") || !strings.Contains(out, "Updated by integration test") { + u.Fatalf("update1 failed\nout: %s", out) + } + }, + }, + // TODO: description cannot be unset yet + // Update{ + // Cmd: "update system", + // Input: ` + //name: "Integration Test System Updated Again" + //description: null + //`, + // }, + // Get{ + // Cmd: "get system", + // Validate: func(u *Utility, out string) { + // if !strings.Contains(out, "Integration Test System Updated Again") || strings.Contains(out, "Updated by integration test") { + // u.Fatalf("update2 failed (description should be unset)\nout: %s", out) + // } + // }, + // }, + Delete{ + Cmd: "delete system", + }, + Missing{ + Cmd: "get system", + }, + }, + } + tc.Run(t) +} diff --git a/src/e2e/team_test.go b/src/e2e/team_test.go new file mode 100644 index 00000000..b5910189 --- /dev/null +++ b/src/e2e/team_test.go @@ -0,0 +1,86 @@ +package e2e + +import ( + "strings" + "testing" +) + +func TestTeamHappyPath(t *testing.T) { + tc := CLITest{ + Steps: []Step{ + Example{ + Cmd: "example team", + Yaml: ` +contacts: [] +managerEmail: example_manager_email +members: [] +name: example_name +parentTeam: + alias: example_parent_team +responsibilities: example_responsibilities +`, + }, + Create{ + Cmd: "create team", + Input: ` +name: "TestTeam" +responsibilities: "Created by integration test" +`, + }, + Get{ + Cmd: "get team", + Validate: func(u *Utility, out string) { + if !strings.Contains(out, "TestTeam") { + u.Fatalf("get after create failed: %s", out) + } + }, + }, + List{ + Cmd: "list team", + Validate: func(u *Utility, out string) { + if !strings.Contains(out, "TestTeam") { + u.Fatalf("list missing team: %s", out) + } + }, + }, + Update{ + Cmd: "update team", + Input: ` +name: "TestTeam Updated" +responsibilities: "Updated by integration test" +`, + }, + Get{ + Cmd: "get team", + Validate: func(u *Utility, out string) { + if !strings.Contains(out, "TestTeam Updated") || !strings.Contains(out, "Updated by integration test") { + u.Fatalf("update1 failed\nout: %s", out) + } + }, + }, + // TODO: responsibilities cannot be unset yet + // Update{ + // Cmd: "update team", + // Input: ` + //name: "Integration Test Team Updated Again" + //responsibilities: null + //`, + // }, + // Get{ + // Cmd: "get team", + // Validate: func(u *Utility, out string) { + // if !strings.Contains(out, "Integration Test Team Updated Again") || strings.Contains(out, "Updated by integration test") { + // u.Fatalf("update2 failed (responsibilities should be unset)\nout: %s", out) + // } + // }, + // }, + Delete{ + Cmd: "delete team", + }, + Missing{ + Cmd: "get team", + }, + }, + } + tc.Run(t) +} diff --git a/src/go.mod b/src/go.mod index 7c73e9a4..c55ce7d1 100644 --- a/src/go.mod +++ b/src/go.mod @@ -55,8 +55,8 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/gosimple/unidecode v1.0.1 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-retryablehttp v0.7.7 // indirect - github.com/hasura/go-graphql-client v0.14.3 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect + github.com/hasura/go-graphql-client v0.14.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/itchyny/timefmt-go v0.1.6 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect @@ -92,10 +92,10 @@ require ( go.opentelemetry.io/otel/sdk v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.38.0 // indirect - golang.org/x/net v0.40.0 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.0 // indirect + golang.org/x/text v0.26.0 // indirect google.golang.org/protobuf v1.36.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/src/go.sum b/src/go.sum index 27b79c9a..09abc552 100644 --- a/src/go.sum +++ b/src/go.sum @@ -141,10 +141,10 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= -github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= -github.com/hasura/go-graphql-client v0.14.3 h1:7La92TuA/FRkVmFd1IN8E+WGW8Lxyn6NKOXAgWcoBDA= -github.com/hasura/go-graphql-client v0.14.3/go.mod h1:jfSZtBER3or+88Q9vFhWHiFMPppfYILRyl+0zsgPIIw= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hasura/go-graphql-client v0.14.4 h1:bYU7/+V50T2YBGdNQXt6l4f2cMZPECPUd8cyCR+ixtw= +github.com/hasura/go-graphql-client v0.14.4/go.mod h1:jfSZtBER3or+88Q9vFhWHiFMPppfYILRyl+0zsgPIIw= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -285,17 +285,17 @@ go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= -golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -313,13 +313,13 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= -golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= diff --git a/src/submodules/opslevel-go b/src/submodules/opslevel-go index 095e4b04..fc57f4af 160000 --- a/src/submodules/opslevel-go +++ b/src/submodules/opslevel-go @@ -1 +1 @@ -Subproject commit 095e4b041e53c38dff17aa0a23501e0a52c79d07 +Subproject commit fc57f4af07fd26bff92998437fff6ecfc2bb8e23