@appliqation/automation-sdk 2.5.1 → 2.7.0
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 +135 -0
- package/package.json +11 -3
- package/src/cli/auth-setup.js +245 -0
- package/src/index.d.ts +57 -0
- package/src/login/index.d.ts +76 -0
- package/src/login/index.js +48 -0
- package/src/utils/index.js +12 -0
- package/src/utils/setupAuth.js +99 -0
package/README.md
CHANGED
|
@@ -274,6 +274,141 @@ npx playwright test tests/login.spec.js -- --appq --appq_run_title="Login Tests
|
|
|
274
274
|
|
|
275
275
|
---
|
|
276
276
|
|
|
277
|
+
## Authenticated apps (gated SUTs)
|
|
278
|
+
|
|
279
|
+
If your application requires login, the SDK ships a portable auth setup
|
|
280
|
+
that works the same way in Appliqation's executor and in your own CI/local.
|
|
281
|
+
|
|
282
|
+
The login flow itself is **your code, in your repo** (`tests/appliqation/auth/login.ts`).
|
|
283
|
+
Appliqation's executor and `npx appq-auth-setup` both run that same function — so
|
|
284
|
+
SSO, MFA, multi-step login, OAuth, captcha bypass, custom IDP redirects all work
|
|
285
|
+
because you write the Playwright code that handles them.
|
|
286
|
+
|
|
287
|
+
### One-time setup per project
|
|
288
|
+
|
|
289
|
+
1. **In Appliqation**: configure roles + credentials in **Project Settings → Auth Config** (role-by-role usernames/passwords). Credentials are encrypted server-side via AWS Secrets Manager.
|
|
290
|
+
|
|
291
|
+
2. **In your repo**: create `tests/appliqation/auth/login.ts` describing your login flow:
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
import { defineLogin } from '@appliqation/automation-sdk/login';
|
|
295
|
+
|
|
296
|
+
export default defineLogin(async (page, { username, password, role, baseURL }) => {
|
|
297
|
+
await page.goto('/login'); // baseURL handles env (staging/prod)
|
|
298
|
+
await page.getByLabel('Email').fill(username);
|
|
299
|
+
await page.getByLabel('Password').fill(password);
|
|
300
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
301
|
+
await page.waitForURL('**/dashboard');
|
|
302
|
+
|
|
303
|
+
// Branch on role for multi-role flows / SSO / role-specific landing pages
|
|
304
|
+
if (role === 'admin') {
|
|
305
|
+
await page.getByRole('button', { name: 'Switch to admin view' }).click();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Push to the main branch of your project's GitHub repo. Appliqation's GitHub webhook ingests the file automatically.
|
|
311
|
+
|
|
312
|
+
3. **In Appliqation**: click the small **📥 download .env for CI** link on the project settings page. Fill in the placeholders and add to your CI secrets:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
APPLIQATION_API_KEY="<paste your project's API key>"
|
|
316
|
+
APPLIQATION_BASE_URL="https://appliqation.io"
|
|
317
|
+
APPLIQATION_PROJECT_KEY=126
|
|
318
|
+
APPLIQATION_SUT_BASE_URL="<your SUT URL, e.g. https://staging.acme.com>"
|
|
319
|
+
|
|
320
|
+
# Role: default
|
|
321
|
+
APPQ_PROJECT_126_DEFAULT_USERNAME="<paste username>"
|
|
322
|
+
APPQ_PROJECT_126_DEFAULT_PASSWORD="<paste password>"
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
`APPLIQATION_SUT_BASE_URL` is the URL of the system you're testing — your login function's `await page.goto('/login')` resolves against this, so the same code works against staging, preprod, and prod by changing one env var.
|
|
326
|
+
|
|
327
|
+
4. **(Optional)** Click **🧪 Test login** on the project settings page to validate your `login.ts` works against the live SUT. Surfaces success / specific failure inline within ~10 seconds.
|
|
328
|
+
|
|
329
|
+
### In your test files
|
|
330
|
+
|
|
331
|
+
Use `setupAuth` to declare which storage state Playwright should use:
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
const { mapAppqUuid, setupAuth } = require('@appliqation/automation-sdk/utils');
|
|
335
|
+
const { test, expect } = require('@playwright/test');
|
|
336
|
+
|
|
337
|
+
test.use({
|
|
338
|
+
storageState: setupAuth({ project_id: 126, role: 'default' }),
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('manager dashboard loads', async ({ page }, testInfo) => {
|
|
342
|
+
mapAppqUuid(testInfo, '1141-...');
|
|
343
|
+
await page.goto('/dashboard'); // already authenticated
|
|
344
|
+
});
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
`setupAuth()` returns a deterministic file path (e.g. `~/.appq-auth/project-126-default.json`). It does NOT perform login — that's handled separately:
|
|
348
|
+
|
|
349
|
+
- **In Appliqation's executor (cloud runs)**: the LoginHelper imports your `login.ts` from the canonical store, runs it, and writes the storage state at this path before your test runs. Nothing else for you to do.
|
|
350
|
+
- **In your CI / local**: run `npx appq-auth-setup` once before `playwright test`.
|
|
351
|
+
|
|
352
|
+
### CI workflow
|
|
353
|
+
|
|
354
|
+
```yaml
|
|
355
|
+
# .github/workflows/playwright.yml
|
|
356
|
+
- run: npx appq-auth-setup --project-id 126 --role default
|
|
357
|
+
- run: npx playwright test
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
The CLI:
|
|
361
|
+
1. Reads `APPQ_PROJECT_126_DEFAULT_USERNAME` / `_PASSWORD` from env
|
|
362
|
+
2. Reads `APPLIQATION_SUT_BASE_URL` for the SUT base URL
|
|
363
|
+
3. Dynamically imports `tests/appliqation/auth/login.ts` from your local checkout
|
|
364
|
+
4. Launches Chromium, runs your login function, saves the storage state to the path `setupAuth()` will resolve to
|
|
365
|
+
5. Tests run, already authenticated
|
|
366
|
+
|
|
367
|
+
Re-run the CLI when sessions expire (typical: once per CI run). Idempotent and fast on cache hits.
|
|
368
|
+
|
|
369
|
+
#### TypeScript loader (peer dependency)
|
|
370
|
+
|
|
371
|
+
If your `login.ts` is TypeScript (recommended for the type checking on `LoginContext`), the CLI needs a TS loader installed in your project. Install one as a dev dep:
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
npm install --save-dev tsx
|
|
375
|
+
# or
|
|
376
|
+
npm install --save-dev ts-node
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Plain `.js` / `.mjs` / `.cjs` login files don't need either.
|
|
380
|
+
|
|
381
|
+
#### Custom login file path
|
|
382
|
+
|
|
383
|
+
Default convention is `tests/appliqation/auth/login.ts`. Override with the `--login-file` flag or `APPQ_LOGIN_FILE` env var if your project uses a different layout:
|
|
384
|
+
|
|
385
|
+
```yaml
|
|
386
|
+
- run: npx appq-auth-setup --project-id 126 --role default --login-file e2e/auth/login.ts
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Multiple roles
|
|
390
|
+
|
|
391
|
+
Configure each role in Project Settings, download the updated `.env` template (it lists all configured roles), fill in the values:
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
APPQ_PROJECT_126_DEFAULT_USERNAME="..."
|
|
395
|
+
APPQ_PROJECT_126_DEFAULT_PASSWORD="..."
|
|
396
|
+
APPQ_PROJECT_126_MANAGER_USERNAME="..."
|
|
397
|
+
APPQ_PROJECT_126_MANAGER_PASSWORD="..."
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Then in CI — one `appq-auth-setup` call per role you want to test:
|
|
401
|
+
|
|
402
|
+
```yaml
|
|
403
|
+
- run: npx appq-auth-setup --project-id 126 --role default
|
|
404
|
+
- run: npx appq-auth-setup --project-id 126 --role manager
|
|
405
|
+
- run: npx playwright test
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Tests reference the role they need: `setupAuth({ project_id: 126, role: 'manager' })`. Your `login.ts` receives the role in its `LoginContext` so you can branch login flow per role if needed.
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
277
412
|
## Enabling Appliqation Reporting (--appq Flag)
|
|
278
413
|
|
|
279
414
|
By default, Appliqation reporting is **DISABLED**. You must explicitly enable it by adding the `--appq` flag to your test command.
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appliqation/automation-sdk",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Appliqation Automation SDK
|
|
3
|
+
"version": "2.7.0",
|
|
4
|
+
"description": "Appliqation Automation SDK — API key auth, custom run titles, framework reporters, portable storageState (setupAuth), and customer-defined login flows (defineLogin) for gated apps",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"appq-auth-setup": "./src/cli/auth-setup.js"
|
|
9
|
+
},
|
|
7
10
|
"exports": {
|
|
8
11
|
".": {
|
|
9
12
|
"require": "./src/index.js",
|
|
@@ -40,6 +43,11 @@
|
|
|
40
43
|
"./utils": {
|
|
41
44
|
"require": "./src/utils/index.js",
|
|
42
45
|
"import": "./src/utils/index.js"
|
|
46
|
+
},
|
|
47
|
+
"./login": {
|
|
48
|
+
"types": "./src/login/index.d.ts",
|
|
49
|
+
"require": "./src/login/index.js",
|
|
50
|
+
"import": "./src/login/index.js"
|
|
43
51
|
}
|
|
44
52
|
},
|
|
45
53
|
"files": [
|
|
@@ -110,7 +118,7 @@
|
|
|
110
118
|
}
|
|
111
119
|
},
|
|
112
120
|
"engines": {
|
|
113
|
-
"node": ">=
|
|
121
|
+
"node": ">=18.0.0"
|
|
114
122
|
},
|
|
115
123
|
"repository": {
|
|
116
124
|
"type": "git",
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* appq-auth-setup — populate Playwright storageState for an Appliqation
|
|
4
|
+
* project's role by dynamic-importing the customer's login.ts file
|
|
5
|
+
* and running it with role-specific creds from env vars.
|
|
6
|
+
*
|
|
7
|
+
* npx appq-auth-setup --project-id 126 --role manager
|
|
8
|
+
*
|
|
9
|
+
* Customer setup (one-time, per project):
|
|
10
|
+
* 1. Configure roles in Appliqation Project Settings → Auth Config
|
|
11
|
+
* 2. Write `tests/appliqation/auth/login.ts` in their repo:
|
|
12
|
+
*
|
|
13
|
+
* import { defineLogin } from '@appliqation/automation-sdk/login';
|
|
14
|
+
* export default defineLogin(async (page, { username, password }) => {
|
|
15
|
+
* await page.goto('/login');
|
|
16
|
+
* await page.getByLabel('Email').fill(username);
|
|
17
|
+
* await page.getByLabel('Password').fill(password);
|
|
18
|
+
* await page.getByRole('button', { name: 'Sign in' }).click();
|
|
19
|
+
* await page.waitForURL('**\/dashboard');
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* 3. Download the .env template from Project Settings, fill in
|
|
23
|
+
* values, drop into CI secrets
|
|
24
|
+
* 4. Add to CI yaml (one line per role used by their tests):
|
|
25
|
+
* - run: npx appq-auth-setup --project-id 126 --role default
|
|
26
|
+
* - run: npx playwright test
|
|
27
|
+
*
|
|
28
|
+
* What this CLI does:
|
|
29
|
+
* 1. Read APPQ_PROJECT_<id>_<ROLE>_USERNAME / _PASSWORD from env
|
|
30
|
+
* 2. Dynamic-import the customer's login.ts (path discoverable via
|
|
31
|
+
* --login-file flag, env var, or convention)
|
|
32
|
+
* 3. Launch Chromium, run customer's login function with creds +
|
|
33
|
+
* baseURL from APPLIQATION_SUT_BASE_URL env var
|
|
34
|
+
* 4. Save resulting Playwright storageState to the canonical path
|
|
35
|
+
* from setupAuth({ project_id, role })
|
|
36
|
+
*
|
|
37
|
+
* Tests then load via:
|
|
38
|
+
* test.use({ storageState: setupAuth({ project_id, role }) })
|
|
39
|
+
*
|
|
40
|
+
* Usage in CI: run BEFORE `playwright test`. Idempotent — fast on
|
|
41
|
+
* cache hit (~3s for the login flow), faster on no-op.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const fs = require('fs');
|
|
45
|
+
const path = require('path');
|
|
46
|
+
const {
|
|
47
|
+
authStatePath,
|
|
48
|
+
envVarNames,
|
|
49
|
+
sanitizeRole,
|
|
50
|
+
} = require('../utils/setupAuth');
|
|
51
|
+
|
|
52
|
+
const FATAL_EXIT = 1;
|
|
53
|
+
const DEFAULT_LOGIN_FILE = 'tests/appliqation/auth/login.ts';
|
|
54
|
+
|
|
55
|
+
function parseArgs(argv) {
|
|
56
|
+
const args = {};
|
|
57
|
+
for (let i = 2; i < argv.length; i++) {
|
|
58
|
+
const arg = argv[i];
|
|
59
|
+
if (!arg.startsWith('--')) continue;
|
|
60
|
+
const key = arg.slice(2);
|
|
61
|
+
const next = argv[i + 1];
|
|
62
|
+
if (next && !next.startsWith('--')) {
|
|
63
|
+
args[key] = next;
|
|
64
|
+
i++;
|
|
65
|
+
} else {
|
|
66
|
+
args[key] = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return args;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function fail(message) {
|
|
73
|
+
console.error(`✗ ${message}`);
|
|
74
|
+
process.exit(FATAL_EXIT);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve the customer's login.ts path. Priority:
|
|
79
|
+
* 1. --login-file CLI flag (explicit override)
|
|
80
|
+
* 2. APPQ_LOGIN_FILE env var (CI override without changing CLI args)
|
|
81
|
+
* 3. Convention: tests/appliqation/auth/login.ts relative to CWD
|
|
82
|
+
*
|
|
83
|
+
* Throws clearly if the resolved path doesn't exist — customers who
|
|
84
|
+
* haven't created the file yet need a path-specific error, not a
|
|
85
|
+
* cryptic dynamic-import failure.
|
|
86
|
+
*/
|
|
87
|
+
function resolveLoginFile(args) {
|
|
88
|
+
const candidate = args['login-file']
|
|
89
|
+
|| process.env.APPQ_LOGIN_FILE
|
|
90
|
+
|| DEFAULT_LOGIN_FILE;
|
|
91
|
+
const absolute = path.isAbsolute(candidate)
|
|
92
|
+
? candidate
|
|
93
|
+
: path.resolve(process.cwd(), candidate);
|
|
94
|
+
if (!fs.existsSync(absolute)) {
|
|
95
|
+
fail(
|
|
96
|
+
`Login file not found: ${absolute}\n`
|
|
97
|
+
+ ` Create it (default path: ${DEFAULT_LOGIN_FILE}) and export your login flow as a default export:\n`
|
|
98
|
+
+ `\n`
|
|
99
|
+
+ ` import { defineLogin } from '@appliqation/automation-sdk/login';\n`
|
|
100
|
+
+ ` export default defineLogin(async (page, { username, password, baseURL }) => {\n`
|
|
101
|
+
+ ` await page.goto('/login');\n`
|
|
102
|
+
+ ` // ... your login flow\n`
|
|
103
|
+
+ ` });\n`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return absolute;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Dynamic-import the login file and return the default export. Handles
|
|
111
|
+
* both ESM (`export default ...`) and CommonJS (`module.exports = ...`)
|
|
112
|
+
* — the SDK's defineLogin is identity, so customers writing either
|
|
113
|
+
* style end up with a function we can call.
|
|
114
|
+
*
|
|
115
|
+
* For .ts files, requires the customer's repo to have a TS loader
|
|
116
|
+
* registered (tsx, ts-node, esbuild-node-loader). Most Playwright
|
|
117
|
+
* projects do via @playwright/test which transpiles TS on the fly,
|
|
118
|
+
* but the auth-setup CLI runs OUTSIDE the Playwright runner so we
|
|
119
|
+
* need the loader explicitly. We try `tsx` first (lightweight, the
|
|
120
|
+
* most common choice), then `ts-node`, then fail with a clear hint.
|
|
121
|
+
*/
|
|
122
|
+
async function loadLoginFunction(filePath) {
|
|
123
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
124
|
+
if (ext === '.ts' || ext === '.tsx' || ext === '.mts' || ext === '.cts') {
|
|
125
|
+
let registered = false;
|
|
126
|
+
try {
|
|
127
|
+
require('tsx/cjs');
|
|
128
|
+
registered = true;
|
|
129
|
+
} catch {
|
|
130
|
+
try {
|
|
131
|
+
require('ts-node/register/transpile-only');
|
|
132
|
+
registered = true;
|
|
133
|
+
} catch { /* fall through */ }
|
|
134
|
+
}
|
|
135
|
+
if (!registered) {
|
|
136
|
+
fail(
|
|
137
|
+
`Login file is TypeScript (${path.basename(filePath)}) but no TS loader was found.\n`
|
|
138
|
+
+ ` Install one as a dev dependency:\n`
|
|
139
|
+
+ ` npm install --save-dev tsx\n`
|
|
140
|
+
+ ` Or rename your login file to .js (CommonJS) / .mjs (ESM).`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let mod;
|
|
146
|
+
try {
|
|
147
|
+
mod = require(filePath);
|
|
148
|
+
} catch (requireErr) {
|
|
149
|
+
// ESM fallback — `require` of an ESM file throws ERR_REQUIRE_ESM.
|
|
150
|
+
try {
|
|
151
|
+
mod = await import(filePath);
|
|
152
|
+
} catch (importErr) {
|
|
153
|
+
fail(
|
|
154
|
+
`Failed to load ${filePath}:\n`
|
|
155
|
+
+ ` CommonJS error: ${requireErr.message}\n`
|
|
156
|
+
+ ` ESM error: ${importErr.message}`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const fn = mod && (mod.default || mod);
|
|
162
|
+
if (typeof fn !== 'function') {
|
|
163
|
+
fail(
|
|
164
|
+
`${filePath} did not default-export a function.\n`
|
|
165
|
+
+ ` Expected:\n`
|
|
166
|
+
+ ` export default defineLogin(async (page, ctx) => { ... });\n`
|
|
167
|
+
+ ` Got: ${typeof fn}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return fn;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function main() {
|
|
174
|
+
const args = parseArgs(process.argv);
|
|
175
|
+
|
|
176
|
+
const project_id = args['project-id']
|
|
177
|
+
|| process.env.APPLIQATION_PROJECT_KEY
|
|
178
|
+
|| null;
|
|
179
|
+
const role = sanitizeRole(args.role || 'default');
|
|
180
|
+
|
|
181
|
+
if (!project_id) {
|
|
182
|
+
fail(
|
|
183
|
+
'Missing --project-id. Pass it as a flag or set APPLIQATION_PROJECT_KEY '
|
|
184
|
+
+ 'in your env (the downloaded .env from your project settings includes it).'
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const baseURL = (process.env.APPLIQATION_SUT_BASE_URL || '').replace(/\/$/, '');
|
|
189
|
+
if (!baseURL) {
|
|
190
|
+
fail(
|
|
191
|
+
'APPLIQATION_SUT_BASE_URL env var is not set. This is your SUT (System Under Test) URL '
|
|
192
|
+
+ '— e.g. https://staging.acme.com — that the login flow runs against. Add it to your .env / CI secrets.'
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const { username: userVar, password: pwdVar } = envVarNames({ project_id, role });
|
|
197
|
+
const username = process.env[userVar];
|
|
198
|
+
const password = process.env[pwdVar];
|
|
199
|
+
if (!username || !password) {
|
|
200
|
+
fail(
|
|
201
|
+
`Missing credential env vars: ${userVar} and/or ${pwdVar}.\n`
|
|
202
|
+
+ ` Download the .env template from your project settings in Appliqation,\n`
|
|
203
|
+
+ ` fill in the values, and add them to your CI secrets.`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const loginFile = resolveLoginFile(args);
|
|
208
|
+
console.log(`→ Loading login flow from ${path.relative(process.cwd(), loginFile)}`);
|
|
209
|
+
const loginFn = await loadLoginFunction(loginFile);
|
|
210
|
+
|
|
211
|
+
// Lazy-require playwright — it's a peer dep, not always installed.
|
|
212
|
+
let chromium;
|
|
213
|
+
try {
|
|
214
|
+
({ chromium } = require('playwright'));
|
|
215
|
+
} catch {
|
|
216
|
+
fail(
|
|
217
|
+
'Playwright is not installed. Run `npm install --save-dev playwright` '
|
|
218
|
+
+ '(or @playwright/test) and try again.'
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const targetPath = authStatePath({ project_id, role });
|
|
223
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
224
|
+
|
|
225
|
+
console.log(`→ Launching Chromium for login as role "${role}" against ${baseURL}`);
|
|
226
|
+
const browser = await chromium.launch({ headless: true });
|
|
227
|
+
const context = await browser.newContext({
|
|
228
|
+
baseURL,
|
|
229
|
+
ignoreHTTPSErrors: true,
|
|
230
|
+
});
|
|
231
|
+
const page = await context.newPage();
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
await loginFn(page, { username, password, role, baseURL });
|
|
235
|
+
await context.storageState({ path: targetPath });
|
|
236
|
+
console.log(`✓ Auth state saved to ${targetPath}`);
|
|
237
|
+
console.log(` Tests using setupAuth({ project_id: ${project_id}, role: '${role}' }) will pick this up.`);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
fail(`Login failed: ${err.message}`);
|
|
240
|
+
} finally {
|
|
241
|
+
await browser.close();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
main().catch((err) => fail(err.message || String(err)));
|
package/src/index.d.ts
CHANGED
|
@@ -331,3 +331,60 @@ export const logger: {
|
|
|
331
331
|
* ```
|
|
332
332
|
*/
|
|
333
333
|
export function mapAppqUuid(testInfo: any, uuid: string): void;
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Returns the canonical Playwright `storageState` file path for a given
|
|
337
|
+
* Appliqation project + role. The same path resolves regardless of
|
|
338
|
+
* execution context, so the same generated/authored test runs unchanged
|
|
339
|
+
* in:
|
|
340
|
+
* - Appliqation's executor (LoginHelper writes the file at this path)
|
|
341
|
+
* - Customer CI / local (the `appq-auth-setup` CLI writes the file)
|
|
342
|
+
*
|
|
343
|
+
* Does NOT perform login itself — only computes the path. Pair with
|
|
344
|
+
* `npx appq-auth-setup --project-id X --role Y` (or Appliqation's
|
|
345
|
+
* executor) to populate the file before tests run.
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```typescript
|
|
349
|
+
* import { test } from '@playwright/test';
|
|
350
|
+
* import { mapAppqUuid, setupAuth } from '@appliqation/automation-sdk/utils';
|
|
351
|
+
*
|
|
352
|
+
* test.use({ storageState: setupAuth({ project_id: 126, role: 'default' }) });
|
|
353
|
+
*
|
|
354
|
+
* test('manager dashboard loads', async ({ page }, testInfo) => {
|
|
355
|
+
* mapAppqUuid(testInfo, '1141-...');
|
|
356
|
+
* await page.goto('/dashboard'); // already authenticated
|
|
357
|
+
* });
|
|
358
|
+
* ```
|
|
359
|
+
*/
|
|
360
|
+
export function setupAuth(options: { project_id: number | string; role?: string }): string;
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Computes the canonical storageState file path. Same as setupAuth, but
|
|
364
|
+
* usable by tooling that needs the path without going through Playwright
|
|
365
|
+
* (e.g. the appq-auth-setup CLI writes to this path).
|
|
366
|
+
*/
|
|
367
|
+
export function authStatePath(options: { project_id: number | string; role?: string }): string;
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Base directory for storageState files. Defaults to `~/.appq-auth/`;
|
|
371
|
+
* overridable via APPQ_AUTH_STATE_DIR env (Appliqation's executor sets
|
|
372
|
+
* this per-run for tenant isolation).
|
|
373
|
+
*/
|
|
374
|
+
export function authStateDir(): string;
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* The canonical env-var names the CLI looks up for credentials, given
|
|
378
|
+
* a project_id + role. Customers populate these from the `.env` template
|
|
379
|
+
* downloaded from Appliqation's project settings.
|
|
380
|
+
*/
|
|
381
|
+
export function envVarNames(options: { project_id: number | string; role?: string }): {
|
|
382
|
+
username: string;
|
|
383
|
+
password: string;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Normalize a role name to lowercase with only alphanumerics, underscore,
|
|
388
|
+
* and hyphen. Mirrors the role-name validation in the Appliqation UI.
|
|
389
|
+
*/
|
|
390
|
+
export function sanitizeRole(role: string): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript definitions for @appliqation/automation-sdk/login.
|
|
3
|
+
*
|
|
4
|
+
* Customers writing `tests/appliqation/auth/login.ts` import these
|
|
5
|
+
* types for autocomplete + compile-time checking on the login flow
|
|
6
|
+
* function shape. Same file gets imported (untyped, dynamically) by
|
|
7
|
+
* the Appliqation executor's LoginHelper and the appq-auth-setup CLI.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Page } from '@playwright/test';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Per-call context passed to the customer's login function. Sourced
|
|
14
|
+
* differently in each runtime:
|
|
15
|
+
*
|
|
16
|
+
* - Appliqation executor: username + password from AWS Secrets
|
|
17
|
+
* Manager (read-only IAM); baseURL from the project's env
|
|
18
|
+
* configuration; role from snapshot.auth_role.
|
|
19
|
+
* - Customer CI (`npx appq-auth-setup`): username + password from
|
|
20
|
+
* APPQ_PROJECT_<id>_<ROLE>_USERNAME/PASSWORD env vars; baseURL
|
|
21
|
+
* from APPLIQATION_SUT_BASE_URL env var; role from --role flag.
|
|
22
|
+
*/
|
|
23
|
+
export interface LoginContext {
|
|
24
|
+
/** Username / email for the configured role. */
|
|
25
|
+
username: string;
|
|
26
|
+
/** Password for the configured role. */
|
|
27
|
+
password: string;
|
|
28
|
+
/**
|
|
29
|
+
* Role name as configured in Project Settings. Lets the customer
|
|
30
|
+
* branch login flow per role (e.g. an admin's login may include a
|
|
31
|
+
* "Switch to admin view" step that other roles skip).
|
|
32
|
+
*/
|
|
33
|
+
role: string;
|
|
34
|
+
/**
|
|
35
|
+
* SUT base URL for the env this run targets. Use `await
|
|
36
|
+
* page.goto('/login')` — Playwright resolves the relative path
|
|
37
|
+
* against the context's baseURL, which is set from this value.
|
|
38
|
+
* Provided here for cases where the customer needs the full URL
|
|
39
|
+
* (e.g. cross-origin OAuth redirects).
|
|
40
|
+
*/
|
|
41
|
+
baseURL: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Customer's login function. Should leave the page in an
|
|
46
|
+
* authenticated state — typically by navigating to the login URL,
|
|
47
|
+
* filling credentials, submitting, and waiting for a post-login URL
|
|
48
|
+
* or visible-element indicator. The caller (LoginHelper or
|
|
49
|
+
* appq-auth-setup) captures the resulting storageState immediately
|
|
50
|
+
* after this resolves.
|
|
51
|
+
*
|
|
52
|
+
* Throw on failure. The caller will surface the error verbatim in
|
|
53
|
+
* the executor's run audit / the CLI's exit code.
|
|
54
|
+
*/
|
|
55
|
+
export type LoginFunction = (
|
|
56
|
+
page: Page,
|
|
57
|
+
ctx: LoginContext,
|
|
58
|
+
) => Promise<void>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Identity helper for type inference. Wrap your login function with
|
|
62
|
+
* this so your IDE provides autocomplete on the LoginContext fields:
|
|
63
|
+
*
|
|
64
|
+
* ```typescript
|
|
65
|
+
* import { defineLogin } from '@appliqation/automation-sdk/login';
|
|
66
|
+
*
|
|
67
|
+
* export default defineLogin(async (page, { username, password }) => {
|
|
68
|
+
* await page.goto('/login');
|
|
69
|
+
* await page.getByLabel('Email').fill(username);
|
|
70
|
+
* await page.getByLabel('Password').fill(password);
|
|
71
|
+
* await page.getByRole('button', { name: 'Sign in' }).click();
|
|
72
|
+
* await page.waitForURL('**\/dashboard');
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function defineLogin<F extends LoginFunction>(fn: F): F;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @appliqation/automation-sdk/login — types + helpers for customer-
|
|
3
|
+
* supplied login flows.
|
|
4
|
+
*
|
|
5
|
+
* Customers define their SUT's login flow as a Playwright function in
|
|
6
|
+
* their own repo (canonical path: `tests/appliqation/auth/login.ts`).
|
|
7
|
+
* The function is imported by:
|
|
8
|
+
* 1. Appliqation's executor (LoginHelper) — pulled from MongoDB
|
|
9
|
+
* after a GitHub-webhook upsert into automan_canonical_scripts
|
|
10
|
+
* with script_type:'login', dynamic-imported per role per cache
|
|
11
|
+
* miss.
|
|
12
|
+
* 2. Customer CI (`npx appq-auth-setup`) — dynamic-imported from
|
|
13
|
+
* disk, run with role-specific creds from env vars, output a
|
|
14
|
+
* Playwright storageState file all tests load via
|
|
15
|
+
* test.use({ storageState: setupAuth({ ... }) }).
|
|
16
|
+
*
|
|
17
|
+
* Same source file, two readers. Both run the customer's own code.
|
|
18
|
+
*
|
|
19
|
+
* Why offline GitOps and not in-app upload/edit:
|
|
20
|
+
* - Customer's IDE + lint + PR review + git history come for free
|
|
21
|
+
* - No in-app editor to build, no sandboxing concern beyond what we
|
|
22
|
+
* already have for test scripts
|
|
23
|
+
* - The login function is just Playwright code — it should live next
|
|
24
|
+
* to the Playwright tests
|
|
25
|
+
*
|
|
26
|
+
* The selector-fill model in the legacy auth_config schema (login_url
|
|
27
|
+
* + 3 CSS selectors + success_indicator) only handled the simplest
|
|
28
|
+
* form-login case. SSO / MFA / OAuth / multi-step / captcha all
|
|
29
|
+
* require custom code anyway. Customer login.ts handles everything.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Identity helper that gives type inference + auto-complete for
|
|
34
|
+
* customers writing their login.ts. Mirrors Playwright's
|
|
35
|
+
* `defineConfig` pattern — pure function, no runtime side-effects.
|
|
36
|
+
*
|
|
37
|
+
* @template {LoginFunction} F
|
|
38
|
+
* @param {F} fn
|
|
39
|
+
* @returns {F}
|
|
40
|
+
*/
|
|
41
|
+
function defineLogin(fn) {
|
|
42
|
+
if (typeof fn !== 'function') {
|
|
43
|
+
throw new TypeError('defineLogin: argument must be a function');
|
|
44
|
+
}
|
|
45
|
+
return fn;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { defineLogin };
|
package/src/utils/index.js
CHANGED
|
@@ -6,6 +6,13 @@ const UuidValidator = require('./UuidValidator');
|
|
|
6
6
|
const PayloadBuilder = require('./PayloadBuilder');
|
|
7
7
|
const RunDataNormalizer = require('./RunDataNormalizer');
|
|
8
8
|
const { mapAppqUuid } = require('./mapAppqUuid');
|
|
9
|
+
const {
|
|
10
|
+
setupAuth,
|
|
11
|
+
authStatePath,
|
|
12
|
+
authStateDir,
|
|
13
|
+
envVarNames,
|
|
14
|
+
sanitizeRole,
|
|
15
|
+
} = require('./setupAuth');
|
|
9
16
|
const logger = require('./logger');
|
|
10
17
|
|
|
11
18
|
module.exports = {
|
|
@@ -13,5 +20,10 @@ module.exports = {
|
|
|
13
20
|
PayloadBuilder,
|
|
14
21
|
RunDataNormalizer,
|
|
15
22
|
mapAppqUuid,
|
|
23
|
+
setupAuth,
|
|
24
|
+
authStatePath,
|
|
25
|
+
authStateDir,
|
|
26
|
+
envVarNames,
|
|
27
|
+
sanitizeRole,
|
|
16
28
|
logger
|
|
17
29
|
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* setupAuth — portable Playwright storageState resolution.
|
|
3
|
+
*
|
|
4
|
+
* Returns a deterministic file path. The same path resolves to the same
|
|
5
|
+
* file regardless of execution context, so the same generated/authored
|
|
6
|
+
* test script runs unchanged in:
|
|
7
|
+
*
|
|
8
|
+
* - Appliqation's executor (the LoginHelper writes the file at this
|
|
9
|
+
* path during validation prep, using AWS Secrets Manager-backed
|
|
10
|
+
* credentials)
|
|
11
|
+
*
|
|
12
|
+
* - Customer CI / local (the `appq-auth-setup` CLI writes the file at
|
|
13
|
+
* this path, using credentials from env vars per the convention
|
|
14
|
+
* downloaded from Project Settings → "download .env for CI")
|
|
15
|
+
*
|
|
16
|
+
* setupAuth() does NOT perform login itself — that's the CLI's job (CI)
|
|
17
|
+
* or the executor's job (Appliqation cloud). It only computes the path.
|
|
18
|
+
*
|
|
19
|
+
* Usage in a Playwright test file:
|
|
20
|
+
*
|
|
21
|
+
* const { mapAppqUuid, setupAuth } = require('@appliqation/automation-sdk/utils');
|
|
22
|
+
*
|
|
23
|
+
* test.use({ storageState: setupAuth({ project_id: 126, role: 'default' }) });
|
|
24
|
+
*
|
|
25
|
+
* test('...', async ({ page }, testInfo) => {
|
|
26
|
+
* mapAppqUuid(testInfo, '<uuid>');
|
|
27
|
+
* // body — already authenticated
|
|
28
|
+
* });
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const path = require('path');
|
|
32
|
+
const os = require('os');
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Default base directory for storageState files. Overridable via
|
|
36
|
+
* APPQ_AUTH_STATE_DIR env var (Appliqation executor sets this per-run
|
|
37
|
+
* for tenant isolation; customers normally don't need to).
|
|
38
|
+
*/
|
|
39
|
+
function authStateDir() {
|
|
40
|
+
return process.env.APPQ_AUTH_STATE_DIR
|
|
41
|
+
|| path.join(os.homedir(), '.appq-auth');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Compute the canonical storageState path for a given project + role.
|
|
46
|
+
* Pure function — no I/O. Both the SDK CLI and the Appliqation executor
|
|
47
|
+
* call this same function so they always agree on the location.
|
|
48
|
+
*/
|
|
49
|
+
function authStatePath({ project_id, role = 'default' }) {
|
|
50
|
+
if (project_id === undefined || project_id === null || project_id === '') {
|
|
51
|
+
throw new Error('authStatePath requires project_id');
|
|
52
|
+
}
|
|
53
|
+
const safeRole = sanitizeRole(role);
|
|
54
|
+
return path.join(authStateDir(), `project-${project_id}-${safeRole}.json`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The public entry point. Returns a string suitable for Playwright's
|
|
59
|
+
* `storageState` option (a file path).
|
|
60
|
+
*/
|
|
61
|
+
function setupAuth({ project_id, role = 'default' } = {}) {
|
|
62
|
+
if (project_id === undefined || project_id === null || project_id === '') {
|
|
63
|
+
throw new Error(
|
|
64
|
+
'setupAuth requires project_id. Generated test files include this '
|
|
65
|
+
+ 'as a literal; for hand-authored tests, copy from your project URL '
|
|
66
|
+
+ 'in Appliqation.'
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return authStatePath({ project_id, role });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Lowercase + alphanumeric/underscore/hyphen only. Mirrors the role-name
|
|
74
|
+
* validation in the Appliqation project settings UI so paths line up.
|
|
75
|
+
*/
|
|
76
|
+
function sanitizeRole(role) {
|
|
77
|
+
return String(role).toLowerCase().replace(/[^a-z0-9_-]/g, '_');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Env-var name convention for the CLI (and any other consumer) to look
|
|
82
|
+
* up credentials. Matches what the "download .env for CI" feature in
|
|
83
|
+
* Appliqation generates. Exported so the CLI and the SDK never drift.
|
|
84
|
+
*/
|
|
85
|
+
function envVarNames({ project_id, role }) {
|
|
86
|
+
const safeRole = sanitizeRole(role).toUpperCase();
|
|
87
|
+
return {
|
|
88
|
+
username: `APPQ_PROJECT_${project_id}_${safeRole}_USERNAME`,
|
|
89
|
+
password: `APPQ_PROJECT_${project_id}_${safeRole}_PASSWORD`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
setupAuth,
|
|
95
|
+
authStatePath,
|
|
96
|
+
authStateDir,
|
|
97
|
+
envVarNames,
|
|
98
|
+
sanitizeRole,
|
|
99
|
+
};
|