Skip to content

Commit

Permalink
Db/integration tests (#311)
Browse files Browse the repository at this point in the history
* add integration tests for team and user

* add task test-integration

* add manual Github workflow for testing integrations

* separate tests out to cmd_test package

* handle os pipe error in testing

* PR feedback

* break up tests into smaller chunks

* PR feedback
  • Loading branch information
davidbloss authored Aug 9, 2024
1 parent 2b1e239 commit c6b7c15
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 2 deletions.
24 changes: 24 additions & 0 deletions .github/workflows/tests-integration.yml
Original file line number Diff line number Diff line change
@@ -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 }}
8 changes: 7 additions & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
103 changes: 103 additions & 0 deletions src/cmd/team_test.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion src/cmd/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 [email protected]`,
Args: cobra.ExactArgs(1),
ArgAliases: []string{"ID"},
Expand Down
103 changes: 103 additions & 0 deletions src/cmd/user_test.go
Original file line number Diff line number Diff line change
@@ -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: "[email protected]"},
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
}
9 changes: 9 additions & 0 deletions src/cmd/util_helper_test.go
Original file line number Diff line number Diff line change
@@ -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
)
85 changes: 85 additions & 0 deletions src/cmd/util_test.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit c6b7c15

Please sign in to comment.