Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add integration tests for important commands #210

Merged
merged 9 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,6 @@ jobs:
uses: golangci/golangci-lint-action@v3
with:
version: v1.59

- name: Run tests
run: make test
20 changes: 8 additions & 12 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ stages:
- check
- php
- build
- behave-test
- integration-test
- release

.go-cache:
Expand Down Expand Up @@ -127,18 +127,14 @@ build:
- dist/
expire_in: 1 day

behave-test-linux:
stage: behave-test
integration-test-linux:
stage: integration-test
image: cimg/go:1.22
extends: .go-cache
dependencies:
- build
variables:
PATH_CLI: platform_linux_amd64_v1/platform
before_script:
- apt-get install -y python3 python3-pip
- pip3 install --no-cache-dir behave sh selenium requests
script:
- bash tests/test-behave.sh
image: pjcdawkins/platformsh-cli
script: |
TEST_CLI_PATH=$PWD/dist/platform_linux_amd64_v1/platform go test -v ./tests/...

release:
stage: release
Expand All @@ -152,7 +148,7 @@ release:
- make release
dependencies:
- unit-test-lint
- behave-test-linux
- integration-test-linux
- build-php-linux-arm
- build-php-linux-x86
- build-php-macos-arm
Expand Down
12 changes: 8 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
PHP_VERSION = 8.2.24
LEGACY_CLI_VERSION = 4.21.0
LEGACY_CLI_VERSION = 4.21.1

GORELEASER_ID ?= platform

Expand Down Expand Up @@ -62,7 +62,7 @@ php: $(PHP_BINARY_PATH)

.PHONY: goreleaser
goreleaser:
go install github.com/goreleaser/goreleaser@$(GORELEASER_VERSION)
command -v goreleaser >/dev/null || go install github.com/goreleaser/goreleaser@$(GORELEASER_VERSION)

.PHONY: single
single: goreleaser internal/legacy/archives/platform.phar php ## Build a single target release for Platform.sh or Upsun
Expand All @@ -84,10 +84,14 @@ release: goreleaser clean-phar internal/legacy/archives/platform.phar php ## Rel
.PHONY: test
test: ## Run unit tests
go clean -testcache
go test -v -race -mod=readonly -cover ./...
go test -v -race -short -cover ./...

.PHONY: integration-test
integration-test: single
go test -v ./tests/...

golangci-lint:
go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
command -v golangci-lint >/dev/null || go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)

.PHONY: lint
lint: golangci-lint ## Run linter
Expand Down
2 changes: 1 addition & 1 deletion commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import (
"os/exec"
"path/filepath"
"regexp"
"slices"
"strings"

"github.com/fatih/color"
"github.com/platformsh/platformify/commands"
"github.com/platformsh/platformify/vendorization"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/exp/slices"

"github.com/platformsh/cli/internal"
"github.com/platformsh/cli/internal/config"
Expand Down
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ toolchain go1.22.4

require (
github.com/fatih/color v1.17.0
github.com/go-chi/chi/v5 v5.1.0
github.com/go-playground/validator/v10 v10.20.0
github.com/gofrs/flock v0.8.1
github.com/mattn/go-isatty v0.0.20
github.com/oklog/ulid/v2 v2.1.0
github.com/platformsh/platformify v0.2.11
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
github.com/wk8/go-ordered-map/v2 v2.1.8
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
golang.org/x/crypto v0.24.0
gopkg.in/yaml.v3 v3.0.1
)

Expand Down Expand Up @@ -58,7 +60,7 @@ require (
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/term v0.21.0 // indirect
Expand Down
9 changes: 5 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
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/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
Expand Down Expand Up @@ -90,12 +92,11 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
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/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/platformsh/platformify v0.2.9 h1:JKakEM6kY3p+IG3/J427Cw8T97pyPnPMomx9FzWuvgE=
github.com/platformsh/platformify v0.2.9/go.mod h1:fgmCcfQfHbhe1oXsIdIhpnniyZu8IdIMOlcBAa/ygic=
github.com/platformsh/platformify v0.2.10 h1:5/b5hXpXWV0rVswstvx1fSmE7c7qaYs3u2pICDCcA3E=
github.com/platformsh/platformify v0.2.10/go.mod h1:fgmCcfQfHbhe1oXsIdIhpnniyZu8IdIMOlcBAa/ygic=
github.com/platformsh/platformify v0.2.11 h1:9TRej4tDgQahRfl1tDOGaCry79yXYXbzDR1ZMdOPsU8=
github.com/platformsh/platformify v0.2.11/go.mod h1:fgmCcfQfHbhe1oXsIdIhpnniyZu8IdIMOlcBAa/ygic=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down
80 changes: 80 additions & 0 deletions internal/mockapi/api_server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Package mockapi provides mocks of the HTTP API for use in integration tests.
package mockapi

import (
"encoding/json"
"net/http"
"strings"
"testing"

"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/stretchr/testify/require"
)

type Handler struct {
*chi.Mux

t *testing.T

store
}

func NewHandler(t *testing.T) *Handler {
h := &Handler{t: t}
h.Mux = chi.NewRouter()

if testing.Verbose() {
h.Mux.Use(middleware.DefaultLogger)
}

h.Mux.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
authHeader := req.Header.Get("Authorization")
require.NotEmpty(t, authHeader)
require.True(t, strings.HasPrefix(authHeader, "Bearer "))
next.ServeHTTP(w, req)
})
})

