diff --git a/.github/workflows/tests-integration.yml b/.github/workflows/tests-integration.yml new file mode 100644 index 00000000..38787c50 --- /dev/null +++ b/.github/workflows/tests-integration.yml @@ -0,0 +1,24 @@ +name: Integration Tests + +on: + workflow_dispatch: {} + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Install Task + uses: arduino/setup-task@v2 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Run Tests + run: task test-integration + env: + OPSLEVEL_API_TOKEN: ${{ secrets.OPSLEVEL_PAT_API_TOKEN }} diff --git a/Taskfile.yml b/Taskfile.yml index dde21c9f..0754339d 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -50,7 +50,13 @@ tasks: desc: Run tests dir: "{{.SRC_DIR}}" cmds: - - go test -race -coverprofile=coverage.txt -covermode=atomic -v ./... {{ .CLI_ARGS }} + - go test -race -coverprofile=coverage.txt -covermode=atomic -v ./common/... {{ .CLI_ARGS }} + + test-integration: + desc: Run Integration tests + dir: "{{.SRC_DIR}}" + cmds: + - go test -race -coverprofile=coverage.txt -covermode=atomic -v ./cmd/... {{ .CLI_ARGS }} update-opslevel-go: desc: Update opslevel-go version to latest release diff --git a/src/cmd/team_test.go b/src/cmd/team_test.go new file mode 100644 index 00000000..1a0fe2b3 --- /dev/null +++ b/src/cmd/team_test.go @@ -0,0 +1,103 @@ +package cmd_test + +import ( + "fmt" + "testing" + + "github.com/opslevel/cli/cmd" + "github.com/opslevel/opslevel-go/v2024" +) + +const ( + teamFileName = "test_team.yaml" + teamName = "CLI Test Team" +) + +func Test_TeamCRUD(t *testing.T) { + // Create Team + teamToCreate := opslevel.TeamCreateInput{ + Name: teamName, + Responsibilities: opslevel.RefOf("all the things"), + } + teamId, err := createTeam(teamToCreate) + if err != nil { + t.Fatal(err) + } + + // Get Team + createdTeam, err := getTeam(teamId) + if err != nil { + t.Fatal(err) + } + if createdTeam.Name != teamToCreate.Name || + createdTeam.Responsibilities != *teamToCreate.Responsibilities { + t.Errorf("Create 'team' failed, expected team '%+v' but got '%+v'", teamToCreate, createdTeam) + } + + // Update Team + teamToUpdate := opslevel.TeamUpdateInput{ + Name: opslevel.RefOf(createdTeam.Name), + Responsibilities: opslevel.RefOf("new things"), + } + updatedTeamId, err := updateTeam(teamId, teamToUpdate) + if err != nil { + _ = deleteTeam(string(createdTeam.Id)) + t.Fatal(err) + } + if string(createdTeam.Id) != updatedTeamId { + t.Errorf("Update 'team' failed, expected returned ID '%s' but got '%s'", string(createdTeam.Id), updatedTeamId) + } + + // Delete Team + if err = deleteTeam(string(createdTeam.Id)); err != nil { + t.Errorf("Delete 'team' failed, got error '%s'", err) + } +} + +func createTeam(teamToCreate opslevel.TeamCreateInput) (string, error) { + if err := writeToYaml(teamFileName, teamToCreate); err != nil { + return "", fmt.Errorf("Error while writing '%v' to file '%s': %v", teamToCreate, teamFileName, err) + } + + cliArgs := []string{teamToCreate.Name, "-f", teamFileName} + cmd.RootCmd.SetArgs(cliArgs) + + // Create Team + createOutput, err := execCmd(Create, "team", cliArgs...) + if err != nil { + return "", fmt.Errorf("Create 'team' failed, got error: %v", err) + } + return asString(createOutput), nil +} + +func getTeam(teamId string) (*opslevel.Team, error) { + getOutput, err := execCmd(Get, "team", teamId) + if err != nil { + return nil, fmt.Errorf("Get 'team' failed, got error: %v", err) + } + + createdTeam, err := jsonToResource[opslevel.Team](getOutput) + if err != nil { + return nil, fmt.Errorf("Failed to convert JSON from API to 'opslevel.Team' struct") + } + return createdTeam, err +} + +func updateTeam(teamId string, teamToUpdate opslevel.TeamUpdateInput) (string, error) { + if err := writeToYaml(teamFileName, teamToUpdate); err != nil { + return "", fmt.Errorf("Error while writing '%v' to file '%s': %v", teamToUpdate, teamFileName, err) + } + + // Store Update Team stuff to "file" + cliArgs := []string{teamId, "-f", teamFileName} + updateOutput, err := execCmd(Update, "team", cliArgs...) + if err != nil { + return "", fmt.Errorf("Update 'team' failed, got error: %v", err) + } + return asString(updateOutput), nil +} + +func deleteTeam(teamId string) error { + _, err := execCmd(Delete, "team", teamId) + return err +} diff --git a/src/cmd/user.go b/src/cmd/user.go index 0035270a..3dc44f13 100644 --- a/src/cmd/user.go +++ b/src/cmd/user.go @@ -81,7 +81,7 @@ EOF var getUserCmd = &cobra.Command{ Use: "user {ID|EMAIL}", - Short: "Get details about a filter", + Short: "Get details about a user", Example: `opslevel get user john@example.com`, Args: cobra.ExactArgs(1), ArgAliases: []string{"ID"}, diff --git a/src/cmd/user_test.go b/src/cmd/user_test.go new file mode 100644 index 00000000..189e716c --- /dev/null +++ b/src/cmd/user_test.go @@ -0,0 +1,103 @@ +package cmd_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/opslevel/cli/cmd" + "github.com/opslevel/opslevel-go/v2024" +) + +const ( + defaultUserRole = opslevel.UserRoleUser + userFileName = "test_user.yaml" + userName = "CLI Test User" +) + +func Test_UserCRUD(t *testing.T) { + expectedUser := opslevel.User{ + UserId: opslevel.UserId{Email: "testcli+pat@opslevel.com"}, + Name: userName, + } + // Create User + userId, err := createUser(expectedUser) + if err != nil { + t.Fatal(err) + } + + // Get User + createdUser, err := getUser(userId) + if err != nil { + t.Fatal(err) + } + if createdUser.Name != expectedUser.Name || + createdUser.Email != expectedUser.Email || + string(createdUser.Role) != string(defaultUserRole) || + !strings.HasPrefix(createdUser.HTMLUrl, "https://app.opslevel.com/users/") { + t.Errorf("Create 'user' failed, expected user '%+v' but got '%+v'", expectedUser, createdUser) + } + + // Update User + expectedUpdatedUser := opslevel.User{ + UserId: createdUser.UserId, + Name: createdUser.Name, + Role: opslevel.UserRoleTeamMember, + } + updatedUserId, err := updateUser(string(createdUser.Id), expectedUpdatedUser) + if err != nil { + t.Fatal(err) + } + if string(createdUser.Id) != updatedUserId { + t.Errorf("Update 'user' failed, expected returned ID '%s' but got '%s'", string(createdUser.Id), updatedUserId) + } + + // Delete User + if err = deleteUser(string(createdUser.Id)); err != nil { + t.Error(err) + } +} + +func createUser(expectedUser opslevel.User) (string, error) { + cliArgs := []string{expectedUser.Email, expectedUser.Name} + cmd.RootCmd.SetArgs(cliArgs) + + // Create User + createOutput, err := execCmd(Create, "user", cliArgs...) + if err != nil { + return "", fmt.Errorf("Create 'user' failed, got error: %v", err) + } + return asString(createOutput), nil +} + +func getUser(userId string) (*opslevel.User, error) { + getOutput, err := execCmd(Get, "user", userId) + if err != nil { + return nil, fmt.Errorf("Get 'user' failed, got error: %v", err) + } + + createdUser, err := jsonToResource[opslevel.User](getOutput) + if err != nil { + return nil, fmt.Errorf("Failed to convert JSON from API to 'opslevel.User' struct") + } + return createdUser, err +} + +func updateUser(userId string, userToUpdate opslevel.User) (string, error) { + if err := writeToYaml(userFileName, userToUpdate); err != nil { + return "", fmt.Errorf("Error while writing '%v' to file '%s': %v", userToUpdate, userFileName, err) + } + + // Store Update User stuff to "file" + cliArgs := []string{userId, "-f", userFileName} + updateOutput, err := execCmd(Update, "user", cliArgs...) + if err != nil { + return "", fmt.Errorf("Update 'user' failed, got error: %v", err) + } + return asString(updateOutput), nil +} + +func deleteUser(userId string) error { + _, err := execCmd(Delete, "user", userId) + return err +} diff --git a/src/cmd/util_helper_test.go b/src/cmd/util_helper_test.go new file mode 100644 index 00000000..48c1d8b2 --- /dev/null +++ b/src/cmd/util_helper_test.go @@ -0,0 +1,9 @@ +package cmd + +// Workaround for testing unexported functions. +// +// Running `go help build` displays: +// When compiling packages, build ignores files that end in '_test.go'. +var ( + RootCmd = rootCmd +) diff --git a/src/cmd/util_test.go b/src/cmd/util_test.go new file mode 100644 index 00000000..e06fc70a --- /dev/null +++ b/src/cmd/util_test.go @@ -0,0 +1,85 @@ +package cmd_test + +import ( + "encoding/json" + "io" + "os" + "strings" + + "github.com/opslevel/cli/cmd" + "gopkg.in/yaml.v2" +) + +type Operation string + +const ( + Assign Operation = "assign" + Create Operation = "create" + Delete Operation = "delete" + Example Operation = "example" + Get Operation = "get" + List Operation = "list" + Update Operation = "update" + Unassign Operation = "unassign" +) + +// execute any OpsLevel CLI command +func execCmd(command Operation, resource string, inputs ...string) ([]byte, error) { + cliArgs := []string{string(command), resource} + cliArgs = append(cliArgs, inputs...) + + r, oldStdout, err := redirectStdout() + defer r.Close() + if err != nil { + return nil, err + } + + cmd.RootCmd.SetArgs(cliArgs) + if err = cmd.RootCmd.Execute(); err != nil { + return nil, err + } + + return captureOutput(r, oldStdout) +} + +// redirectStdout redirects os.Stdout to a pipe and returns the read and write ends of the pipe. +func redirectStdout() (*os.File, *os.File, error) { + r, w, err := os.Pipe() + oldStdout := os.Stdout + os.Stdout = w + return r, oldStdout, err +} + +// captureOutput reads from r until EOF and returns the result as a string. +func captureOutput(r *os.File, oldStdout *os.File) ([]byte, error) { + w := os.Stdout + os.Stdout = oldStdout + w.Close() + return io.ReadAll(r) +} + +// convert a simple API response to a string +func asString(data []byte) string { + return strings.TrimSpace(string(data)) +} + +// convert JSON response from API to OpsLevel resource +func jsonToResource[T any](jsonData []byte) (*T, error) { + var resource T + if err := json.Unmarshal(jsonData, &resource); err != nil { + return nil, err + } + return &resource, nil +} + +// write OpsLevel resource to YAML file for commands that read in a file +func writeToYaml(givenFileName string, opslevelResource any) error { + yamlData, err := yaml.Marshal(&opslevelResource) + if err != nil { + return err + } + if err = os.WriteFile(givenFileName, yamlData, 0o644); err != nil { + return err + } + return nil +}