diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6cd3bc47..2dfb58569 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,30 +46,23 @@ jobs: CI_COMMIT_SHORT_SHA=$(echo $CI_COMMIT_SHA | head -c8) ./bin/platform self:build --no-composer-rebuild --yes --replace-version "$CI_COMMIT_REF_NAME"-"$CI_COMMIT_SHORT_SHA" --output platform.phar - - name: Clone main CLI repository - run: git clone https://github.com/platformsh/cli.git ./cli - - name: Set up Go uses: actions/setup-go@v4 with: go-version: 1.22 cache-dependency-path: cli/go.sum - - uses: actions/upload-artifact@v4 - with: - name: cli-phar - path: platform.phar - - name: Run integration tests run: | export TEST_CLI_PATH=$(realpath "./platform.phar") chmod +x "$TEST_CLI_PATH" - cd cli + cd go-tests + go test ./... -v - # Temporary workaround for new test (https://github.com/platformsh/cli/pull/216). - git grep -q can-create || git merge af8078a84dd0ade724e88577773b098bd2527344 - - go test ./tests -v + - uses: actions/upload-artifact@v4 + with: + name: cli-phar + path: platform.phar # TODO run these when upgraded for PHP 8+ compatibility # - name: Run unit tests diff --git a/go-tests/app_config_test.go b/go-tests/app_config_test.go new file mode 100644 index 000000000..8c6a28cc7 --- /dev/null +++ b/go-tests/app_config_test.go @@ -0,0 +1,96 @@ +package tests + +import ( + "encoding/base64" + "encoding/json" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/platformsh/cli/pkg/mockapi" +) + +func TestAppConfig(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + apiHandler := mockapi.NewHandler(t) + + projectID := "aht1iegh3nei9" + + apiHandler.SetProjects([]*mockapi.Project{{ + ID: projectID, + Links: mockapi.MakeHALLinks("self=/projects/"+projectID, + "environments=/projects/"+projectID+"/environments"), + DefaultBranch: "main", + }}) + + main := makeEnv(projectID, "main", "production", "active", nil) + main.SetCurrentDeployment(&mockapi.Deployment{ + WebApps: map[string]mockapi.App{ + "app": {Name: "app", Type: "golang:1.23", Size: "M", Disk: 2048, Mounts: map[string]mockapi.Mount{}}, + }, + Links: mockapi.MakeHALLinks("self=/projects/" + projectID + "/environments/main/deployment/current"), + }) + envs := []*mockapi.Environment{main} + + apiHandler.SetEnvironments(envs) + + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + run := runnerWithAuth(t, apiServer.URL, authServer.URL) + + assert.Equal(t, strings.TrimLeft(` +name: app +type: 'golang:1.23' +size: M +disk: 2048 +mounts: { } +`, "\n"), run("app:config", "-p", projectID, "-e", ".", "--refresh")) + + assert.Equal(t, "golang:1.23\n", run("app:config", "-p", projectID, "-e", ".", "--refresh", "-P", "type")) +} + +func TestAppConfigLocal(t *testing.T) { + run := runWithLocalApp(t, &mockapi.App{ + Name: "local-app", + Type: "golang:1.24", + Size: "L", + Disk: 1024, + Mounts: map[string]mockapi.Mount{ + "example": { + Source: "local", + SourcePath: "example", + }, + }, + }) + + assert.Equal(t, strings.TrimLeft(` +name: local-app +type: 'golang:1.24' +size: L +disk: 1024 +mounts: + example: + source: local + source_path: example +`, "\n"), run("app:config")) + + assert.Equal(t, "local\n", run("app:config", "--property", "mounts.example.source")) +} + +func runWithLocalApp(t *testing.T, app *mockapi.App) func(args ...string) string { + return func(args ...string) string { + j, err := json.Marshal(app) + require.NoError(t, err) + cmd := command(t, args...) + cmd.Env = append(cmd.Env, "PLATFORM_APPLICATION="+base64.StdEncoding.EncodeToString(j)) + b, err := cmd.Output() + require.NoError(t, err) + return string(b) + } +} diff --git a/go-tests/app_list_test.go b/go-tests/app_list_test.go new file mode 100644 index 000000000..67396eeea --- /dev/null +++ b/go-tests/app_list_test.go @@ -0,0 +1,76 @@ +package tests + +import ( + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/platformsh/cli/pkg/mockapi" +) + +func TestAppList(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + apiHandler := mockapi.NewHandler(t) + + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + projectID := "nu8ohgeizah1a" + + apiHandler.SetProjects([]*mockapi.Project{{ + ID: projectID, + Links: mockapi.MakeHALLinks("self=/projects/"+projectID, + "environments=/projects/"+projectID+"/environments"), + DefaultBranch: "main", + }}) + + main := makeEnv(projectID, "main", "production", "active", nil) + main.SetCurrentDeployment(&mockapi.Deployment{ + WebApps: map[string]mockapi.App{ + "app": {Name: "app", Type: "golang:1.23", Size: "AUTO"}, + }, + Services: map[string]mockapi.App{}, + Routes: map[string]any{}, + Workers: map[string]mockapi.Worker{ + "app--worker1": { + App: mockapi.App{Name: "app--worker1", Type: "golang:1.23", Size: "AUTO"}, + Worker: mockapi.WorkerInfo{Commands: mockapi.Commands{Start: "sleep 60"}}, + }, + }, + Links: mockapi.MakeHALLinks("self=/projects/" + projectID + "/environments/main/deployment/current"), + }) + + envs := []*mockapi.Environment{ + main, + makeEnv(projectID, "staging", "staging", "active", "main"), + makeEnv(projectID, "dev", "development", "active", "staging"), + makeEnv(projectID, "fix", "development", "inactive", "dev"), + } + + apiHandler.SetEnvironments(envs) + + run := runnerWithAuth(t, apiServer.URL, authServer.URL) + + assert.Equal(t, strings.TrimLeft(` +Name Type +app golang:1.23 +`, "\n"), run("apps", "-p", projectID, "-e", ".", "--refresh", "--format", "tsv")) + + assert.Equal(t, strings.TrimLeft(` ++--------------+-------------+-------------------+ +| Name | Type | Commands | ++--------------+-------------+-------------------+ +| app--worker1 | golang:1.23 | start: 'sleep 60' | ++--------------+-------------+-------------------+ +`, "\n"), run("workers", "-v", "-p", projectID, "-e", ".")) + + runCombinedOutput := runnerCombinedOutput(t, apiServer.URL, authServer.URL) + co, err := runCombinedOutput("services", "-p", projectID, "-e", "main") + require.NoError(t, err) + assert.Contains(t, co, "No services found") +} diff --git a/go-tests/auth_info_test.go b/go-tests/auth_info_test.go new file mode 100644 index 000000000..5b389ef02 --- /dev/null +++ b/go-tests/auth_info_test.go @@ -0,0 +1,52 @@ +package tests + +import ( + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/platformsh/cli/pkg/mockapi" +) + +func TestAuthInfo(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + apiHandler := mockapi.NewHandler(t) + apiHandler.SetMyUser(&mockapi.User{ + ID: "my-user-id", + Deactivated: false, + Namespace: "ns", + Username: "my-username", + FirstName: "Foo", + LastName: "Bar", + Email: "my-user@example.com", + EmailVerified: true, + Picture: "https://example.com/profile.png", + Country: "NO", + PhoneNumberVerified: true, + MFAEnabled: true, + }) + + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + run := runnerWithAuth(t, apiServer.URL, authServer.URL) + + assert.Equal(t, strings.TrimLeft(` ++-----------------------+---------------------+ +| Property | Value | ++-----------------------+---------------------+ +| id | my-user-id | +| first_name | Foo | +| last_name | Bar | +| username | my-username | +| email | my-user@example.com | +| phone_number_verified | true | ++-----------------------+---------------------+ +`, "\n"), run("auth:info", "-v", "--refresh")) + + assert.Equal(t, "my-user-id\n", run("auth:info", "-P", "id")) +} diff --git a/go-tests/backup_test.go b/go-tests/backup_test.go new file mode 100644 index 000000000..486244799 --- /dev/null +++ b/go-tests/backup_test.go @@ -0,0 +1,118 @@ +package tests + +import ( + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/platformsh/cli/pkg/mockapi" +) + +func TestBackupList(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + apiHandler := mockapi.NewHandler(t) + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + projectID := "rai7quieroohu" + + apiHandler.SetProjects([]*mockapi.Project{ + { + ID: projectID, + DefaultBranch: "main", + Links: mockapi.MakeHALLinks( + "self=/projects/"+projectID, + "environments=/projects/"+projectID+"/environments", + ), + }, + }) + main := makeEnv(projectID, "main", "production", "active", nil) + main.Links["backups"] = mockapi.HALLink{HREF: "/projects/" + projectID + "//environments/main/backups"} + apiHandler.SetEnvironments([]*mockapi.Environment{main}) + + created1, err := time.Parse(time.RFC3339, "2014-04-01T10:00:00+01:00") + require.NoError(t, err) + created2, err := time.Parse(time.RFC3339, "2015-04-01T10:00:00+01:00") + require.NoError(t, err) + + apiHandler.SetProjectBackups(projectID, []*mockapi.Backup{ + { + ID: "123", + EnvironmentID: "main", + Status: "CREATED", + Safe: true, + Restorable: true, + Automated: false, + CommitID: "foo", + CreatedAt: created1, + }, + { + ID: "456", + EnvironmentID: "main", + Status: "CREATED", + Safe: false, + Restorable: true, + Automated: true, + CommitID: "bar", + CreatedAt: created2, + }, + }) + + run := runnerWithAuth(t, apiServer.URL, authServer.URL) + + assert.Equal(t, strings.TrimLeft(` ++---------------------------+-----------+------------+ +| Created | Backup ID | Restorable | ++---------------------------+-----------+------------+ +| 2015-04-01T09:00:00+00:00 | 456 | true | +| 2014-04-01T09:00:00+00:00 | 123 | true | ++---------------------------+-----------+------------+ +`, "\n"), run("backups", "-p", projectID, "-e", ".")) + + assert.Equal(t, strings.TrimLeft(` ++---------------------------+-----------+------------+-----------+-----------+ +| Created | Backup ID | Restorable | Automated | Commit ID | ++---------------------------+-----------+------------+-----------+-----------+ +| 2015-04-01T09:00:00+00:00 | 456 | true | true | bar | +| 2014-04-01T09:00:00+00:00 | 123 | true | false | foo | ++---------------------------+-----------+------------+-----------+-----------+ +`, "\n"), run("backups", "-p", projectID, "-e", ".", "--columns", "+automated,commit_id")) +} + +func TestBackupCreate(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + apiHandler := mockapi.NewHandler(t) + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + projectID := "vei8wah5Ohl2e" + + apiHandler.SetProjects([]*mockapi.Project{ + { + ID: projectID, + DefaultBranch: "main", + Links: mockapi.MakeHALLinks( + "self=/projects/"+projectID, + "environments=/projects/"+projectID+"/environments", + ), + }, + }) + main := makeEnv(projectID, "main", "production", "active", nil) + main.Links["backups"] = mockapi.HALLink{HREF: "/projects/" + projectID + "/environments/main//backups"} + main.Links["#backup"] = mockapi.HALLink{HREF: "/projects/" + projectID + "/environments/main/backups"} + apiHandler.SetEnvironments([]*mockapi.Environment{main}) + + run := runnerWithAuth(t, apiServer.URL, authServer.URL) + + run("backup", "-p", projectID, "-e", ".") + + assert.NotEmpty(t, run("backups", "-p", projectID, "-e", ".")) +} diff --git a/go-tests/config.yaml b/go-tests/config.yaml new file mode 100644 index 000000000..6bfed8518 --- /dev/null +++ b/go-tests/config.yaml @@ -0,0 +1,36 @@ +# Test CLI configuration +application: + name: 'Platform Test CLI' + slug: 'platform-test-cli' + version: '1.0.0' + executable: 'platform-test' + env_prefix: 'TEST_CLI_' + user_config_dir: '.platform-test-cli' + +service: + name: 'Platform.sh Testing' + env_prefix: 'PLATFORM_' + project_config_dir: '.platform' + console_url: 'https://console.cli-tests.example.com' + +api: + # Placeholder URLs which can be replaced during tests. + base_url: 'http://127.0.0.1' + auth_url: 'http://127.0.0.1' + + organizations: true + centralized_permissions: true + teams: true + user_verification: true + metrics: true + + vendor_filter: 'test-vendor' + +ssh: + domain_wildcards: ['*.cli-tests.example.com'] + +detection: + git_remote_name: 'platform-test' + git_domain: 'git.cli-tests.example.com' + site_domains: ['cli-tests.example.com'] + cluster_header: 'X-Platform-Cluster' diff --git a/go-tests/environment_list_test.go b/go-tests/environment_list_test.go new file mode 100644 index 000000000..e6a4f2d1e --- /dev/null +++ b/go-tests/environment_list_test.go @@ -0,0 +1,84 @@ +package tests + +import ( + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/platformsh/cli/pkg/mockapi" +) + +func TestEnvironmentList(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + apiHandler := mockapi.NewHandler(t) + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + apiHandler.SetProjects([]*mockapi.Project{ + { + ID: mockProjectID, + Links: mockapi.MakeHALLinks( + "self=/projects/"+mockProjectID, + "environments=/projects/"+mockProjectID+"/environments", + ), + }, + }) + apiHandler.SetEnvironments([]*mockapi.Environment{ + makeEnv(mockProjectID, "main", "production", "active", nil), + makeEnv(mockProjectID, "staging", "staging", "active", "main"), + makeEnv(mockProjectID, "dev", "development", "active", "staging"), + makeEnv(mockProjectID, "fix", "development", "inactive", "dev"), + }) + + run := runnerWithAuth(t, apiServer.URL, authServer.URL) + + assert.Equal(t, strings.TrimLeft(` ++-----------+---------+----------+-------------+ +| ID | Title | Status | Type | ++-----------+---------+----------+-------------+ +| main | Main | Active | production | +| staging | Staging | Active | staging | +| dev | Dev | Active | development | +| fix | Fix | Inactive | development | ++-----------+---------+----------+-------------+ +`, "\n"), run("environment:list", "-v", "-p", mockProjectID)) + + assert.Equal(t, strings.TrimLeft(` +ID Title Status Type +main Main Active production +staging Staging Active staging +dev Dev Active development +fix Fix Inactive development +`, "\n"), run("environment:list", "-v", "-p", mockProjectID, "--format", "plain")) + + assert.Equal(t, strings.TrimLeft(` +ID Title Status Type +main Main Active production +staging Staging Active staging +dev Dev Active development +`, "\n"), run("environment:list", "-v", "-p", mockProjectID, "--format", "plain", "--no-inactive")) + + assert.Equal(t, "fix\n", + run("environment:list", "-v", "-p", mockProjectID, "--pipe", "--status=inactive")) +} + +func makeEnv(projectID, name, envType, status string, parent any) *mockapi.Environment { + return &mockapi.Environment{ + ID: name, + Name: name, + MachineName: name + "-xyz", + Title: strings.ToTitle(name[:1]) + name[1:], + Parent: parent, + Type: envType, + Status: status, + Project: projectID, + Links: mockapi.MakeHALLinks( + "self=/projects/" + url.PathEscape(projectID) + "/environments/" + url.PathEscape(name), + ), + } +} diff --git a/go-tests/go.mod b/go-tests/go.mod new file mode 100644 index 000000000..0a023b5b9 --- /dev/null +++ b/go-tests/go.mod @@ -0,0 +1,15 @@ +module github.com/platformsh/legacy-cli/tests + +go 1.22.9 + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/go-chi/chi/v5 v5.1.0 // indirect + github.com/oklog/ulid/v2 v2.1.0 // indirect + github.com/platformsh/cli v0.0.0-20241126124927-2e901f7c6a3b // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/stretchr/testify v1.9.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/sys v0.21.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go-tests/go.sum b/go-tests/go.sum new file mode 100644 index 000000000..6c4f70255 --- /dev/null +++ b/go-tests/go.sum @@ -0,0 +1,20 @@ +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= +github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/platformsh/cli v0.0.0-20241126124927-2e901f7c6a3b h1:Pbyjf2FNzShe71EJnG/8ezhUl63RjkC8y7VGmBGmIXM= +github.com/platformsh/cli v0.0.0-20241126124927-2e901f7c6a3b/go.mod h1:j9Aj8DxVGyn+Jm3ntopLnk6p0XtOeLWVBpF3zhqHh7M= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go-tests/mount_list_test.go b/go-tests/mount_list_test.go new file mode 100644 index 000000000..7b53823d5 --- /dev/null +++ b/go-tests/mount_list_test.go @@ -0,0 +1,80 @@ +package tests + +import ( + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/platformsh/cli/pkg/mockapi" +) + +func TestMountList(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + apiHandler := mockapi.NewHandler(t) + + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + projectID := "oa3chu0foot4s" + + apiHandler.SetProjects([]*mockapi.Project{{ + ID: projectID, + Links: mockapi.MakeHALLinks("self=/projects/"+projectID, + "environments=/projects/"+projectID+"/environments"), + DefaultBranch: "main", + }}) + + main := makeEnv(projectID, "main", "production", "active", nil) + main.Links["pf:ssh:app"] = mockapi.HALLink{HREF: "ssh://" + projectID + "--app@ssh.example.com"} + main.SetCurrentDeployment(&mockapi.Deployment{ + WebApps: map[string]mockapi.App{ + "app": { + Name: "app", + Type: "golang:1.23", + Size: "AUTO", + Mounts: map[string]mockapi.Mount{ + "/public/sites/default/files": {Source: "local", SourcePath: "files"}, + }}, + }, + Services: map[string]mockapi.App{}, + Routes: map[string]any{}, + Workers: map[string]mockapi.Worker{}, + Links: mockapi.MakeHALLinks("self=/projects/" + projectID + "/environments/main/deployment/current"), + }) + + apiHandler.SetEnvironments([]*mockapi.Environment{main}) + + run := runnerWithAuth(t, apiServer.URL, authServer.URL) + + assert.Equal(t, strings.TrimLeft(` +Mount path Definition +public/sites/default/files "source: local +source_path: files" +`, "\n"), run("mounts", "-p", projectID, "-e", "main", "--refresh", "--format", "tsv")) + + assert.Equal(t, "public/sites/default/files\n", run("mounts", "-p", projectID, "-e", "main", "--paths")) +} + +func TestMountListLocal(t *testing.T) { + run := runWithLocalApp(t, &mockapi.App{ + Name: "local-app", + Type: "golang:1.24", + Size: "L", + Mounts: map[string]mockapi.Mount{ + "/tmp": {Source: "local", SourcePath: "tmp"}, + }, + }) + + assert.Equal(t, strings.TrimLeft(` ++------------+------------------+ +| Mount path | Definition | ++------------+------------------+ +| tmp | source: local | +| | source_path: tmp | ++------------+------------------+ +`, "\n"), run("mounts")) +} diff --git a/go-tests/org_create_test.go b/go-tests/org_create_test.go new file mode 100644 index 000000000..a46ac96ce --- /dev/null +++ b/go-tests/org_create_test.go @@ -0,0 +1,63 @@ +package tests + +import ( + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/platformsh/cli/pkg/mockapi" +) + +func TestOrgCreate(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + myUserID := "user-for-org-create-test" + + apiHandler := mockapi.NewHandler(t) + apiHandler.SetMyUser(&mockapi.User{ID: myUserID}) + apiHandler.SetOrgs([]*mockapi.Org{ + makeOrg("org-id-1", "acme", "ACME Inc.", myUserID), + }) + + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + run := runnerWithAuth(t, apiServer.URL, authServer.URL) + + // TODO disable the cache? + run("cc") + + assert.Equal(t, strings.TrimLeft(` ++------+-----------+--------------------------------------+ +| Name | Label | Owner email | ++------+-----------+--------------------------------------+ +| acme | ACME Inc. | user-for-org-create-test@example.com | ++------+-----------+--------------------------------------+ +`, "\n"), run("orgs")) + + runCombinedOutput := runnerCombinedOutput(t, apiServer.URL, authServer.URL) + + co, err := runCombinedOutput("org:create", "--name", "hooli", "--yes") + assert.Error(t, err) + assert.Contains(t, co, "--country is required") + + co, err = runCombinedOutput("org:create", "--name", "hooli", "--yes", "--country", "XY") + assert.Error(t, err) + assert.Contains(t, co, "Invalid country: XY") + + co, err = runCombinedOutput("org:create", "--name", "hooli", "--yes", "--country", "US") + assert.NoError(t, err) + assert.Contains(t, co, "Hooli") + + assert.Equal(t, strings.TrimLeft(` ++-------+-----------+--------------------------------------+ +| Name | Label | Owner email | ++-------+-----------+--------------------------------------+ +| acme | ACME Inc. | user-for-org-create-test@example.com | +| hooli | Hooli | user-for-org-create-test@example.com | ++-------+-----------+--------------------------------------+ +`, "\n"), run("orgs")) +} diff --git a/go-tests/org_info_test.go b/go-tests/org_info_test.go new file mode 100644 index 000000000..bdfdd8820 --- /dev/null +++ b/go-tests/org_info_test.go @@ -0,0 +1,44 @@ +package tests + +import ( + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/platformsh/cli/pkg/mockapi" +) + +func TestOrgInfo(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + myUserID := "user-for-org-info-test" + + apiHandler := mockapi.NewHandler(t) + apiHandler.SetMyUser(&mockapi.User{ID: myUserID}) + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + apiHandler.SetOrgs([]*mockapi.Org{ + makeOrg("org-id-1", "org-1", "Org 1", myUserID), + }) + + run := runnerWithAuth(t, apiServer.URL, authServer.URL) + + assert.Contains(t, run("org:info", "-o", "org-1", "--format", "csv", "--refresh"), `Property,Value +id,org-id-1 +name,org-1 +label,Org 1 +owner_id,user-for-org-info-test +capabilities,`) + + assert.Equal(t, "Org 1\n", run("org:info", "-o", "org-1", "label")) + + runCombinedOutput := runnerCombinedOutput(t, apiServer.URL, authServer.URL) + co, err := runCombinedOutput("org:info", "-o", "org-1", "label", "New Label") + assert.NoError(t, err) + assert.Contains(t, co, "Property label set to: New Label\n") + + assert.Equal(t, "New Label\n", run("org:info", "-o", "org-1", "label")) +} diff --git a/go-tests/org_list_test.go b/go-tests/org_list_test.go new file mode 100644 index 000000000..849b22695 --- /dev/null +++ b/go-tests/org_list_test.go @@ -0,0 +1,66 @@ +package tests + +import ( + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/platformsh/cli/pkg/mockapi" +) + +func TestOrgList(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + myUserID := "user-id-1" + + apiHandler := mockapi.NewHandler(t) + apiHandler.SetMyUser(&mockapi.User{ID: myUserID}) + apiHandler.SetOrgs([]*mockapi.Org{ + makeOrg("org-id-1", "acme", "ACME Inc.", myUserID), + makeOrg("org-id-2", "four-seasons", "Four Seasons Total Landscaping", myUserID), + makeOrg("org-id-3", "duff", "Duff Beer", "user-id-2"), + }) + + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + run := runnerWithAuth(t, apiServer.URL, authServer.URL) + + assert.Equal(t, strings.TrimLeft(` ++--------------+--------------------------------+-----------------------+ +| Name | Label | Owner email | ++--------------+--------------------------------+-----------------------+ +| acme | ACME Inc. | user-id-1@example.com | +| duff | Duff Beer | user-id-2@example.com | +| four-seasons | Four Seasons Total Landscaping | user-id-1@example.com | ++--------------+--------------------------------+-----------------------+ +`, "\n"), run("orgs")) + + assert.Equal(t, strings.TrimLeft(` +Name Label Owner email +acme ACME Inc. user-id-1@example.com +duff Duff Beer user-id-2@example.com +four-seasons Four Seasons Total Landscaping user-id-1@example.com +`, "\n"), run("orgs", "--format", "plain")) + + assert.Equal(t, strings.TrimLeft(` +org-id-1,acme +org-id-3,duff +org-id-2,four-seasons +`, "\n"), run("orgs", "--format", "csv", "--columns", "id,name", "--no-header")) +} + +func makeOrg(id, name, label, owner string) *mockapi.Org { + return &mockapi.Org{ + ID: id, + Name: name, + Label: label, + Owner: owner, + Capabilities: []string{}, + Links: mockapi.MakeHALLinks("self=/organizations/" + url.PathEscape(id)), + } +} diff --git a/go-tests/project_create_test.go b/go-tests/project_create_test.go new file mode 100644 index 000000000..ea1051f3d --- /dev/null +++ b/go-tests/project_create_test.go @@ -0,0 +1,219 @@ +package tests + +import ( + "bytes" + "io" + "net/http/httptest" + "os" + "os/exec" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/platformsh/cli/pkg/mockapi" +) + +func TestProjectCreate(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + apiHandler := mockapi.NewHandler(t) + apiHandler.SetOrgs([]*mockapi.Org{ + makeOrg("cli-test-id", "cli-tests", "CLI Test Org", "my-user-id"), + }) + + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + title := "Test Project Title" + region := "test-region" + + cmd := authenticatedCommand(t, apiServer.URL, authServer.URL, + "project:create", "-v", "--region", region, "--title", title, "--org", "cli-tests") + + var stdErrBuf bytes.Buffer + var stdOutBuf bytes.Buffer + cmd.Stderr = &stdErrBuf + if testing.Verbose() { + cmd.Stderr = io.MultiWriter(&stdErrBuf, os.Stderr) + } + cmd.Stdout = &stdOutBuf + t.Log("Running:", cmd) + require.NoError(t, cmd.Run()) + + // stdout should contain the project ID. + projectID := strings.TrimSpace(stdOutBuf.String()) + assert.NotEmpty(t, projectID) + + // stderr should contain various messages. + stderr := stdErrBuf.String() + + assert.Contains(t, stderr, "The estimated monthly cost of this project is: $1,000 USD") + assert.Contains(t, stderr, "Region: "+region) + assert.Contains(t, stderr, "Project ID: "+projectID) + assert.Contains(t, stderr, "Project title: "+title) +} + +func TestProjectCreate_CanCreateError(t *testing.T) { + cases := []struct { + orgName string + canCreateResponse *mockapi.CanCreateResponse + expectExitCode int + expectStderrEquals string + }{ + { + orgName: "need-cc", + canCreateResponse: &mockapi.CanCreateResponse{ + Message: "Please add a credit card for verification to continue with the project creation process.", + RequiredAction: &mockapi.CanCreateRequiredAction{ + Action: "verification", + Type: "credit-card", + }, + }, + // The API message is replaced for known verification methods. + // The Console link is shown when the CLI's service.console_url is configured. + expectStderrEquals: "Credit card verification is required before creating a project.\n\n" + + "Please use Console to create your first project:\n" + + "https://console.cli-tests.example.com\n", + }, + { + orgName: "need-phone", + canCreateResponse: &mockapi.CanCreateResponse{ + Message: "Please verify your phone number to continue with the project creation process.", + RequiredAction: &mockapi.CanCreateRequiredAction{ + Action: "verification", + Type: "phone", + }, + }, + expectStderrEquals: "Phone number verification is required before creating a project.\n\n" + + "Please open the following URL in a browser to verify your phone number:\n" + + "https://console.cli-tests.example.com/-/phone-verify\n", + }, + { + orgName: "need-support", + canCreateResponse: &mockapi.CanCreateResponse{ + Message: "Please contact support in order to proceed.", + RequiredAction: &mockapi.CanCreateRequiredAction{ + Action: "verification", + Type: "ticket", + }, + }, + expectStderrEquals: "Verification via a support ticket is required before creating a project.\n\n" + + "Please open the following URL in a browser to create a ticket:\n" + + "https://console.cli-tests.example.com/support\n", + }, + { + orgName: "inactive", + canCreateResponse: &mockapi.CanCreateResponse{ + Message: "You cannot create projects at this time because your organization is inactive.", + RequiredAction: &mockapi.CanCreateRequiredAction{ + Action: "ticket", + }, + }, + expectStderrEquals: "You cannot create projects at this time because your organization is inactive.\n\n" + + "Please open the following URL in a browser to create a ticket:\n" + + "https://console.cli-tests.example.com/support\n", + }, + { + orgName: "arbitrary-ticket-message", + canCreateResponse: &mockapi.CanCreateResponse{ + Message: "[Arbitrary message displayed alongside the ticket action]", + RequiredAction: &mockapi.CanCreateRequiredAction{ + Action: "ticket", + }, + }, + expectStderrEquals: "[Arbitrary message displayed alongside the ticket action]\n\n" + + "Please open the following URL in a browser to create a ticket:\n" + + "https://console.cli-tests.example.com/support\n", + }, + { + orgName: "overdue-invoice", + canCreateResponse: &mockapi.CanCreateResponse{ + Message: "You cannot create projects at this time because your organization has an overdue invoice.", + RequiredAction: &mockapi.CanCreateRequiredAction{Action: "billing_details"}, + }, + expectStderrEquals: "You cannot create projects at this time because your organization has an overdue invoice.\n\n" + + "View or update billing details at:\n" + + "https://console.cli-tests.example.com/overdue-invoice/-/billing\n", + }, + { + orgName: "trial-limit-reached", + canCreateResponse: &mockapi.CanCreateResponse{ + Message: "You have reached the resources limit for this organization's trial.", + RequiredAction: &mockapi.CanCreateRequiredAction{Action: "billing_details"}, + }, + expectStderrEquals: "You have reached the resources limit for this organization's trial.\n\n" + + "View or update billing details at:\n" + + "https://console.cli-tests.example.com/trial-limit-reached/-/billing\n", + }, + { + orgName: "billing-details-arbitrary-message", + canCreateResponse: &mockapi.CanCreateResponse{ + Message: "[Arbitrary message displayed alongside billing_details action]", + RequiredAction: &mockapi.CanCreateRequiredAction{Action: "billing_details"}, + }, + expectStderrEquals: "[Arbitrary message displayed alongside billing_details action]\n\n" + + "View or update billing details at:\n" + + "https://console.cli-tests.example.com/billing-details-arbitrary-message/-/billing\n", + }, + { + orgName: "license-activation", + canCreateResponse: &mockapi.CanCreateResponse{ + Message: "Your organization license is being processed. Please try again in a few seconds", + RequiredAction: &mockapi.CanCreateRequiredAction{Action: "retry", Type: "license_activation"}, + }, + expectStderrEquals: "Your organization license is being processed. Please try again in a few seconds\n", + }, + { + orgName: "new-unknown-verification-message", + canCreateResponse: &mockapi.CanCreateResponse{ + Message: "Arbitrary verification message.", + RequiredAction: &mockapi.CanCreateRequiredAction{Action: "verification"}, + }, + expectStderrEquals: "Arbitrary verification message.\n", + }, + { + orgName: "new-unknown-action-and-message", + canCreateResponse: &mockapi.CanCreateResponse{ + Message: "Arbitrary message for unknown action.", + RequiredAction: &mockapi.CanCreateRequiredAction{Action: "unknown"}, + }, + expectStderrEquals: "Arbitrary message for unknown action.\n", + }, + } + + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + apiHandler := mockapi.NewHandler(t) + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + orgs := make([]*mockapi.Org, 0, len(cases)) + for _, c := range cases { + orgs = append(orgs, makeOrg(c.orgName+"-id", c.orgName, c.orgName, "my-user-id")) + apiHandler.SetCanCreate(c.orgName+"-id", c.canCreateResponse) + } + apiHandler.SetOrgs(orgs) + + for _, c := range cases { + t.Run("can_create_"+c.orgName, func(t *testing.T) { + cmd := authenticatedCommand(t, apiServer.URL, authServer.URL, + "project:create", "-v", "--org", c.orgName) + var stdErrBuf bytes.Buffer + cmd.Stderr = &stdErrBuf + if testing.Verbose() { + cmd.Stderr = io.MultiWriter(&stdErrBuf, os.Stderr) + } + t.Log("Running:", cmd) + err := cmd.Run() + stdErr := stdErrBuf.String() + ee := &exec.ExitError{} + require.ErrorAs(t, err, &ee) + assert.Equal(t, 1, ee.ExitCode()) + assert.Equal(t, c.expectStderrEquals, stdErr) + }) + } +} diff --git a/go-tests/project_info_test.go b/go-tests/project_info_test.go new file mode 100644 index 000000000..b78b4be15 --- /dev/null +++ b/go-tests/project_info_test.go @@ -0,0 +1,84 @@ +package tests + +import ( + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/platformsh/cli/pkg/mockapi" +) + +func TestProjectInfo(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + myUserID := "my-user-id" + vendor := "test-vendor" + + apiHandler := mockapi.NewHandler(t) + apiHandler.SetMyUser(&mockapi.User{ID: myUserID}) + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + apiHandler.SetOrgs([]*mockapi.Org{ + makeOrg("org-id-1", "org-1", "Org 1", myUserID), + }) + + projectID := "eer4jee4ri3mo" + created, err := time.Parse(time.RFC3339, "2014-04-01T10:00:00+01:00") + require.NoError(t, err) + + apiHandler.SetProjects([]*mockapi.Project{ + { + ID: projectID, + Title: "Project 1", + Region: "region-1", + Organization: "org-id-1", + Vendor: vendor, + Repository: mockapi.ProjectRepository{ + URL: "git@git.region-1.example.com:mock-project.git", + }, + DefaultBranch: "main", + CreatedAt: created, + UpdatedAt: created.Add(time.Second * 86400), + Links: mockapi.MakeHALLinks( + "self=/projects/"+url.PathEscape(projectID), + "#edit=/projects/"+url.PathEscape(projectID), + ), + }, + }) + + run := runnerWithAuth(t, apiServer.URL, authServer.URL) + + expectedLines := `Property Value +id eer4jee4ri3mo +title Project 1 +region region-1 +organization org-id-1 +vendor test-vendor +repository url: 'git@git.region-1.example.com:mock-project.git' +default_branch main +created_at 2014-04-01T09:00:00+00:00 +updated_at 2014-04-02T09:00:00+00:00 +git git@git.region-1.example.com:mock-project.git` + + output := run("pro:info", "-p", projectID, "--format", "plain", "--refresh") + + for _, line := range strings.Split(expectedLines, "\n") { + assert.True(t, strings.Contains(output, line+"\n")) + } + + assert.Equal(t, "2014-04-01\n", run("pro:info", "-p", projectID, "created_at", "--date-fmt", "Y-m-d")) + + assert.Equal(t, "Project 1\n", run("pro:info", "-p", projectID, "title")) + + run("pro:info", "-v", "-p", projectID, "title", "New Title") + + // TODO --refresh should not be needed here + assert.Equal(t, "New Title\n", run("pro:info", "-p", projectID, "title", "--refresh")) +} diff --git a/go-tests/project_list_test.go b/go-tests/project_list_test.go new file mode 100644 index 000000000..659e00828 --- /dev/null +++ b/go-tests/project_list_test.go @@ -0,0 +1,122 @@ +package tests + +import ( + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/platformsh/cli/pkg/mockapi" +) + +func TestProjectList(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + myUserID := "my-user-id" + otherUserID := "other-user-id" + vendor := "test-vendor" + + apiHandler := mockapi.NewHandler(t) + apiHandler.SetMyUser(&mockapi.User{ID: myUserID}) + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + apiHandler.SetOrgs([]*mockapi.Org{ + makeOrg("org-id-1", "org-1", "Org 1", myUserID), + makeOrg("org-id-2", "org-2", "Org 2", otherUserID), + }) + apiHandler.SetProjects([]*mockapi.Project{ + makeProject("project-id-1", "org-id-1", vendor, "Project 1", "region-1"), + makeProject("project-id-2", "org-id-2", vendor, "Project 2", "region-2"), + makeProject("project-id-3", "org-id-2", vendor, "Project 3", "region-2"), + makeProject("project-other-vendor-3", "org-other-vendor", "acme", "Other Vendor's Project", "region-1"), + }) + apiHandler.SetUserGrants([]*mockapi.UserGrant{ + { + ResourceID: "org-id-1", + ResourceType: "organization", + OrganizationID: "org-id-1", + UserID: myUserID, + Permissions: []string{"admin"}, + }, + { + ResourceID: "project-id-1", + ResourceType: "project", + OrganizationID: "org-id-1", + UserID: myUserID, + Permissions: []string{"admin"}, + }, + { + ResourceID: "project-id-2", + ResourceType: "project", + OrganizationID: "org-id-2", + UserID: "user-id-2", + Permissions: []string{"admin"}, + }, + { + ResourceID: "project-id-2", + ResourceType: "project", + OrganizationID: "org-id-2", + UserID: myUserID, + Permissions: []string{"viewer", "development:admin"}, + }, + { + ResourceID: "project-id-3", + ResourceType: "project", + OrganizationID: "org-id-2", + UserID: myUserID, + Permissions: []string{"viewer", "development:contributor"}, + }, + }) + + run := runnerWithAuth(t, apiServer.URL, authServer.URL) + + assert.Equal(t, strings.TrimLeft(` ++--------------+-----------+----------+--------------+ +| ID | Title | Region | Organization | ++--------------+-----------+----------+--------------+ +| project-id-1 | Project 1 | region-1 | org-1 | +| project-id-2 | Project 2 | region-2 | org-2 | +| project-id-3 | Project 3 | region-2 | org-2 | ++--------------+-----------+----------+--------------+ +`, "\n"), run("pro", "-v")) + + assert.Equal(t, strings.TrimLeft(` +ID Title Region Organization +project-id-1 Project 1 region-1 org-1 +project-id-2 Project 2 region-2 org-2 +project-id-3 Project 3 region-2 org-2 +`, "\n"), run("pro", "-v", "--format", "plain")) + + assert.Equal(t, strings.TrimLeft(` +ID,Organization ID +project-id-1,org-id-1 +project-id-2,org-id-2 +project-id-3,org-id-2 +`, "\n"), run("pro", "-v", "--format", "csv", "--columns", "id,organization_id")) + + assert.Equal(t, strings.TrimLeft(` +ID Title Region Organization +project-id-1 Project 1 region-1 org-1 +`, "\n"), run("pro", "-v", "--format", "plain", "--my")) + + assert.Equal(t, strings.TrimLeft(` +project-id-1 +project-id-2 +project-id-3 +`, "\n"), run("pro", "-v", "--pipe")) +} + +func makeProject(id, org, vendor, title, region string) *mockapi.Project { + return &mockapi.Project{ + ID: id, + Organization: org, + Vendor: vendor, + Title: title, + Region: region, + Links: mockapi.MakeHALLinks("self=/projects/" + url.PathEscape(id)), + } +} diff --git a/go-tests/ssh_cert_test.go b/go-tests/ssh_cert_test.go new file mode 100644 index 000000000..81bcbfaad --- /dev/null +++ b/go-tests/ssh_cert_test.go @@ -0,0 +1,29 @@ +package tests + +import ( + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/platformsh/cli/pkg/mockapi" +) + +func TestSSHCerts(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + myUserID := "my-user-id" + + apiHandler := mockapi.NewHandler(t) + apiHandler.SetMyUser(&mockapi.User{ID: myUserID}) + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + run := runnerWithAuth(t, apiServer.URL, authServer.URL) + + output := run("ssh-cert:info") + assert.Regexp(t, `(?m)^filename: .+?id_ed25519-cert\.pub$`, output) + assert.Contains(t, output, "key_id: test-key-id\n") + assert.Contains(t, output, "key_type: ssh-ed25519-cert-v01@openssh.com\n") +} diff --git a/go-tests/tests.go b/go-tests/tests.go new file mode 100644 index 000000000..fde6f5927 --- /dev/null +++ b/go-tests/tests.go @@ -0,0 +1,118 @@ +// Package tests contains integration tests, which run the CLI as a shell command and verify its output. +// +// A TEST_CLI_PATH environment variable can be provided to override the path to a +// CLI executable. It defaults to `platform` in the repository root. +package tests + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/platformsh/cli/pkg/mockapi" +) + +var _validatedCommand string + +// The legacy CLI identifier expects project IDs to be alphanumeric. +// See: https://github.com/platformsh/legacy-cli/blob/main/src/Service/Identifier.php#L75 +const mockProjectID = "abcdefg123456" + +func getCommandName(t *testing.T) string { + if testing.Short() { + t.Skip("skipping integration test due to -short flag") + } + if _validatedCommand != "" { + return _validatedCommand + } + candidate := os.Getenv("TEST_CLI_PATH") + if candidate != "" { + _, err := os.Stat(candidate) + require.NoError(t, err) + } else { + matches, _ := filepath.Glob("../bin/platform") + if len(matches) == 0 { + t.Skipf("skipping integration tests: CLI not found matching path: %s", "../bin/platform") + return "" + } + c, err := filepath.Abs(matches[0]) + require.NoError(t, err) + candidate = c + } + versionCmd := exec.Command(candidate, "--version") + versionCmd.Env = testEnv() + output, err := versionCmd.Output() + require.NoError(t, err, "running '--version' must succeed under the CLI at: %s", candidate) + require.Contains(t, string(output), "Platform Test CLI ") + t.Logf("Validated CLI command %s", candidate) + _validatedCommand = candidate + return _validatedCommand +} + +func command(t *testing.T, args ...string) *exec.Cmd { + cmd := exec.Command(getCommandName(t), args...) //nolint:gosec + cmd.Env = testEnv() + cmd.Dir = os.TempDir() + if testing.Verbose() { + cmd.Stderr = os.Stderr + } + return cmd +} + +func authenticatedCommand(t *testing.T, apiURL, authURL string, args ...string) *exec.Cmd { + cmd := command(t, args...) + cmd.Env = append( + cmd.Env, + EnvPrefix+"API_BASE_URL="+apiURL, + EnvPrefix+"API_AUTH_URL="+authURL, + EnvPrefix+"TOKEN="+mockapi.ValidAPITokens[0], + ) + return cmd +} + +// runnerWithAuth returns a function to authenticate and run a CLI command, returning stdout output. +// This asserts that the command has not failed. +func runnerWithAuth(t *testing.T, apiURL, authURL string) func(args ...string) string { + return func(args ...string) string { + cmd := authenticatedCommand(t, apiURL, authURL, args...) + t.Log("Running:", cmd) + b, err := cmd.Output() + require.NoError(t, err) + return string(b) + } +} + +// runnerCombinedOutput returns a function to authenticate and run a CLI command, returning combined output. +func runnerCombinedOutput(t *testing.T, apiURL, authURL string) func(args ...string) (string, error) { + return func(args ...string) (string, error) { + cmd := authenticatedCommand(t, apiURL, authURL, args...) + var b bytes.Buffer + cmd.Stdout = &b + cmd.Stderr = &b + t.Log("Running:", cmd) + err := cmd.Run() + return b.String(), err + } +} + +const EnvPrefix = "TEST_CLI_" + +func testEnv() []string { + configPath, err := filepath.Abs("config.yaml") + if err != nil { + panic(err) + } + return append( + os.Environ(), + "COLUMNS=120", + "CLI_CONFIG_FILE="+configPath, + EnvPrefix+"NO_INTERACTION=1", + EnvPrefix+"VERSION=1.0.0", + EnvPrefix+"HOME="+os.TempDir(), + "TZ=UTC", + ) +} diff --git a/go-tests/user_list_test.go b/go-tests/user_list_test.go new file mode 100644 index 000000000..45b3f2fe1 --- /dev/null +++ b/go-tests/user_list_test.go @@ -0,0 +1,80 @@ +package tests + +import ( + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/platformsh/cli/pkg/mockapi" +) + +func TestUserList(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + myUserID := "my-user-id" + vendor := "test-vendor" + + apiHandler := mockapi.NewHandler(t) + apiHandler.SetMyUser(&mockapi.User{ID: myUserID}) + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + apiHandler.SetOrgs([]*mockapi.Org{ + makeOrg("org-id-1", "org-1", "Org 1", myUserID), + }) + apiHandler.SetProjects([]*mockapi.Project{ + makeProject(mockProjectID, "org-id-1", vendor, "Project 1", "region-1"), + }) + apiHandler.SetUserGrants([]*mockapi.UserGrant{ + { + ResourceID: "org-id-1", + ResourceType: "organization", + OrganizationID: "org-id-1", + UserID: myUserID, + Permissions: []string{"admin"}, + }, + { + ResourceID: mockProjectID, + ResourceType: "project", + OrganizationID: "org-id-1", + UserID: myUserID, + Permissions: []string{"admin"}, + }, + { + ResourceID: mockProjectID, + ResourceType: "project", + OrganizationID: "org-id-1", + UserID: "user-id-2", + Permissions: []string{"viewer", "development:viewer"}, + }, + { + ResourceID: mockProjectID, + ResourceType: "project", + OrganizationID: "org-id-1", + UserID: "user-id-3", + Permissions: []string{"viewer", "production:viewer", "development:admin", "staging:contributor"}, + }, + }) + + run := runnerWithAuth(t, apiServer.URL, authServer.URL) + + assert.Equal(t, strings.TrimLeft(` ++------------------------+-----------------+--------------+------------+ +| Email address | Name | Project role | ID | ++------------------------+-----------------+--------------+------------+ +| my-user-id@example.com | User my-user-id | admin | my-user-id | +| user-id-2@example.com | User user-id-2 | viewer | user-id-2 | +| user-id-3@example.com | User user-id-3 | viewer | user-id-3 | ++------------------------+-----------------+--------------+------------+ +`, "\n"), run("users", "-p", mockProjectID)) + + assert.Equal(t, strings.TrimLeft(` +Email address Name Project role ID Permissions +my-user-id@example.com User my-user-id admin my-user-id admin +user-id-2@example.com User user-id-2 viewer user-id-2 viewer, development:viewer +user-id-3@example.com User user-id-3 viewer user-id-3 viewer, production:viewer, development:admin, staging:contributor +`, "\n"), run("users", "-p", mockProjectID, "--format", "plain", "--columns", "+perm%")) +}