h.Mux.Get("/users/me", h.handleUsersMe)
h.Mux.Get("/users/{id}/extended-access", h.handleUserExtendedAccess)
h.Mux.Get("/ref/users", h.handleUserRefs)
h.Mux.Post("/me/verification", func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]any{"state": false, "type": ""})
})

h.Mux.Get("/organizations", h.handleListOrgs)
h.Mux.Post("/organizations", h.handleCreateOrg)
h.Mux.Get("/organizations/{id}", h.handleGetOrg)
h.Mux.Patch("/organizations/{id}", h.handlePatchOrg)
h.Mux.Get("/users/{id}/organizations", h.handleListOrgs)
h.Mux.Get("/ref/organizations", h.handleOrgRefs)

h.Mux.Post("/organizations/{id}/subscriptions", h.handleCreateSubscription)
h.Mux.Get("/subscriptions/{id}", h.handleGetSubscription)
h.Mux.Get("/organizations/{id}/setup/options", func(w http.ResponseWriter, _ *http.Request) {
type options struct {
Plans []string `json:"plans"`
Regions []string `json:"regions"`
}
_ = json.NewEncoder(w).Encode(options{[]string{"development"}, []string{"test-region"}})
})
h.Mux.Get("/organizations/{id}/subscriptions/estimate", func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]any{"total": "$1,000 USD"})
})

h.Mux.Get("/projects/{id}", h.handleGetProject)
h.Mux.Patch("/projects/{id}", h.handlePatchProject)
h.Mux.Get("/projects/{id}/environments", h.handleListEnvironments)
h.Mux.Get("/projects/{project_id}/environments/{environment_id}", h.handleGetEnvironment)
h.Mux.Get("/projects/{project_id}/environments/{environment_id}/backups", h.handleListBackups)
h.Mux.Post("/projects/{project_id}/environments/{environment_id}/backups", h.handleCreateBackup)
h.Mux.Get("/projects/{project_id}/environments/{environment_id}/deployments/current", h.handleGetCurrentDeployment)
h.Mux.Get("/projects/{project_id}/user-access", h.handleProjectUserAccess)
h.Mux.Get("/ref/projects", h.handleProjectRefs)

h.Mux.Get("/regions", h.handleListRegions)

return h
}
107 changes: 107 additions & 0 deletions internal/mockapi/auth_server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package mockapi

import (
"crypto/ed25519"
"crypto/rand"
"encoding/json"
"net/http"
"net/http/httptest"
"slices"
"testing"
"time"

"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
)

var ValidAPITokens = []string{"api-token-1"}
var accessTokens = []string{"access-token-1"}

// NewAuthServer creates a new mock authentication server.
// The caller must call Close() on the server when finished.
func NewAuthServer(t *testing.T) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if testing.Verbose() {
t.Log(req)
}
if req.Method == http.MethodPost && req.URL.Path == "/oauth2/token" {
require.NoError(t, req.ParseForm())
if gt := req.Form.Get("grant_type"); gt != "api_token" {
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]string{"error": "invalid grant type: " + gt})
return
}
apiToken := req.Form.Get("api_token")
if slices.Contains(ValidAPITokens, apiToken) {
_ = json.NewEncoder(w).Encode(struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
Type string `json:"token_type"`
}{AccessToken: accessTokens[0], ExpiresIn: 60, Type: "bearer"})
return
}
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]string{"error": "invalid API token"})
return
}

if req.Method == http.MethodPost && req.URL.Path == "/ssh" {
var options struct {
PublicKey string `json:"key"`
}
err := json.NewDecoder(req.Body).Decode(&options)
require.NoError(t, err)
key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(options.PublicKey))
require.NoError(t, err)
signer, err := sshSigner()
require.NoError(t, err)
extensions := make(map[string]string)

// Add standard ssh options
extensions["permit-X11-forwarding"] = ""
extensions["permit-agent-forwarding"] = ""
extensions["permit-port-forwarding"] = ""
extensions["permit-pty"] = ""
extensions["permit-user-rc"] = ""
cert := &ssh.Certificate{
Key: key,
Serial: 0,
CertType: ssh.UserCert,
KeyId: "test-key-id",
ValidAfter: uint64(time.Now().Add(-1 * time.Second).Unix()),
ValidBefore: uint64(time.Now().Add(time.Minute).Unix()),
Permissions: ssh.Permissions{
Extensions: extensions,
},
}
err = cert.SignCert(rand.Reader, signer)
require.NoError(t, err)
_ = json.NewEncoder(w).Encode(struct {
Cert string `json:"certificate"`
}{string(ssh.MarshalAuthorizedKey(cert))})
require.NoError(t, err)
return
}

w.WriteHeader(http.StatusNotFound)
_ = json.NewEncoder(w).Encode(map[string]string{"error": "not found"})
}))
}

var signer ssh.Signer // TODO reuse to validate SSH connection

func sshSigner() (ssh.Signer, error) {
if signer != nil {
return signer, nil
}
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
s, err := ssh.NewSignerFromKey(privateKey)
if err != nil {
return nil, err
}
signer = s
return s, nil
}
Loading
Loading