Skip to content

Commit

Permalink
feat: add an initial spike of playwright UI tests
Browse files Browse the repository at this point in the history
They are called from the feature e2e tests, this lets us use cucumber statements to set up UI test scenarios.

Signed-off-by: Hiram Chirino <[email protected]>
  • Loading branch information
chirino committed Nov 14, 2023
1 parent 0c967fd commit d31ddef
Show file tree
Hide file tree
Showing 18 changed files with 763 additions and 20 deletions.
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ ui/node_modules
Containerfile.*
dist
*-envs.json
.git/

# config
.auth-providers.yaml
Expand All @@ -14,6 +15,7 @@ dist
.gen-*
.*-lint
.generate
.git

#tests
./tests/*.json
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ jobs:
tags: quay.io/nexodus/nexd:latest
- name: envsubst
tags: quay.io/nexodus/envsubst:latest
- name: playwright
tags: quay.io/nexodus/playwright:latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
Expand Down Expand Up @@ -297,13 +299,20 @@ jobs:
name: envsubst
path: /tmp

- name: Download playwright image
uses: actions/download-artifact@v3
with:
name: playwright
path: /tmp

- name: Load Docker images
run: |
docker load --input /tmp/apiserver.tar
docker load --input /tmp/frontend.tar
docker load --input /tmp/ipam.tar
docker load --input /tmp/nexd.tar
docker load --input /tmp/envsubst.tar
docker load --input /tmp/playwright.tar
- name: Setup KIND
run: |
Expand Down
9 changes: 9 additions & 0 deletions Containerfile.playwright
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM mcr.microsoft.com/playwright:v1.39.0-jammy

ENV NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/cacerts.crt

RUN apt-get update -y \
&& apt-get install -y libnss3-tools \
&& apt-get clean all

COPY --chmod=755 ./hack/update-ca.sh /update-ca.sh
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ dist/.generate: $(SWAGGER_YAML) dist/.ui-fmt docs/user-guide/nexd.md docs/user-g
$(CMD_PREFIX) touch $@

.PHONY: e2e
e2e: e2eprereqs dist/nexd dist/nexctl image-nexd ## Run e2e verbose tests
e2e: e2eprereqs dist/nexd dist/nexctl image-nexd image-playwright ## Run e2e verbose tests
CGO_ENABLED=1 gotestsum --format $(GOTESTSUM_FMT) -- \
-race --tags=integration ./integration-tests/... $(shell [ -z "$$NEX_TEST" ] || echo "-run $$NEX_TEST" )

Expand Down Expand Up @@ -610,6 +610,14 @@ image-envsubst:
docker build -f Containerfile.envsubst -t quay.io/nexodus/envsubst:$(TAG) .
docker tag quay.io/nexodus/envsubst:$(TAG) quay.io/nexodus/envsubst:latest

.PHONY: image-playwright ## Build the playwright image
image-playwright: dist/.image-playwright
dist/.image-playwright: Containerfile.playwright hack/update-ca.sh | dist
$(CMD_PREFIX) docker build -f Containerfile.playwright \
-t quay.io/nexodus/playwright:$(TAG) .
$(CMD_PREFIX) docker tag quay.io/nexodus/nexd:$(TAG) quay.io/nexodus/nexd:latest
$(CMD_PREFIX) touch $@

.PHONY: images
images: image-nexd image-frontend image-apiserver image-ipam image-envsubst ## Create container images

Expand Down
10 changes: 9 additions & 1 deletion hack/update-ca.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@ set -e

if [ -f /.certs/rootCA.pem ]; then
if [ -x /usr/sbin/update-ca-certificates ]; then
cp /.certs/rootCA.pem /usr/local/share/ca-certificates/rootCA.crt
cp /.certs/rootCA.pem /usr/local/share/ca-certificates/cacerts.crt
/usr/sbin/update-ca-certificates 2> /dev/null > /dev/null

# the following is needed to get the playwright browsers to know about the root CA
if [ -x /usr/bin/certutil ]; then
mkdir -p $HOME/.pki/nssdb
/usr/bin/certutil --empty-password -d $HOME/.pki/nssdb -N
/usr/bin/certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n cacerts.crt -i /usr/local/share/ca-certificates/cacerts.crt
fi

elif [ -x /usr/bin/update-ca-trust ]; then
cp ./.certs/rootCA.pem /etc/pki/ca-trust/source/anchors/rootCA.crt
/usr/bin/update-ca-trust 2> /dev/null > /dev/null
Expand Down
10 changes: 10 additions & 0 deletions integration-tests/features/webui.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Feature: Web UI

Background:
Given a user named "Oscar" with password "testpass"
Given a user named "EvilBob" with password "testpass"

Scenario: Bob can login to the UI

Given I am logged in as "Oscar"
Then I run playwright script "./tests/login.spec.ts"
1 change: 1 addition & 0 deletions integration-tests/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func TestFeatures(t *testing.T) {
s.Context = ctx
s.ApiURL = "https://api.try.nexodus.127.0.0.1.nip.io"
s.TlsConfig = tlsConfig
s.TestingT = t

status := godog.TestSuite{
Name: shortName,
Expand Down
119 changes: 111 additions & 8 deletions integration-tests/steps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@ import (
"context"
"fmt"
"github.com/cucumber/godog"
"github.com/docker/docker/api/types/container"
"github.com/google/uuid"
"github.com/nexodus-io/nexodus/internal/cucumber"
"github.com/nexodus-io/nexodus/internal/wgcrypto"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"path"
"strings"
)

type extender struct {
Expand Down Expand Up @@ -34,6 +41,7 @@ func init() {
ctx.Step(`^I generate a new public key as \${([^}]*)}$`, e.iGenerateANewPublicKeyAsVariable)
ctx.Step(`^I generate a new key pair as \${([^}]*)}/\${([^}]*)}$`, e.iGenerateANewPublicKeyPairAsVariable)
ctx.Step(`^I decrypt the sealed "([^"]*)" with "([^"]*)" and store the result as \${([^}]*)}$`, e.iDeycryptTheSealedWithAndStoreTheResultAsDevice_bearer_token)
ctx.Step(`^I run playwright script "([^"]*)"$`, e.iRunPlaywrightScript)

ctx.After(func(ctx context.Context, sc *godog.Scenario, err error) (context.Context, error) {
if err == nil {
Expand All @@ -49,14 +57,14 @@ func init() {

func (s *extender) aUserNamedWithPassword(username string, password string) error {
// users are shared concurrently across scenarios. so lock while we create the user...
s.Suite.Mu.Lock()
defer s.Suite.Mu.Unlock()
s.TestSuite.Mu.Lock()

Check failure on line 60 in integration-tests/steps_test.go

View workflow job for this annotation

GitHub Actions / go-lint-linux (linux)

s.TestSuite undefined (type *extender has no field or method TestSuite) (typecheck)

Check failure on line 60 in integration-tests/steps_test.go

View workflow job for this annotation

GitHub Actions / go-lint-rest (darwin)

s.TestSuite undefined (type *extender has no field or method TestSuite) (typecheck)

Check failure on line 60 in integration-tests/steps_test.go

View workflow job for this annotation

GitHub Actions / go-lint-rest (windows)

s.TestSuite undefined (type *extender has no field or method TestSuite) (typecheck)
defer s.TestSuite.Mu.Unlock()

Check failure on line 61 in integration-tests/steps_test.go

View workflow job for this annotation

GitHub Actions / go-lint-linux (linux)

s.TestSuite undefined (type *extender has no field or method TestSuite) (typecheck)

Check failure on line 61 in integration-tests/steps_test.go

View workflow job for this annotation

GitHub Actions / go-lint-rest (darwin)

s.TestSuite undefined (type *extender has no field or method TestSuite) (typecheck)

Check failure on line 61 in integration-tests/steps_test.go

View workflow job for this annotation

GitHub Actions / go-lint-rest (windows)

s.TestSuite undefined (type *extender has no field or method TestSuite) (typecheck)

// user already exists...
if s.Users[username] != nil {

Check failure on line 64 in integration-tests/steps_test.go

View workflow job for this annotation

GitHub Actions / go-lint-linux (linux)

s.Users undefined (type *extender has no field or method Users) (typecheck)

Check failure on line 64 in integration-tests/steps_test.go

View workflow job for this annotation

GitHub Actions / go-lint-rest (darwin)

s.Users undefined (type *extender has no field or method Users) (typecheck)

Check failure on line 64 in integration-tests/steps_test.go

View workflow job for this annotation

GitHub Actions / go-lint-rest (windows)

s.Users undefined (type *extender has no field or method Users) (typecheck)
return nil
}
ctx := s.Suite.Context
ctx := s.TestSuite.Context

Check failure on line 67 in integration-tests/steps_test.go

View workflow job for this annotation

GitHub Actions / go-lint-linux (linux)

s.TestSuite undefined (type *extender has no field or method TestSuite) (typecheck)

Check failure on line 67 in integration-tests/steps_test.go

View workflow job for this annotation

GitHub Actions / go-lint-rest (darwin)

s.TestSuite undefined (type *extender has no field or method TestSuite) (typecheck)

Check failure on line 67 in integration-tests/steps_test.go

View workflow job for this annotation

GitHub Actions / go-lint-rest (windows)

s.TestSuite undefined (type *extender has no field or method TestSuite) (typecheck)
userId, cleanup, err := createNewUserWithName(ctx, username, password)
if err != nil {
return err
Expand All @@ -72,8 +80,8 @@ func (s *extender) aUserNamedWithPassword(username string, password string) erro
}

func (s *extender) storeUserId(name, varName string) error {
s.Suite.Mu.Lock()
defer s.Suite.Mu.Unlock()
s.TestSuite.Mu.Lock()
defer s.TestSuite.Mu.Unlock()
user := s.Users[name]
if user != nil {
s.Variables[varName] = user.Subject
Expand All @@ -82,16 +90,16 @@ func (s *extender) storeUserId(name, varName string) error {
}

func (s *extender) iAmLoggedInAs(username string) error {
s.Suite.Mu.Lock()
s.TestSuite.Mu.Lock()
user := s.Users[username]
s.Suite.Mu.Unlock()
s.TestSuite.Mu.Unlock()

if user == nil {
return fmt.Errorf("previous step has not defined user: %s", username)
}

// do the oauth login...
ctx := s.Suite.Context
ctx := s.TestSuite.Context
var err error
user.Token, err = getOauth2Token(ctx, user.Subject, user.Password)
if err != nil {
Expand Down Expand Up @@ -161,3 +169,98 @@ func (s *extender) iDeycryptTheSealedWithAndStoreTheResultAsDevice_bearer_token(
s.Variables[storeAs] = string(value)
return nil
}

func (s *extender) iRunPlaywrightScript(script string) error {
name := s.TestingT.Name() + "-" + uuid.New().String()
name = strings.ReplaceAll(name, "/", "-")

certsDir, err := findCertsDir()
require.NoError(s.TestingT, err)
projectDir := path.Join(certsDir, "..")

s.TestSuite.Mu.Lock()
user := s.Users[s.CurrentUser]
s.TestSuite.Mu.Unlock()

container, err := testcontainers.GenericContainer(s.Context, testcontainers.GenericContainerRequest{
ProviderType: providerType,
ContainerRequest: testcontainers.ContainerRequest{
// Too bad the following does not work on my mac..
//FromDockerfile: testcontainers.FromDockerfile{
// Context: projectDir,
// Dockerfile: "Containerfile.playwright",
// Repo: "quay.io",
// Tag: "nexodus/playwright:latest",
//},
Image: "quay.io/nexodus/playwright:latest",
Name: name,
Networks: []string{defaultNetwork},
HostConfigModifier: func(hostConfig *container.HostConfig) {
hostConfig.ExtraHosts = []string{
fmt.Sprintf("try.nexodus.127.0.0.1.nip.io:%s", hostDNSName),
fmt.Sprintf("api.try.nexodus.127.0.0.1.nip.io:%s", hostDNSName),
fmt.Sprintf("auth.try.nexodus.127.0.0.1.nip.io:%s", hostDNSName),
}
hostConfig.AutoRemove = false
},
Mounts: []testcontainers.ContainerMount{
{
Source: testcontainers.GenericBindMountSource{
HostPath: certsDir,
},
Target: "/.certs",
ReadOnly: true,
},
{
Source: testcontainers.GenericBindMountSource{
HostPath: path.Join(projectDir, "ui"),
},
Target: "/ui",
ReadOnly: false,
},
},
// User: fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()),
Env: map[string]string{
"CI": "true",
"NEXODUS_USERNAME": user.Subject,
"NEXODUS_PASSWORD": user.Password,
},
Cmd: []string{
"/update-ca.sh",
"npx", "playwright", "test", script,
},
ConfigModifier: func(config *container.Config) {
config.WorkingDir = "/ui"
},
},
Started: true,
})
if err != nil {
return err
}
defer func() {
go func() {
_ = container.Terminate(s.Context)
}()
}()
container.FollowOutput(FnConsumer{
Apply: func(l testcontainers.Log) {
text := string(l.Content)
s.Logf("%s", text)
},
})
err = container.StartLogProducer(s.Context)
if err != nil {
return err
}

wait.ForExit().WaitUntilReady(s.Context, container)
state, err := container.State(s.Context)
if err != nil {
return err
}
if state.ExitCode != 0 {
return fmt.Errorf("playwright failed with exit code %d", state.ExitCode)
}
return nil
}
17 changes: 12 additions & 5 deletions internal/cucumber/cucumber.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"reflect"
"strings"
"sync"
"testing"
"time"

"github.com/cucumber/godog"
Expand Down Expand Up @@ -74,6 +75,7 @@ type TestSuite struct {
nextOrgId uint32
TlsConfig *tls.Config
DB *gorm.DB
TestingT *testing.T
}

// TestUser represents a user that can login to the system. The same users are shared by
Expand All @@ -89,7 +91,8 @@ type TestUser struct {
// TestScenario holds that state of single scenario. It is not accessed
// concurrently.
type TestScenario struct {
Suite *TestSuite
*TestSuite
DB *gorm.DB
CurrentUser string
PathPrefix string
sessions map[string]*TestSession
Expand All @@ -98,9 +101,13 @@ type TestScenario struct {
hasTestCaseLock bool
}

func (s *TestScenario) Logf(format string, args ...any) {
s.TestingT.Logf(format, args...)
}

func (s *TestScenario) User() *TestUser {
s.Suite.Mu.Lock()
defer s.Suite.Mu.Unlock()
s.TestSuite.Mu.Lock()
defer s.TestSuite.Mu.Unlock()
return s.Users[s.CurrentUser]
}

Expand All @@ -111,7 +118,7 @@ func (s *TestScenario) Session() *TestSession {
TestUser: s.User(),
Client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: s.Suite.TlsConfig,
TLSClientConfig: s.TestSuite.TlsConfig,
},
},
Header: http.Header{},
Expand Down Expand Up @@ -360,7 +367,7 @@ var StepModules []func(ctx *godog.ScenarioContext, s *TestScenario)

func (suite *TestSuite) InitializeScenario(ctx *godog.ScenarioContext) {
s := &TestScenario{
Suite: suite,
TestSuite: suite,
Users: map[string]*TestUser{},
sessions: map[string]*TestSession{},
Variables: map[string]interface{}{},
Expand Down
2 changes: 1 addition & 1 deletion internal/cucumber/http_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func (s *TestScenario) SendHttpRequestWithJsonBodyAndStyle(method, path string,
if err != nil {
return err
}
fullUrl := s.Suite.ApiURL + s.PathPrefix + expandedPath
fullUrl := s.TestSuite.ApiURL + s.PathPrefix + expandedPath

// Lets reset all the response session state...
if session.Resp != nil {
Expand Down
4 changes: 4 additions & 0 deletions ui/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@
.env.production.local

npm-debug.log*
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
Loading

0 comments on commit d31ddef

Please sign in to comment.