@toolstackhq/create-qa-patterns 1.0.14 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/index.js +252 -1076
- package/lib/args.js +139 -0
- package/lib/constants.js +115 -0
- package/lib/interactive.js +131 -0
- package/lib/local-env.js +65 -0
- package/lib/metadata.js +329 -0
- package/lib/output.js +326 -0
- package/lib/prereqs.js +72 -0
- package/lib/scaffold.js +120 -0
- package/lib/templates.js +40 -0
- package/package.json +5 -3
- package/templates/cypress-template/.env.example +2 -2
- package/templates/cypress-template/.github/workflows/cypress-tests.yml +2 -2
- package/templates/cypress-template/README.md +10 -6
- package/templates/cypress-template/allurerc.mjs +1 -1
- package/templates/cypress-template/config/environments.ts +13 -11
- package/templates/cypress-template/config/runtime-config.ts +17 -12
- package/templates/cypress-template/config/secret-manager.ts +1 -1
- package/templates/cypress-template/config/test-env.ts +3 -3
- package/templates/cypress-template/cypress/e2e/ui-journey.cy.ts +12 -10
- package/templates/cypress-template/cypress/support/app-config.ts +5 -5
- package/templates/cypress-template/cypress/support/commands.ts +7 -7
- package/templates/cypress-template/cypress/support/data/data-factory.ts +6 -4
- package/templates/cypress-template/cypress/support/data/id-generator.ts +1 -1
- package/templates/cypress-template/cypress/support/data/seeded-faker.ts +2 -2
- package/templates/cypress-template/cypress/support/e2e.ts +2 -2
- package/templates/cypress-template/cypress/support/pages/login-page.ts +4 -4
- package/templates/cypress-template/cypress/support/pages/people-page.ts +10 -10
- package/templates/cypress-template/cypress.config.ts +9 -9
- package/templates/cypress-template/demo-apps/ui-demo-app/public/styles.css +1 -1
- package/templates/cypress-template/demo-apps/ui-demo-app/src/server.js +44 -41
- package/templates/cypress-template/demo-apps/ui-demo-app/src/store.js +31 -3
- package/templates/cypress-template/demo-apps/ui-demo-app/src/templates.js +5 -5
- package/templates/cypress-template/eslint.config.mjs +53 -45
- package/templates/cypress-template/package.json +6 -5
- package/templates/cypress-template/scripts/ensure-local-env.mjs +36 -0
- package/templates/cypress-template/scripts/generate-allure-report.mjs +16 -10
- package/templates/cypress-template/scripts/run-cypress.mjs +33 -24
- package/templates/cypress-template/scripts/run-tests.sh +1 -0
- package/templates/cypress-template/tsconfig.json +7 -1
- package/templates/playwright-template/.env.example +6 -6
- package/templates/playwright-template/.github/workflows/playwright-tests.yml +14 -5
- package/templates/playwright-template/README.md +6 -5
- package/templates/playwright-template/allurerc.mjs +1 -1
- package/templates/playwright-template/components/flash-message.ts +2 -2
- package/templates/playwright-template/config/environments.ts +16 -14
- package/templates/playwright-template/config/runtime-config.ts +17 -12
- package/templates/playwright-template/config/secret-manager.ts +1 -1
- package/templates/playwright-template/config/test-env.ts +3 -3
- package/templates/playwright-template/data/factories/data-factory.ts +6 -4
- package/templates/playwright-template/data/generators/id-generator.ts +1 -1
- package/templates/playwright-template/data/generators/seeded-faker.ts +2 -2
- package/templates/playwright-template/demo-apps/api-demo-server/src/server.js +9 -9
- package/templates/playwright-template/demo-apps/api-demo-server/src/store.js +1 -1
- package/templates/playwright-template/demo-apps/ui-demo-app/public/styles.css +1 -1
- package/templates/playwright-template/demo-apps/ui-demo-app/src/server.js +44 -41
- package/templates/playwright-template/demo-apps/ui-demo-app/src/store.js +31 -3
- package/templates/playwright-template/demo-apps/ui-demo-app/src/templates.js +5 -5
- package/templates/playwright-template/eslint.config.mjs +40 -40
- package/templates/playwright-template/fixtures/test-fixtures.ts +27 -12
- package/templates/playwright-template/lint/architecture-plugin.cjs +36 -31
- package/templates/playwright-template/package.json +7 -6
- package/templates/playwright-template/pages/base-page.ts +4 -4
- package/templates/playwright-template/pages/login-page.ts +9 -9
- package/templates/playwright-template/pages/people-page.ts +21 -17
- package/templates/playwright-template/playwright.config.ts +22 -19
- package/templates/playwright-template/reporters/structured-reporter.ts +11 -8
- package/templates/playwright-template/scripts/ensure-local-env.mjs +37 -0
- package/templates/playwright-template/scripts/generate-allure-report.mjs +16 -10
- package/templates/playwright-template/scripts/run-tests.sh +1 -0
- package/templates/playwright-template/tests/api-people.spec.ts +8 -6
- package/templates/playwright-template/tests/ui-journey.spec.ts +13 -8
- package/templates/playwright-template/tsconfig.json +3 -11
- package/templates/playwright-template/utils/logger.ts +12 -8
- package/templates/playwright-template/utils/test-step.ts +5 -5
- package/templates/wdio-template/.env.example +14 -0
- package/templates/wdio-template/.github/workflows/wdio-tests.yml +46 -0
- package/templates/wdio-template/README.md +241 -0
- package/templates/wdio-template/allurerc.mjs +10 -0
- package/templates/wdio-template/components/README.md +5 -0
- package/templates/wdio-template/components/flash-message.ts +16 -0
- package/templates/wdio-template/config/README.md +5 -0
- package/templates/wdio-template/config/environments.ts +40 -0
- package/templates/wdio-template/config/runtime-config.ts +53 -0
- package/templates/wdio-template/config/secret-manager.ts +29 -0
- package/templates/wdio-template/config/test-env.ts +9 -0
- package/templates/wdio-template/data/README.md +9 -0
- package/templates/wdio-template/data/factories/README.md +6 -0
- package/templates/wdio-template/data/factories/data-factory.ts +36 -0
- package/templates/wdio-template/data/generators/README.md +5 -0
- package/templates/wdio-template/data/generators/id-generator.ts +18 -0
- package/templates/wdio-template/data/generators/seeded-faker.ts +14 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/public/styles.css +120 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/src/server.js +152 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/src/store.js +71 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/src/templates.js +121 -0
- package/templates/wdio-template/eslint.config.mjs +86 -0
- package/templates/wdio-template/lint/architecture-plugin.cjs +123 -0
- package/templates/wdio-template/package-lock.json +11058 -0
- package/templates/wdio-template/package.json +44 -0
- package/templates/wdio-template/pages/README.md +6 -0
- package/templates/wdio-template/pages/base-page.ts +15 -0
- package/templates/wdio-template/pages/login-page.ts +27 -0
- package/templates/wdio-template/pages/people-page.ts +54 -0
- package/templates/wdio-template/reporters/README.md +5 -0
- package/templates/wdio-template/reporters/structured-reporter.ts +78 -0
- package/templates/wdio-template/scripts/README.md +5 -0
- package/templates/wdio-template/scripts/ensure-local-env.mjs +36 -0
- package/templates/wdio-template/scripts/generate-allure-report.mjs +72 -0
- package/templates/wdio-template/scripts/run-tests.sh +7 -0
- package/templates/wdio-template/scripts/run-wdio.mjs +114 -0
- package/templates/wdio-template/tests/README.md +7 -0
- package/templates/wdio-template/tests/ui-journey.spec.ts +52 -0
- package/templates/wdio-template/tsconfig.json +22 -0
- package/templates/wdio-template/utils/README.md +5 -0
- package/templates/wdio-template/utils/logger.ts +60 -0
- package/templates/wdio-template/utils/test-step.ts +20 -0
- package/templates/wdio-template/wdio.conf.ts +58 -0
- package/tests/args.test.js +58 -0
- package/tests/local-env.test.js +70 -0
- package/tests/metadata.test.js +147 -0
- package/tests/templates.test.js +44 -0
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// Starts the local demo app when needed, then launches Cypress in run or open mode.
|
|
4
|
-
import process from
|
|
5
|
-
import path from
|
|
6
|
-
import { spawn } from
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
7
|
|
|
8
|
-
import dotenv from
|
|
8
|
+
import dotenv from 'dotenv';
|
|
9
9
|
|
|
10
|
-
const mode = process.argv[2] ??
|
|
10
|
+
const mode = process.argv[2] ?? 'run';
|
|
11
11
|
const args = process.argv.slice(3);
|
|
12
12
|
const cwd = process.cwd();
|
|
13
|
-
const healthUrl =
|
|
14
|
-
const environment = process.env.TEST_ENV ??
|
|
13
|
+
const healthUrl = 'http://127.0.0.1:3000/health';
|
|
14
|
+
const environment = process.env.TEST_ENV ?? 'dev';
|
|
15
15
|
const environmentDefaults = {
|
|
16
|
-
dev:
|
|
17
|
-
staging:
|
|
18
|
-
prod:
|
|
16
|
+
dev: 'http://127.0.0.1:3000',
|
|
17
|
+
staging: 'https://staging-ui.example.internal',
|
|
18
|
+
prod: 'https://ui.example.internal'
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
dotenv.config({ path: path.resolve(cwd,
|
|
22
|
-
dotenv.config({
|
|
21
|
+
dotenv.config({ path: path.resolve(cwd, '.env') });
|
|
22
|
+
dotenv.config({
|
|
23
|
+
path: path.resolve(cwd, `.env.${environment}`),
|
|
24
|
+
override: true
|
|
25
|
+
});
|
|
23
26
|
|
|
24
27
|
const uiBaseUrl =
|
|
25
28
|
process.env[`${environment.toUpperCase()}_UI_BASE_URL`] ??
|
|
@@ -28,18 +31,18 @@ const uiBaseUrl =
|
|
|
28
31
|
environmentDefaults.dev;
|
|
29
32
|
|
|
30
33
|
const shouldAutoStartDemoApp =
|
|
31
|
-
environment ===
|
|
34
|
+
environment === 'dev' &&
|
|
32
35
|
uiBaseUrl === environmentDefaults.dev &&
|
|
33
|
-
process.env.CY_DISABLE_LOCAL_DEMO_APP !==
|
|
36
|
+
process.env.CY_DISABLE_LOCAL_DEMO_APP !== 'true';
|
|
34
37
|
|
|
35
38
|
function getCommandName(command) {
|
|
36
|
-
return process.platform ===
|
|
39
|
+
return process.platform === 'win32' ? `${command}.cmd` : command;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
function spawnCommand(command, commandArgs, options = {}) {
|
|
40
43
|
return spawn(getCommandName(command), commandArgs, {
|
|
41
44
|
cwd,
|
|
42
|
-
stdio:
|
|
45
|
+
stdio: 'inherit',
|
|
43
46
|
...options
|
|
44
47
|
});
|
|
45
48
|
}
|
|
@@ -68,7 +71,7 @@ function killChild(child) {
|
|
|
68
71
|
return;
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
child.kill(
|
|
74
|
+
child.kill('SIGTERM');
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
async function run() {
|
|
@@ -76,16 +79,20 @@ async function run() {
|
|
|
76
79
|
|
|
77
80
|
try {
|
|
78
81
|
if (shouldAutoStartDemoApp) {
|
|
79
|
-
demoAppProcess = spawnCommand(
|
|
82
|
+
demoAppProcess = spawnCommand('npm', ['run', 'demo:ui']);
|
|
80
83
|
await waitForHealthcheck(healthUrl);
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
const cypressCommand = mode ===
|
|
84
|
-
const cypressProcess = spawnCommand(
|
|
86
|
+
const cypressCommand = mode === 'open' ? 'open' : 'run';
|
|
87
|
+
const cypressProcess = spawnCommand('npx', [
|
|
88
|
+
'cypress',
|
|
89
|
+
cypressCommand,
|
|
90
|
+
...args
|
|
91
|
+
]);
|
|
85
92
|
|
|
86
93
|
const exitCode = await new Promise((resolve) => {
|
|
87
|
-
cypressProcess.on(
|
|
88
|
-
cypressProcess.on(
|
|
94
|
+
cypressProcess.on('close', resolve);
|
|
95
|
+
cypressProcess.on('error', () => resolve(1));
|
|
89
96
|
});
|
|
90
97
|
|
|
91
98
|
if (exitCode !== 0) {
|
|
@@ -96,11 +103,13 @@ async function run() {
|
|
|
96
103
|
}
|
|
97
104
|
}
|
|
98
105
|
|
|
99
|
-
for (const signal of [
|
|
106
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
100
107
|
process.on(signal, () => process.exit(1));
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
run().catch((error) => {
|
|
104
|
-
process.stderr.write(
|
|
111
|
+
process.stderr.write(
|
|
112
|
+
`${error instanceof Error ? error.message : String(error)}\n`
|
|
113
|
+
);
|
|
105
114
|
process.exit(1);
|
|
106
115
|
});
|
|
@@ -11,5 +11,11 @@
|
|
|
11
11
|
"noEmit": true,
|
|
12
12
|
"types": ["cypress", "node"]
|
|
13
13
|
},
|
|
14
|
-
"include": [
|
|
14
|
+
"include": [
|
|
15
|
+
"cypress/**/*.ts",
|
|
16
|
+
"cypress/**/*.d.ts",
|
|
17
|
+
"config/**/*.ts",
|
|
18
|
+
"cypress.config.ts",
|
|
19
|
+
"allurerc.mjs"
|
|
20
|
+
]
|
|
15
21
|
}
|
|
@@ -3,15 +3,15 @@ TEST_RUN_ID=local
|
|
|
3
3
|
|
|
4
4
|
DEV_UI_BASE_URL=http://127.0.0.1:3000
|
|
5
5
|
DEV_API_BASE_URL=http://127.0.0.1:3001
|
|
6
|
-
DEV_APP_USERNAME=
|
|
7
|
-
DEV_APP_PASSWORD=
|
|
6
|
+
DEV_APP_USERNAME=generate-a-local-username
|
|
7
|
+
DEV_APP_PASSWORD=generate-a-local-password
|
|
8
8
|
|
|
9
9
|
STAGING_UI_BASE_URL=https://staging-ui.example.internal
|
|
10
10
|
STAGING_API_BASE_URL=https://staging-api.example.internal
|
|
11
|
-
STAGING_APP_USERNAME=staging-
|
|
12
|
-
STAGING_APP_PASSWORD=
|
|
11
|
+
STAGING_APP_USERNAME=your-staging-username
|
|
12
|
+
STAGING_APP_PASSWORD=your-staging-password
|
|
13
13
|
|
|
14
14
|
PROD_UI_BASE_URL=https://ui.example.internal
|
|
15
15
|
PROD_API_BASE_URL=https://api.example.internal
|
|
16
|
-
PROD_APP_USERNAME=prod-
|
|
17
|
-
PROD_APP_PASSWORD=
|
|
16
|
+
PROD_APP_USERNAME=your-prod-username
|
|
17
|
+
PROD_APP_PASSWORD=your-prod-password
|
|
@@ -4,9 +4,9 @@ on:
|
|
|
4
4
|
workflow_dispatch:
|
|
5
5
|
push:
|
|
6
6
|
paths:
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
7
|
+
- 'templates/playwright-template/**'
|
|
8
|
+
- 'test-apps/**'
|
|
9
|
+
- 'package.json'
|
|
10
10
|
|
|
11
11
|
jobs:
|
|
12
12
|
playwright:
|
|
@@ -17,7 +17,7 @@ jobs:
|
|
|
17
17
|
|
|
18
18
|
- uses: actions/setup-node@v4
|
|
19
19
|
with:
|
|
20
|
-
node-version:
|
|
20
|
+
node-version: '20'
|
|
21
21
|
|
|
22
22
|
- name: Install workspace dependencies
|
|
23
23
|
run: npm ci
|
|
@@ -50,13 +50,18 @@ jobs:
|
|
|
50
50
|
|
|
51
51
|
playwright-docker:
|
|
52
52
|
runs-on: ubuntu-latest
|
|
53
|
+
env:
|
|
54
|
+
DEV_APP_USERNAME: ci-docker-user
|
|
55
|
+
DEV_APP_PASSWORD: ci-docker-password-A1!
|
|
56
|
+
UI_DEMO_USERNAME: ci-docker-user
|
|
57
|
+
UI_DEMO_PASSWORD: ci-docker-password-A1!
|
|
53
58
|
|
|
54
59
|
steps:
|
|
55
60
|
- uses: actions/checkout@v4
|
|
56
61
|
|
|
57
62
|
- uses: actions/setup-node@v4
|
|
58
63
|
with:
|
|
59
|
-
node-version:
|
|
64
|
+
node-version: '20'
|
|
60
65
|
|
|
61
66
|
- name: Install workspace dependencies
|
|
62
67
|
run: npm ci
|
|
@@ -91,6 +96,10 @@ jobs:
|
|
|
91
96
|
-e TEST_RUN_ID=ci-docker \
|
|
92
97
|
-e DEV_UI_BASE_URL=http://host.docker.internal:3000 \
|
|
93
98
|
-e DEV_API_BASE_URL=http://host.docker.internal:3001 \
|
|
99
|
+
-e DEV_APP_USERNAME=${DEV_APP_USERNAME} \
|
|
100
|
+
-e DEV_APP_PASSWORD=${DEV_APP_PASSWORD} \
|
|
101
|
+
-e UI_DEMO_USERNAME=${UI_DEMO_USERNAME} \
|
|
102
|
+
-e UI_DEMO_PASSWORD=${UI_DEMO_PASSWORD} \
|
|
94
103
|
-v "${GITHUB_WORKSPACE}/templates/playwright-template/reports:/workspace/reports" \
|
|
95
104
|
-v "${GITHUB_WORKSPACE}/templates/playwright-template/allure-results:/workspace/allure-results" \
|
|
96
105
|
-v "${GITHUB_WORKSPACE}/templates/playwright-template/test-results:/workspace/test-results" \
|
|
@@ -89,8 +89,7 @@ Default local values:
|
|
|
89
89
|
|
|
90
90
|
- UI base URL: `http://127.0.0.1:3000`
|
|
91
91
|
- API base URL: `http://127.0.0.1:3001`
|
|
92
|
-
-
|
|
93
|
-
- password: `Password123!`
|
|
92
|
+
- credentials: generated into local `.env` on first run
|
|
94
93
|
|
|
95
94
|
## Environment and secrets
|
|
96
95
|
|
|
@@ -110,7 +109,7 @@ The same pattern is used for credentials:
|
|
|
110
109
|
|
|
111
110
|
1. `DEV_APP_USERNAME` or `DEV_APP_PASSWORD`
|
|
112
111
|
2. `APP_USERNAME` or `APP_PASSWORD`
|
|
113
|
-
3. built-in defaults for the selected environment
|
|
112
|
+
3. built-in empty defaults for the selected environment
|
|
114
113
|
|
|
115
114
|
For local overrides, copy:
|
|
116
115
|
|
|
@@ -129,6 +128,8 @@ The template loads:
|
|
|
129
128
|
- `.env`
|
|
130
129
|
- `.env.<TEST_ENV>`
|
|
131
130
|
|
|
131
|
+
On the first local run, the template also creates a `.env` file with random demo credentials if one does not already exist.
|
|
132
|
+
|
|
132
133
|
Example:
|
|
133
134
|
|
|
134
135
|
```bash
|
|
@@ -192,7 +193,7 @@ If you only want Playwright reporting, remove the `allure-playwright` reporter e
|
|
|
192
193
|
Create tests under `tests/` and import the shared fixtures:
|
|
193
194
|
|
|
194
195
|
```ts
|
|
195
|
-
import { expect, test } from
|
|
196
|
+
import { expect, test } from '../fixtures/test-fixtures';
|
|
196
197
|
```
|
|
197
198
|
|
|
198
199
|
Keep the pattern simple:
|
|
@@ -204,7 +205,7 @@ Keep the pattern simple:
|
|
|
204
205
|
Example shape:
|
|
205
206
|
|
|
206
207
|
```ts
|
|
207
|
-
test(
|
|
208
|
+
test('do something @smoke', async ({ dataFactory, loginPage }) => {
|
|
208
209
|
const person = dataFactory.person();
|
|
209
210
|
// use page objects here
|
|
210
211
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { Locator, Page } from
|
|
1
|
+
import type { Locator, Page } from '@playwright/test';
|
|
2
2
|
|
|
3
3
|
export class FlashMessage {
|
|
4
4
|
private readonly message: Locator;
|
|
5
5
|
|
|
6
6
|
constructor(page: Page) {
|
|
7
|
-
this.message = page.getByTestId(
|
|
7
|
+
this.message = page.getByTestId('flash-message');
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
async getText(): Promise<string | null> {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Defines the built-in environment defaults used when env vars are not provided.
|
|
2
|
-
import type { TestEnvironment } from
|
|
2
|
+
import type { TestEnvironment } from './test-env';
|
|
3
3
|
|
|
4
4
|
type EnvironmentDefaults = {
|
|
5
5
|
uiBaseUrl: string;
|
|
@@ -12,31 +12,33 @@ type EnvironmentDefaults = {
|
|
|
12
12
|
|
|
13
13
|
const DEFAULTS: Record<TestEnvironment, EnvironmentDefaults> = {
|
|
14
14
|
dev: {
|
|
15
|
-
uiBaseUrl:
|
|
16
|
-
apiBaseUrl:
|
|
15
|
+
uiBaseUrl: 'http://127.0.0.1:3000',
|
|
16
|
+
apiBaseUrl: 'http://127.0.0.1:3001',
|
|
17
17
|
credentials: {
|
|
18
|
-
username:
|
|
19
|
-
password:
|
|
18
|
+
username: '',
|
|
19
|
+
password: ''
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
staging: {
|
|
23
|
-
uiBaseUrl:
|
|
24
|
-
apiBaseUrl:
|
|
23
|
+
uiBaseUrl: 'https://staging-ui.example.internal',
|
|
24
|
+
apiBaseUrl: 'https://staging-api.example.internal',
|
|
25
25
|
credentials: {
|
|
26
|
-
username:
|
|
27
|
-
password:
|
|
26
|
+
username: '',
|
|
27
|
+
password: ''
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
30
|
prod: {
|
|
31
|
-
uiBaseUrl:
|
|
32
|
-
apiBaseUrl:
|
|
31
|
+
uiBaseUrl: 'https://ui.example.internal',
|
|
32
|
+
apiBaseUrl: 'https://api.example.internal',
|
|
33
33
|
credentials: {
|
|
34
|
-
username:
|
|
35
|
-
password:
|
|
34
|
+
username: '',
|
|
35
|
+
password: ''
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
export function getEnvironmentDefaults(
|
|
40
|
+
export function getEnvironmentDefaults(
|
|
41
|
+
testEnv: TestEnvironment
|
|
42
|
+
): EnvironmentDefaults {
|
|
41
43
|
return DEFAULTS[testEnv];
|
|
42
44
|
}
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
// Builds the runtime configuration object that tests and fixtures consume.
|
|
2
|
-
import path from
|
|
2
|
+
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
import dotenv from
|
|
5
|
-
import { z } from
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import { z } from 'zod';
|
|
6
6
|
|
|
7
|
-
import { getEnvironmentDefaults } from
|
|
8
|
-
import { EnvSecretProvider, SecretManager } from
|
|
9
|
-
import { loadTestEnvironment } from
|
|
7
|
+
import { getEnvironmentDefaults } from './environments';
|
|
8
|
+
import { EnvSecretProvider, SecretManager } from './secret-manager';
|
|
9
|
+
import { loadTestEnvironment } from './test-env';
|
|
10
10
|
|
|
11
11
|
const environment = loadTestEnvironment();
|
|
12
12
|
|
|
13
|
-
dotenv.config({ path: path.resolve(process.cwd(),
|
|
14
|
-
dotenv.config({
|
|
13
|
+
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
|
|
14
|
+
dotenv.config({
|
|
15
|
+
path: path.resolve(process.cwd(), `.env.${environment}`),
|
|
16
|
+
override: true
|
|
17
|
+
});
|
|
15
18
|
|
|
16
19
|
const runtimeConfigSchema = z.object({
|
|
17
|
-
testEnv: z.enum([
|
|
20
|
+
testEnv: z.enum(['dev', 'staging', 'prod']),
|
|
18
21
|
testRunId: z.string().min(1),
|
|
19
22
|
uiBaseUrl: z.string().url(),
|
|
20
23
|
apiBaseUrl: z.string().url(),
|
|
@@ -41,14 +44,16 @@ export function loadRuntimeConfig(): RuntimeConfig {
|
|
|
41
44
|
|
|
42
45
|
return runtimeConfigSchema.parse({
|
|
43
46
|
testEnv: environment,
|
|
44
|
-
testRunId: process.env.TEST_RUN_ID ??
|
|
47
|
+
testRunId: process.env.TEST_RUN_ID ?? 'local',
|
|
45
48
|
uiBaseUrl,
|
|
46
49
|
apiBaseUrl,
|
|
47
50
|
credentials: {
|
|
48
51
|
username:
|
|
49
|
-
secretManager.getOptionalSecret(
|
|
52
|
+
secretManager.getOptionalSecret('APP_USERNAME', environment) ??
|
|
53
|
+
defaults.credentials.username,
|
|
50
54
|
password:
|
|
51
|
-
secretManager.getOptionalSecret(
|
|
55
|
+
secretManager.getOptionalSecret('APP_PASSWORD', environment) ??
|
|
56
|
+
defaults.credentials.password
|
|
52
57
|
}
|
|
53
58
|
});
|
|
54
59
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Minimal secret abstraction so env-based secrets can later be replaced cleanly.
|
|
2
|
-
import type { TestEnvironment } from
|
|
2
|
+
import type { TestEnvironment } from './test-env';
|
|
3
3
|
|
|
4
4
|
export interface SecretProvider {
|
|
5
5
|
getSecret(key: string, testEnv: TestEnvironment): string | undefined;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { z } from
|
|
1
|
+
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
export const testEnvironmentSchema = z.enum([
|
|
3
|
+
export const testEnvironmentSchema = z.enum(['dev', 'staging', 'prod']);
|
|
4
4
|
|
|
5
5
|
export type TestEnvironment = z.infer<typeof testEnvironmentSchema>;
|
|
6
6
|
|
|
7
7
|
export function loadTestEnvironment(): TestEnvironment {
|
|
8
|
-
return testEnvironmentSchema.parse(process.env.TEST_ENV ??
|
|
8
|
+
return testEnvironmentSchema.parse(process.env.TEST_ENV ?? 'dev');
|
|
9
9
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generic data builders that keep tests readable and deterministic.
|
|
2
|
-
import { createSeededFaker } from
|
|
3
|
-
import { IdGenerator } from
|
|
2
|
+
import { createSeededFaker } from '../generators/seeded-faker';
|
|
3
|
+
import { IdGenerator } from '../generators/id-generator';
|
|
4
4
|
|
|
5
5
|
export type PersonRecord = {
|
|
6
6
|
personId: string;
|
|
@@ -17,7 +17,7 @@ export class DataFactory {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
person(overrides?: Partial<PersonRecord>): PersonRecord {
|
|
20
|
-
const personId = overrides?.personId ?? this.idGenerator.next(
|
|
20
|
+
const personId = overrides?.personId ?? this.idGenerator.next('person');
|
|
21
21
|
const seededFaker = createSeededFaker(`${this.testRunId}:${personId}`);
|
|
22
22
|
const firstName = seededFaker.person.firstName();
|
|
23
23
|
const lastName = seededFaker.person.lastName();
|
|
@@ -27,7 +27,9 @@ export class DataFactory {
|
|
|
27
27
|
personId,
|
|
28
28
|
name,
|
|
29
29
|
role: overrides?.role ?? seededFaker.person.jobTitle(),
|
|
30
|
-
email:
|
|
30
|
+
email:
|
|
31
|
+
overrides?.email ??
|
|
32
|
+
`${firstName}.${lastName}.${personId}@example.test`.toLowerCase(),
|
|
31
33
|
...overrides
|
|
32
34
|
};
|
|
33
35
|
}
|
|
@@ -7,7 +7,7 @@ export class IdGenerator {
|
|
|
7
7
|
next(prefix: string): string {
|
|
8
8
|
const counter = (this.counters.get(prefix) ?? 0) + 1;
|
|
9
9
|
this.counters.set(prefix, counter);
|
|
10
|
-
return `${prefix}-${this.runId}-${String(counter).padStart(4,
|
|
10
|
+
return `${prefix}-${this.runId}-${String(counter).padStart(4, '0')}`;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
nextSequence(prefix: string): number {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Seeded faker wrapper so generated values stay stable for a given run id.
|
|
2
|
-
import { Faker, en } from
|
|
2
|
+
import { Faker, en } from '@faker-js/faker';
|
|
3
3
|
|
|
4
4
|
function hashSeed(value: string): number {
|
|
5
|
-
return value.split(
|
|
5
|
+
return value.split('').reduce((seed, character) => {
|
|
6
6
|
return ((seed << 5) - seed + character.charCodeAt(0)) | 0;
|
|
7
7
|
}, 0);
|
|
8
8
|
}
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
const express = require(
|
|
1
|
+
const express = require('express');
|
|
2
2
|
|
|
3
|
-
const { createPerson, state } = require(
|
|
3
|
+
const { createPerson, state } = require('./store');
|
|
4
4
|
|
|
5
5
|
const app = express();
|
|
6
|
-
const host = process.env.HOST ||
|
|
7
|
-
const port = Number(process.env.PORT ||
|
|
6
|
+
const host = process.env.HOST || '0.0.0.0';
|
|
7
|
+
const port = Number(process.env.PORT || '3001');
|
|
8
8
|
|
|
9
9
|
app.use(express.json());
|
|
10
10
|
|
|
11
|
-
app.get(
|
|
12
|
-
response.json({ status:
|
|
11
|
+
app.get('/health', (_request, response) => {
|
|
12
|
+
response.json({ status: 'ok' });
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
app.get(
|
|
15
|
+
app.get('/people', (_request, response) => {
|
|
16
16
|
response.json(state.people);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
app.post(
|
|
19
|
+
app.post('/people', (request, response) => {
|
|
20
20
|
const { name, role, email } = request.body;
|
|
21
21
|
if (!name || !role || !email) {
|
|
22
|
-
response.status(400).json({ error:
|
|
22
|
+
response.status(400).json({ error: 'name, role, and email are required' });
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
|