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

dev: playwright mvp #2608

Merged
merged 19 commits into from
Sep 13, 2022
Merged
Show file tree
Hide file tree
Changes from 17 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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
/setup.cfg
/setup.pid
/config.json.bak
/*.session.json
*.zip
/test-results

*.pem
*.key
Expand Down
26 changes: 23 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
include Makefile.binaries.mk

CFGPARAMS = devtools/configparams/*.go
DB_URL = postgres://goalert@localhost:5432/goalert?sslmode=disable

DB_URL = postgres://goalert@localhost:5432/goalert
INT_DB = goalert_integration
INT_DB_URL = $(shell go run ./devtools/scripts/db-url "$(DB_URL)" "$(INT_DB)")
LOG_DIR=
GOPATH:=$(shell go env GOPATH)

Expand All @@ -31,6 +32,11 @@ ifeq ($(CI), 1)
PROD_CY_PROC = Procfile.cypress.ci
endif

INT_PROC = Procfile.integration
ifeq ($(CI), 1)
INT_PROC = Procfile.integration.ci
endif

ifeq ($(PUSH), 1)
PUSH_FLAG=--push
endif
Expand Down Expand Up @@ -105,6 +111,17 @@ start-prod: web/src/build/static/app.js $(BIN_DIR)/tools/prometheus
$(MAKE) $(MFLAGS) bin/goalert BUNDLE=1
go run ./devtools/runproc -f Procfile.prod -l Procfile.local

start-integration: web/src/build/static/app.js bin/goalert bin/psql-lite bin/waitfor bin/runproc $(BIN_DIR)/tools/prometheus
./bin/waitfor -timeout 1s "$(DB_URL)" || make postgres
./bin/psql-lite -d "$(DB_URL)" -c 'DROP DATABASE IF EXISTS $(INT_DB); CREATE DATABASE $(INT_DB);'
./bin/goalert --db-url "$(INT_DB_URL)" migrate
./bin/psql-lite -d "$(INT_DB_URL)" -c "insert into users (id, role, name) values ('00000000-0000-0000-0000-000000000001', 'admin', 'Admin McIntegrationFace'),('00000000-0000-0000-0000-000000000002', 'user', 'User McIntegrationFace');"
./bin/goalert add-user --db-url "$(INT_DB_URL)" --user-id=00000000-0000-0000-0000-000000000001 --user admin --pass admin123
./bin/goalert add-user --db-url "$(INT_DB_URL)" --user-id=00000000-0000-0000-0000-000000000002 --user user --pass user1234
cat test/integration/setup/goalert-config.json | ./bin/goalert set-config --allow-empty-data-encryption-key --db-url "$(INT_DB_URL)"
rm -f *.session.json
GOALERT_DB_URL="$(INT_DB_URL)" ./bin/runproc -f $(INT_PROC)

jest: node_modules
yarn workspace goalert-web run jest $(JEST_ARGS)

Expand Down Expand Up @@ -145,10 +162,13 @@ generate: node_modules pkg/sysapi/sysapi.pb.go pkg/sysapi/sysapi_grpc.pb.go


test-all: test-unit test-smoke test-integration
test-integration: cy-wide-prod-run cy-mobile-prod-run
test-integration: playwright-run cy-wide-prod-run cy-mobile-prod-run
test-smoke: smoketest
test-unit: test

playwright-run: node_modules web/src/build/static/app.js bin/goalert web/src/schema.d.ts $(BIN_DIR)/tools/prometheus
yarn playwright test

smoketest:
(cd test/smoke && go test -parallel 10 -timeout 20m)

Expand Down
9 changes: 9 additions & 0 deletions Procfile.integration
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
build: while true; do make -qs bin/goalert BUNDLE=1 || make bin/goalert BUNDLE=1 || (echo '\033[0;31mBuild Failure'; sleep 3); sleep 0.1; done

@watch-file=./bin/goalert
goalert: ./bin/goalert -l=localhost:6130 --public-url=http://localhost:6130 --engine-cycle-time=50ms

smtp: go run github.com/mailhog/MailHog -ui-bind-addr=localhost:6125 -api-bind-addr=localhost:6125 -smtp-bind-addr=localhost:6105 | grep -v KEEPALIVE

@watch-file=./web/src/esbuild.config.js
ui: yarn workspace goalert-web run esbuild --watch --prod
3 changes: 3 additions & 0 deletions Procfile.integration.ci
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
goalert: ./bin/goalert -l=localhost:6130 --public-url=http://localhost:6130 --engine-cycle-time=50ms

smtp: go run github.com/mailhog/MailHog -ui-bind-addr=localhost:6125 -api-bind-addr=localhost:6125 -smtp-bind-addr=localhost:6105 | grep -v KEEPALIVE
4 changes: 4 additions & 0 deletions app/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,8 @@ func getConfig(ctx context.Context) (Config, error) {
SysAPIKeyFile: viper.GetString("sysapi-key-file"),
SysAPICAFile: viper.GetString("sysapi-ca-file"),

EngineCycleTime: viper.GetDuration("engine-cycle-time"),

HTTPPrefix: viper.GetString("http-prefix"),

SlackBaseURL: viper.GetString("slack-base-url"),
Expand Down Expand Up @@ -707,6 +709,8 @@ func init() {
RootCmd.Flags().String("tls-cert-data", "", "Specifies a PEM-encoded certificate. Has no effect if --listen-tls is unset.")
RootCmd.Flags().String("tls-key-data", "", "Specifies a PEM-encoded private key. Has no effect if --listen-tls is unset.")

RootCmd.Flags().Duration("engine-cycle-time", def.EngineCycleTime, "Time between engine cycles.")

RootCmd.Flags().String("http-prefix", def.HTTPPrefix, "Specify the HTTP prefix of the application.")
RootCmd.Flags().MarkDeprecated("http-prefix", "use --public-url instead")

Expand Down
2 changes: 2 additions & 0 deletions app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ type Config struct {
KubernetesCooldown time.Duration
StatusAddr string

EngineCycleTime time.Duration

EncryptionKeys keyring.Keys

RegionName string
Expand Down
3 changes: 3 additions & 0 deletions app/defaults.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package app

import "time"

// Defaults returns the default app config.
func Defaults() Config {
return Config{
Expand All @@ -9,5 +11,6 @@ func Defaults() Config {
MaxReqBodyBytes: 256 * 1024,
MaxReqHeaderBytes: 4096,
RegionName: "default",
EngineCycleTime: 5 * time.Second,
}
}
2 changes: 2 additions & 0 deletions app/initengine.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ func (app *App) initEngine(ctx context.Context) error {

Keys: app.cfg.EncryptionKeys,

CycleTime: app.cfg.EngineCycleTime,

MaxMessages: 50,

DisableCycle: app.cfg.APIOnly,
Expand Down
36 changes: 36 additions & 0 deletions devtools/scripts/db-url/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package main

import (
"fmt"
"log"
"net/url"
"os"
"strings"
)

func SetDB(input, db string) string {
u, err := url.Parse(input)
if err != nil {
return ""
}

u.Path = "/" + strings.TrimPrefix(db, "/")

return u.String()
}

func main() {
log.SetFlags(log.Lshortfile)
if len(os.Args) < 3 {
log.Fatal("missing argument")
os.Exit(1)
}

newURL := SetDB(os.Args[1], os.Args[2])
if newURL == "" {
log.Fatal("invalid URL")
os.Exit(1)
}

fmt.Println(newURL)
}
18 changes: 18 additions & 0 deletions devtools/scripts/db-url/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package main

import "testing"

func TestSetDB(t *testing.T) {
check := func(input, db, expected string) {
t.Helper()
actual := SetDB(input, db)
if actual != expected {
t.Errorf("SetDB(%q, %q) = %q; want %q", input, db, actual, expected)
}
}

check("postgres://localhost:5432", "test", "postgres://localhost:5432/test")
check("postgresql://postgres@?client_encoding=UTF8", "test", "postgresql://postgres@/test?client_encoding=UTF8")
check("postgres://goalert@localhost:5432/goalert", "test", "postgres://goalert@localhost:5432/test")
check("postgres://goalert@localhost:5432/goalert?sslmode=disable", "test", "postgres://goalert@localhost:5432/test?sslmode=disable")
}
4 changes: 4 additions & 0 deletions engine/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package engine

import (
"time"

"github.com/target/goalert/alert"
"github.com/target/goalert/alert/alertlog"
"github.com/target/goalert/auth/authlink"
Expand Down Expand Up @@ -35,4 +37,6 @@ type Config struct {

DisableCycle bool
LogCycles bool

CycleTime time.Duration
}
6 changes: 5 additions & 1 deletion engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,11 @@ func (p *Engine) _run(ctx context.Context) error {
}
}

alertTicker := time.NewTicker(5 * time.Second)
dur := p.cfg.CycleTime
if dur == 0 {
dur = 5 * time.Second
}
alertTicker := time.NewTicker(dur)
defer alertTicker.Stop()

defer close(p.triggerCh)
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
]
},
"devDependencies": {
"@playwright/test": "1.25.1",
"@typescript-eslint/eslint-plugin": "5.36.2",
"@typescript-eslint/parser": "5.36.1",
"eslint": "7.32.0",
Expand All @@ -39,6 +40,7 @@
"eslint-plugin-promise": "6.0.1",
"eslint-plugin-react": "7.31.1",
"eslint-plugin-react-hooks": "4.6.0",
"playwright": "1.25.1",
"prettier": "2.7.1",
"stylelint": "14.11.0",
"stylelint-config-standard": "28.0.0",
Expand Down
40 changes: 40 additions & 0 deletions playwright.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { devices } from '@playwright/test'

const config = {
testDir: './test/integration',
globalSetup: require.resolve('./test/integration/setup/global-setup.ts'),
retries: 3,
use: {
trace: 'on-first-retry',
baseURL: 'http://localhost:6130',
viewport: { width: 1440, height: 900 },
timezoneId: 'America/Chicago',
launchOptions: {
// slowMo: 1000,
},
actionTimeout: 5000,
},
projects: [
{
name: 'chromium-wide',
use: {
...devices['Desktop Chrome'],
viewportSize: { width: 1440, height: 900 },
},
},
{
name: 'chromium-mobile',
use: {
...devices['Pixel 5'],
viewportSize: { width: 375, height: 667 },
},
},
],
webServer: {
command: 'make start-integration CI=1',
url: 'http://localhost:6130/health',
reuseExistingServer: true,
},
}

module.exports = config
74 changes: 74 additions & 0 deletions test/integration/email-cm.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { test, expect } from '@playwright/test'
import { userSessionFile } from './lib'
import Chance from 'chance'
const c = new Chance()

test.describe.configure({ mode: 'parallel' })
test.use({ storageState: userSessionFile })

// test create, verify, and delete of an EMAIL contact method
test('EMAIL contact method', async ({ page, browser }) => {
const name = 'pw-email ' + c.name()
const email = 'pw-email-' + c.email()

await page.goto('./profile')
await page.click('[aria-label="Add Items"]')
await page.click('[aria-label="Add Contact Method"]')

await page.fill('input[name=name]', name)
await page.fill('input[name=type]', 'EMAIL')
await page.fill('input[name=value]', email)
await page.click('[role=dialog] button[type=submit]')

const mail = await browser.newPage({
baseURL: 'http://localhost:6125',
viewport: { width: 800, height: 600 },
})
await mail.goto('./')
await mail.fill('#search', email)
await mail.press('#search', 'Enter')

const message = mail.locator('.messages .msglist-message', {
hasText: 'Verification Message',
})
await expect
.poll(
async () => {
await mail.click('button[title=Refresh]')
return await message.isVisible()
},
{ message: 'wait for verification code email', timeout: 10000 },
)
.toBe(true)

await message.click()

const code = await mail
.frameLocator('#preview-html')
.locator('.invite-code')
.textContent()
if (!code) {
throw new Error('No code found')
}
await mail.close()

await page.fill('input[name=code]', code)
await page.click('[role=dialog] button[type=submit]')
await page.locator('[role=dialog]').isHidden()

await page
.locator('.MuiCard-root', {
has: page.locator('div > div > h2', { hasText: 'Contact Methods' }),
})
.locator('li', { hasText: email })
.locator('[aria-label="Other Actions"]')
.click()
await page.locator('[role=menuitem]', { hasText: 'Delete' }).click()
await page.locator('button[type=submit]', { hasText: 'Confirm' }).click()
await page
.locator('.MuiCard-root', {
has: page.locator('div > div > h2', { hasText: 'Contact Methods' }),
})
.locator('li', { hasText: email })
.isHidden()
})
1 change: 1 addition & 0 deletions test/integration/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './login'
40 changes: 40 additions & 0 deletions test/integration/lib/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Page } from '@playwright/test'
import path from 'path'

export const adminSessionFile = path.join(
__dirname,
'../../../admin.session.json',
)
export const userSessionFile = path.join(
__dirname,
'../../../user.session.json',
)

export const adminUserCreds = {
name: 'Admin McIntegrationFace',
user: 'admin',
pass: 'admin123',
}
export const normalUserCreds = {
name: 'User McIntegrationFace',
user: 'user',
pass: 'user1234',
}

export type Creds = typeof adminUserCreds | typeof normalUserCreds

export async function login(
page: Page,
user: string,
pass: string,
): Promise<void> {
await page.fill('input[name=username]', user)
await page.fill('input[name=password]', pass)
await page.click('button[type=submit] >> "Login"')
}

export async function logout(page: Page): Promise<void> {
// click logout from manage profile
await page.locator('[aria-label="Manage Profile"]').click()
await page.locator('button >> "Logout"').click()
}
Loading