From 6fb79e743e6a2d02b2f4f4fb245e8fdc56a2a2a3 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Wed, 31 Aug 2022 09:39:24 -0500 Subject: [PATCH 01/15] add playwright dependency --- package.json | 1 + yarn.lock | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/package.json b/package.json index 3b6c8decc4..a77a63b425 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "eslint-plugin-promise": "5.2.0", "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", diff --git a/yarn.lock b/yarn.lock index b9b467dd77..e8a35bce15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6439,6 +6439,18 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +playwright-core@1.25.1: + version "1.25.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.25.1.tgz#abe56aec8bef645fba988320d9f9328fafab0446" + integrity sha512-lSvPCmA2n7LawD2Hw7gSCLScZ+vYRkhU8xH0AapMyzwN+ojoDqhkH/KIEUxwNu2PjPoE/fcE0wLAksdOhJ2O5g== + +playwright@1.25.1: + version "1.25.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.25.1.tgz#23fe129ca05568a72ee2a3842baa0a1985d1b345" + integrity sha512-kOlW7mllnQ70ALTwAor73q/FhdH9EEXLUqjdzqioYLcSVC4n4NBfDqeCikGuayFZrLECLkU6Hcbziy/szqTXSA== + dependencies: + playwright-core "1.25.1" + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" From 4774469f961add7f070d1e1931dd1dda4df15604 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Wed, 31 Aug 2022 12:17:12 -0500 Subject: [PATCH 02/15] add test dep --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index a77a63b425..2e4e685494 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ ] }, "devDependencies": { + "@playwright/test": "1.25.1", "@typescript-eslint/eslint-plugin": "4.33.0", "@typescript-eslint/parser": "4.33.0", "eslint": "7.32.0", From 99b5d600af1d22064a81c8b105ddc41fee1c6a61 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Wed, 31 Aug 2022 12:17:38 -0500 Subject: [PATCH 03/15] use correct header tags --- web/src/app/details/DetailsPage.tsx | 2 +- web/src/app/main/components/ToolbarPageTitle.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/app/details/DetailsPage.tsx b/web/src/app/details/DetailsPage.tsx index 749c4fc6ec..adb87e9086 100644 --- a/web/src/app/details/DetailsPage.tsx +++ b/web/src/app/details/DetailsPage.tsx @@ -116,7 +116,7 @@ export default function DetailsPage(p: DetailsPageProps): JSX.Element { titleTypographyProps={{ 'data-cy': 'title', variant: 'h5', - component: 'h2', + component: 'h1', }} subheaderTypographyProps={{ 'data-cy': 'subheader', diff --git a/web/src/app/main/components/ToolbarPageTitle.tsx b/web/src/app/main/components/ToolbarPageTitle.tsx index c0d91c8183..23120837df 100644 --- a/web/src/app/main/components/ToolbarPageTitle.tsx +++ b/web/src/app/main/components/ToolbarPageTitle.tsx @@ -52,7 +52,7 @@ const renderCrumb = ( data-cy={`breadcrumb-${index}`} noWrap key={index} - component='h1' + component='h6' sx={{ padding: '0 4px 0 4px', fontSize: '1.25rem', @@ -77,7 +77,7 @@ const renderCrumb = ( '&:hover': { textDecoration: 'none', }, - '&:hover > h1': { + '&:hover > h6': { cursor: 'pointer', backgroundColor: 'rgba(255, 255, 255, 0.2)', borderRadius: '6px', From fac81601375ba14fe29eafcea9bd9b9e5e240458 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Wed, 31 Aug 2022 12:17:50 -0500 Subject: [PATCH 04/15] add login helper --- test/integration/lib/index.ts | 1 + test/integration/lib/login.ts | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 test/integration/lib/index.ts create mode 100644 test/integration/lib/login.ts diff --git a/test/integration/lib/index.ts b/test/integration/lib/index.ts new file mode 100644 index 0000000000..d4389750ae --- /dev/null +++ b/test/integration/lib/index.ts @@ -0,0 +1 @@ +export * from './login' diff --git a/test/integration/lib/login.ts b/test/integration/lib/login.ts new file mode 100644 index 0000000000..fd589f96e8 --- /dev/null +++ b/test/integration/lib/login.ts @@ -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 { + 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 { + // click logout from manage profile + await page.locator('[aria-label="Manage Profile"]').click() + await page.locator('button >> "Logout"').click() +} From 9933892f037c2e6fd8f9f67ca4202e14e6f2a683 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Thu, 1 Sep 2022 10:11:06 -0500 Subject: [PATCH 05/15] configurable engine cycle time --- app/cmd.go | 4 ++++ app/config.go | 2 ++ app/defaults.go | 3 +++ app/initengine.go | 2 ++ engine/config.go | 4 ++++ engine/engine.go | 6 +++++- 6 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/cmd.go b/app/cmd.go index fcf6d51cbc..30b4147182 100644 --- a/app/cmd.go +++ b/app/cmd.go @@ -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"), @@ -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") diff --git a/app/config.go b/app/config.go index 480f8e9b8e..1403ac5fc1 100644 --- a/app/config.go +++ b/app/config.go @@ -48,6 +48,8 @@ type Config struct { KubernetesCooldown time.Duration StatusAddr string + EngineCycleTime time.Duration + EncryptionKeys keyring.Keys RegionName string diff --git a/app/defaults.go b/app/defaults.go index cecff3f89c..76ab3f3c68 100644 --- a/app/defaults.go +++ b/app/defaults.go @@ -1,5 +1,7 @@ package app +import "time" + // Defaults returns the default app config. func Defaults() Config { return Config{ @@ -9,5 +11,6 @@ func Defaults() Config { MaxReqBodyBytes: 256 * 1024, MaxReqHeaderBytes: 4096, RegionName: "default", + EngineCycleTime: 5 * time.Second, } } diff --git a/app/initengine.go b/app/initengine.go index 71ce1a2356..f26d35cfaf 100644 --- a/app/initengine.go +++ b/app/initengine.go @@ -42,6 +42,8 @@ func (app *App) initEngine(ctx context.Context) error { Keys: app.cfg.EncryptionKeys, + CycleTime: app.cfg.EngineCycleTime, + MaxMessages: 50, DisableCycle: app.cfg.APIOnly, diff --git a/engine/config.go b/engine/config.go index 6035d9b640..149bec1d61 100644 --- a/engine/config.go +++ b/engine/config.go @@ -1,6 +1,8 @@ package engine import ( + "time" + "github.com/target/goalert/alert" "github.com/target/goalert/alert/alertlog" "github.com/target/goalert/config" @@ -33,4 +35,6 @@ type Config struct { DisableCycle bool LogCycles bool + + CycleTime time.Duration } diff --git a/engine/engine.go b/engine/engine.go index 27d300f4a3..3d2f4ebd53 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -505,7 +505,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) From 3a4395a2b58725c99954d6b2fe26cc5fd99b6f0a Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Thu, 1 Sep 2022 12:49:09 -0500 Subject: [PATCH 06/15] first test --- .gitignore | 2 + Makefile | 16 ++++- Procfile.integration | 9 +++ playwright.config.js | 40 ++++++++++++ test/integration/email-cm.spec.ts | 74 ++++++++++++++++++++++ test/integration/setup/global-setup.ts | 55 ++++++++++++++++ test/integration/setup/goalert-config.json | 8 +++ web/src/app/util/OtherActionsMobile.js | 3 +- yarn.lock | 8 +++ 9 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 Procfile.integration create mode 100644 playwright.config.js create mode 100644 test/integration/email-cm.spec.ts create mode 100644 test/integration/setup/global-setup.ts create mode 100644 test/integration/setup/goalert-config.json diff --git a/.gitignore b/.gitignore index 5c790d99da..e8c867bf52 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ /setup.cfg /setup.pid /config.json.bak +/*.session.json +*.zip *.pem *.key diff --git a/Makefile b/Makefile index aae5ab1ee8..f430fd4cfc 100644 --- a/Makefile +++ b/Makefile @@ -105,6 +105,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: bin/goalert bin/psql-lite bin/waitfor bin/runproc web/src/build/static/app.js $(BIN_DIR)/tools/prometheus + ./bin/waitfor -timeout 1s "$(DB_URL)" || make postgres + ./bin/psql-lite -d 'postgres://goalert@localhost' -c 'DROP DATABASE IF EXISTS goalert_integration; CREATE DATABASE goalert_integration;' + ./bin/goalert --db-url 'postgres://goalert@localhost/goalert_integration' migrate + ./bin/psql-lite -d 'postgres://goalert@localhost/goalert_integration' -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 'postgres://goalert@localhost/goalert_integration' --user-id=00000000-0000-0000-0000-000000000001 --user admin --pass admin123 + ./bin/goalert add-user --db-url 'postgres://goalert@localhost/goalert_integration' --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 'postgres://goalert@localhost/goalert_integration' + rm -f *.session.json + ./bin/runproc -f Procfile.integration + jest: node_modules yarn workspace goalert-web run jest $(JEST_ARGS) @@ -145,10 +156,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: + yarn playwright test + smoketest: (cd test/smoke && go test -parallel 10 -timeout 20m) diff --git a/Procfile.integration b/Procfile.integration new file mode 100644 index 0000000000..555252b3ed --- /dev/null +++ b/Procfile.integration @@ -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 --db-url=postgres://goalert@localhost/goalert_integration --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 diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000000..4f4bb3e446 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,40 @@ +const { devices } = require('@playwright/test') + +const config = { + testDir: './test/integration', + globalSetup: require.resolve('./test/integration/setup/global-setup.ts'), + retries: 3, + use: { + trace: 'retain-on-failure', + 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', + url: 'http://localhost:6130/health', + reuseExistingServer: true, + }, +} + +module.exports = config diff --git a/test/integration/email-cm.spec.ts b/test/integration/email-cm.spec.ts new file mode 100644 index 0000000000..d42f263f66 --- /dev/null +++ b/test/integration/email-cm.spec.ts @@ -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() +}) diff --git a/test/integration/setup/global-setup.ts b/test/integration/setup/global-setup.ts new file mode 100644 index 0000000000..283e76c4a7 --- /dev/null +++ b/test/integration/setup/global-setup.ts @@ -0,0 +1,55 @@ +import { chromium, FullConfig, expect } from '@playwright/test' +import fs from 'fs' +import { + adminSessionFile, + userSessionFile, + login, + adminUserCreds, + normalUserCreds, + Creds, +} from '../lib' + +async function canRead(file: string): Promise { + try { + await fs.promises.access(file, fs.constants.R_OK) + return true + } catch (err) { + return false + } +} + +export default async function globalSetup(config: FullConfig): Promise { + // return if both files are readable + const [adminReadable, userReadable] = await Promise.all([ + canRead(adminSessionFile), + canRead(userSessionFile), + ]) + if (adminReadable && userReadable) { + return + } + + const browser = await chromium.launch() + + async function createSession(path: string, c: Creds): Promise { + const page = await browser.newPage({ + baseURL: config.projects[0].use.baseURL, + }) + await page.context().tracing.start({ screenshots: true, snapshots: true }) + try { + await page.goto('./profile') + await login(page, c.user, c.pass) + await expect(page.locator('h1')).toContainText(c.name) + await page.context().storageState({ path }) + } finally { + await page.context().tracing.stop({ path: `trace-${c.user}.zip` }) + await page.close() + } + } + + await Promise.all([ + createSession(adminSessionFile, adminUserCreds), + createSession(userSessionFile, normalUserCreds), + ]) + + await browser.close() +} diff --git a/test/integration/setup/goalert-config.json b/test/integration/setup/goalert-config.json new file mode 100644 index 0000000000..cb468fde6a --- /dev/null +++ b/test/integration/setup/goalert-config.json @@ -0,0 +1,8 @@ +{ + "SMTP": { + "Enable": true, + "From": "goalert@localhost", + "Address": "localhost:6105", + "DisableTLS": true + } +} diff --git a/web/src/app/util/OtherActionsMobile.js b/web/src/app/util/OtherActionsMobile.js index d15e7c8788..fc48cb8e50 100644 --- a/web/src/app/util/OtherActionsMobile.js +++ b/web/src/app/util/OtherActionsMobile.js @@ -15,11 +15,12 @@ export default function OtherActionsMobile({ isOpen, onClose, actions }) { onOpen={() => null} onClose={onClose} > - + {actions.map((o, idx) => ( { onClose() o.onClick() diff --git a/yarn.lock b/yarn.lock index e8a35bce15..f1bce2eb88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1172,6 +1172,14 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@playwright/test@1.25.1": + version "1.25.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.25.1.tgz#9847234b6f2b0cca71962b338da7db366a1e9720" + integrity sha512-IJ4X0yOakXtwkhbnNzKkaIgXe6df7u3H3FnuhI9Jqh+CdO0e/lYQlDLYiyI9cnXK8E7UAppAWP+VqAv6VX7HQg== + dependencies: + "@types/node" "*" + playwright-core "1.25.1" + "@popperjs/core@^2.11.6": version "2.11.6" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" From 4d0cbd62a0da64543cc61925ac429596d2a6d290 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Thu, 1 Sep 2022 13:19:21 -0500 Subject: [PATCH 07/15] add tracing --- .gitignore | 1 + Makefile | 23 +++++++++++++++-------- Procfile.integration | 2 +- Procfile.integration.ci | 3 +++ playwright.config.js | 4 ++-- test/integration/setup/global-setup.ts | 11 ++++++++--- 6 files changed, 30 insertions(+), 14 deletions(-) create mode 100644 Procfile.integration.ci diff --git a/.gitignore b/.gitignore index e8c867bf52..912e85b18f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /config.json.bak /*.session.json *.zip +/test-results *.pem *.key diff --git a/Makefile b/Makefile index f430fd4cfc..63db765d2e 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +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 = postgres://goalert@localhost:5432/$(INT_DB) LOG_DIR= GOPATH:=$(shell go env GOPATH) @@ -31,6 +33,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 @@ -107,14 +114,14 @@ start-prod: web/src/build/static/app.js $(BIN_DIR)/tools/prometheus start-integration: bin/goalert bin/psql-lite bin/waitfor bin/runproc web/src/build/static/app.js $(BIN_DIR)/tools/prometheus ./bin/waitfor -timeout 1s "$(DB_URL)" || make postgres - ./bin/psql-lite -d 'postgres://goalert@localhost' -c 'DROP DATABASE IF EXISTS goalert_integration; CREATE DATABASE goalert_integration;' - ./bin/goalert --db-url 'postgres://goalert@localhost/goalert_integration' migrate - ./bin/psql-lite -d 'postgres://goalert@localhost/goalert_integration' -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 'postgres://goalert@localhost/goalert_integration' --user-id=00000000-0000-0000-0000-000000000001 --user admin --pass admin123 - ./bin/goalert add-user --db-url 'postgres://goalert@localhost/goalert_integration' --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 'postgres://goalert@localhost/goalert_integration' + ./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 - ./bin/runproc -f Procfile.integration + GOALERT_DB_URL="$(INT_DB_URL)" ./bin/runproc -f $(INT_PROC) jest: node_modules yarn workspace goalert-web run jest $(JEST_ARGS) diff --git a/Procfile.integration b/Procfile.integration index 555252b3ed..d22f8bf91f 100644 --- a/Procfile.integration +++ b/Procfile.integration @@ -1,7 +1,7 @@ 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 --db-url=postgres://goalert@localhost/goalert_integration --public-url=http://localhost:6130 --engine-cycle-time=50ms +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 diff --git a/Procfile.integration.ci b/Procfile.integration.ci new file mode 100644 index 0000000000..4a40a58458 --- /dev/null +++ b/Procfile.integration.ci @@ -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 diff --git a/playwright.config.js b/playwright.config.js index 4f4bb3e446..fa4c45fb81 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -5,7 +5,7 @@ const config = { globalSetup: require.resolve('./test/integration/setup/global-setup.ts'), retries: 3, use: { - trace: 'retain-on-failure', + trace: 'on-first-retry', baseURL: 'http://localhost:6130', viewport: { width: 1440, height: 900 }, timezoneId: 'America/Chicago', @@ -31,7 +31,7 @@ const config = { }, ], webServer: { - command: 'make start-integration', + command: 'make start-integration CI=1', url: 'http://localhost:6130/health', reuseExistingServer: true, }, diff --git a/test/integration/setup/global-setup.ts b/test/integration/setup/global-setup.ts index 283e76c4a7..40a38d2adb 100644 --- a/test/integration/setup/global-setup.ts +++ b/test/integration/setup/global-setup.ts @@ -34,15 +34,20 @@ export default async function globalSetup(config: FullConfig): Promise { const page = await browser.newPage({ baseURL: config.projects[0].use.baseURL, }) - await page.context().tracing.start({ screenshots: true, snapshots: true }) try { + await page.context().tracing.start({ screenshots: true, snapshots: true }) await page.goto('./profile') await login(page, c.user, c.pass) await expect(page.locator('h1')).toContainText(c.name) await page.context().storageState({ path }) - } finally { - await page.context().tracing.stop({ path: `trace-${c.user}.zip` }) + await page.context().tracing.stop() await page.close() + } catch (error) { + await page.context().tracing.stop({ + path: `test-results/failed-setup-${c.user}-trace.zip`, + }) + await page.close() + throw error } } From fb85a310e00c28d6d901857eb0d20a12c75f4146 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Thu, 1 Sep 2022 15:14:57 -0500 Subject: [PATCH 08/15] inherit db_url in makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 63db765d2e..34ce562585 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ include Makefile.binaries.mk CFGPARAMS = devtools/configparams/*.go DB_URL = postgres://goalert@localhost:5432/goalert INT_DB = goalert_integration -INT_DB_URL = postgres://goalert@localhost:5432/$(INT_DB) +INT_DB_URL = $(shell dirname "$(DB_URL)")/$(INT_DB) LOG_DIR= GOPATH:=$(shell go env GOPATH) From ef973b6712018345ae19303a1c9d865203913281 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Thu, 1 Sep 2022 15:29:27 -0500 Subject: [PATCH 09/15] use import --- playwright.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright.config.js b/playwright.config.js index fa4c45fb81..cbbedbd7b6 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,4 +1,4 @@ -const { devices } = require('@playwright/test') +import { devices } from '@playwright/test' const config = { testDir: './test/integration', From 1564e83f49524505f5bedcf183f83ffd2c7a4ab9 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Mon, 12 Sep 2022 16:27:12 -0500 Subject: [PATCH 10/15] add playwright dep --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 34ce562585..5150f53fb2 100644 --- a/Makefile +++ b/Makefile @@ -167,7 +167,7 @@ test-integration: playwright-run cy-wide-prod-run cy-mobile-prod-run test-smoke: smoketest test-unit: test -playwright-run: +playwright-run: node_modules yarn playwright test smoketest: From 330870b4807910dea1c8d2b3f2034cb26d57272a Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Mon, 12 Sep 2022 16:27:28 -0500 Subject: [PATCH 11/15] update component --- web/src/app/dialogs/components/DialogTitleWrapper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/app/dialogs/components/DialogTitleWrapper.js b/web/src/app/dialogs/components/DialogTitleWrapper.js index 74f1d46cea..3d870dbab6 100644 --- a/web/src/app/dialogs/components/DialogTitleWrapper.js +++ b/web/src/app/dialogs/components/DialogTitleWrapper.js @@ -88,7 +88,7 @@ function DialogTitleWrapper(props) { data-cy='dialog-title' color='inherit' variant='h6' - component='h1' + component='h6' > {title} From 56fbea03f20fa282a9c925abd1f5c62258f7374d Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Mon, 12 Sep 2022 16:37:45 -0500 Subject: [PATCH 12/15] update make target deps for playwright --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 5150f53fb2..078447e36b 100644 --- a/Makefile +++ b/Makefile @@ -112,7 +112,7 @@ 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: bin/goalert bin/psql-lite bin/waitfor bin/runproc web/src/build/static/app.js $(BIN_DIR)/tools/prometheus +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 @@ -167,7 +167,7 @@ test-integration: playwright-run cy-wide-prod-run cy-mobile-prod-run test-smoke: smoketest test-unit: test -playwright-run: node_modules +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: From b192cdf2ad7c4f17352d0d418c51369bd4a72973 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Tue, 13 Sep 2022 14:12:39 -0500 Subject: [PATCH 13/15] handle shortand and parameter db urls --- Makefile | 3 +-- devtools/scripts/db-url/main.go | 36 ++++++++++++++++++++++++++++ devtools/scripts/db-url/main_test.go | 18 ++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 devtools/scripts/db-url/main.go create mode 100644 devtools/scripts/db-url/main_test.go diff --git a/Makefile b/Makefile index 078447e36b..04e41409fa 100644 --- a/Makefile +++ b/Makefile @@ -10,8 +10,7 @@ include Makefile.binaries.mk CFGPARAMS = devtools/configparams/*.go DB_URL = postgres://goalert@localhost:5432/goalert INT_DB = goalert_integration -INT_DB_URL = $(shell dirname "$(DB_URL)")/$(INT_DB) - +INT_DB_URL = $(shell go run ./devtools/scripts/db-url "$(DB_URL)" "$(INT_DB)") LOG_DIR= GOPATH:=$(shell go env GOPATH) diff --git a/devtools/scripts/db-url/main.go b/devtools/scripts/db-url/main.go new file mode 100644 index 0000000000..9a0b1796cf --- /dev/null +++ b/devtools/scripts/db-url/main.go @@ -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) +} diff --git a/devtools/scripts/db-url/main_test.go b/devtools/scripts/db-url/main_test.go new file mode 100644 index 0000000000..a60f7fe375 --- /dev/null +++ b/devtools/scripts/db-url/main_test.go @@ -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") +} From 87436822e2c8b9ed9d4f7c27a2d1105bd86e59f1 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Tue, 13 Sep 2022 14:39:01 -0500 Subject: [PATCH 14/15] simplify menu handling in cypress support --- web/src/cypress/support/menu.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/web/src/cypress/support/menu.ts b/web/src/cypress/support/menu.ts index 15b433e2ce..23b997ffa0 100644 --- a/web/src/cypress/support/menu.ts +++ b/web/src/cypress/support/menu.ts @@ -28,13 +28,8 @@ function menu( cy.wrap(sub).click() // click menu item - if ((options && options.forceWidescreen) || format === 'wide') { - cy.get('ul[role=menu]').contains('li', s).click() - cy.get('ul[role=menu]').should('not.exist') - } else { - cy.get('ul[data-cy=mobile-actions]').contains('*[role=button]', s).click() - cy.get('ul[data-cy=mobile-actions]').should('not.exist') - } + cy.get('ul[role=menu]').contains('[role=menuitem]', s).click() + cy.get('ul[role=menu]').should('not.exist') }) } From 0452317501ace4e94426dd88754a29b4ba472f7f Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Tue, 13 Sep 2022 14:53:49 -0500 Subject: [PATCH 15/15] remvoe unused arg --- web/src/cypress/support/menu.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/web/src/cypress/support/menu.ts b/web/src/cypress/support/menu.ts index 23b997ffa0..d80c32ba0b 100644 --- a/web/src/cypress/support/menu.ts +++ b/web/src/cypress/support/menu.ts @@ -15,11 +15,7 @@ declare global { } } -function menu( - sub: JQuery, - s: string, - options?: MenuSelectOptions, -): Cypress.Chainable { +function menu(sub: JQuery, s: string): Cypress.Chainable { return cy.get('[data-cy=app-bar]').then((el) => { const format: 'mobile' | 'wide' = el.data('cy-format') expect(format, 'header format').to.be.oneOf(['mobile', 'wide'])