@ossy/platform 1.35.1 → 1.36.1
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 +116 -61
- package/package.json +13 -10
- package/src/test/e2e-runner.js +134 -0
- package/src/test/e2e.util.js +109 -0
- package/src/test/index.js +1 -0
- package/src/test/playwright.config.js +30 -0
package/README.md
CHANGED
|
@@ -1,95 +1,150 @@
|
|
|
1
1
|
# @ossy/platform
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Express-based application server runtime for the Ossy platform. It reads the build manifest produced by `@ossy/app build` and wires up pages, API routes, tasks, actions, integrations, aggregates, and startup hooks — all without any server-side configuration.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## What it does
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
At startup `@ossy/platform`:
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
1. Loads `build/manifest.json` produced by `@ossy/app build`.
|
|
10
|
+
2. Registers and runs all **startup hooks** (`*.startup.js`) in order.
|
|
11
|
+
3. Connects all **integrations** (`*.integration.js`) by calling `connect({ env })`.
|
|
12
|
+
4. Registers all **tasks** (`*.task.js`) with `TaskService` and starts the cron scheduler.
|
|
13
|
+
5. Registers all **resource templates** (`*.resource.js`) with `registerResourceTemplate`.
|
|
14
|
+
6. Rebuilds all **aggregates** (`*.aggregate.js`) from the event store.
|
|
15
|
+
7. Registers all **actions** (`*.action.js`) with `ActionService`.
|
|
16
|
+
8. Starts an Express server that routes requests to pages (`*.page.jsx`) and API handlers (`*.api.js`).
|
|
17
|
+
9. Auto-mounts every action at `POST /actions/:id`.
|
|
10
18
|
|
|
11
|
-
|
|
19
|
+
## Quick start
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
```sh
|
|
22
|
+
# In your app directory
|
|
23
|
+
npm install @ossy/app @ossy/platform
|
|
14
24
|
|
|
15
|
-
|
|
25
|
+
# Build the app
|
|
26
|
+
npx app build
|
|
16
27
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
| `credentials` | `string[]` | Names of environment variables that must be present. If any are missing the integration is skipped at startup with a warning. |
|
|
21
|
-
| `connect` | `async ({ env }) => client` | Called once at startup with the full `process.env`. Returns the client object stored by `id`. If it throws, the integration is skipped (non-fatal). |
|
|
28
|
+
# Start the server
|
|
29
|
+
npx platform start
|
|
30
|
+
```
|
|
22
31
|
|
|
23
|
-
|
|
32
|
+
Or programmatically:
|
|
24
33
|
|
|
25
34
|
```js
|
|
26
|
-
|
|
27
|
-
import Stripe from 'stripe'
|
|
35
|
+
import { startServer } from '@ossy/platform'
|
|
28
36
|
|
|
29
|
-
|
|
37
|
+
const { port, close } = await startServer({
|
|
38
|
+
cwd: process.cwd(), // defaults to process.cwd()
|
|
39
|
+
buildDir: 'build', // defaults to 'build'
|
|
40
|
+
port: 3000, // also reads --port / PORT env var
|
|
41
|
+
})
|
|
42
|
+
```
|
|
30
43
|
|
|
31
|
-
|
|
44
|
+
## Server configuration
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
The server reads configuration from:
|
|
47
|
+
|
|
48
|
+
- `--port` / `-p` CLI flag, or the `PORT` environment variable (default `3000`).
|
|
49
|
+
- `build/manifest.json` — produced by `@ossy/app build`.
|
|
50
|
+
- `process.env` — used by integrations for their credentials and by startup hooks.
|
|
37
51
|
|
|
38
|
-
|
|
52
|
+
Optional environment variables used by the platform itself:
|
|
39
53
|
|
|
40
|
-
|
|
54
|
+
| Variable | Description |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `DB_URL` | MongoDB connection string. Required for tasks and aggregates. |
|
|
57
|
+
| `API_URL` + `OSSY_API_KEY` | SDK configuration. When set, tasks receive a pre-configured SDK instance. |
|
|
58
|
+
| `PORT` | HTTP port. |
|
|
59
|
+
|
|
60
|
+
## Exported API
|
|
41
61
|
|
|
42
62
|
```js
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
63
|
+
import {
|
|
64
|
+
startServer,
|
|
65
|
+
loadManifest,
|
|
66
|
+
resolveEntryUrl,
|
|
67
|
+
ConfigService,
|
|
68
|
+
ActionService,
|
|
69
|
+
StorageClient,
|
|
70
|
+
S3Client,
|
|
71
|
+
LocalStorageClient,
|
|
72
|
+
getSystemResourceTemplates,
|
|
73
|
+
normalizeAndValidateDocumentContent,
|
|
74
|
+
validateResourceTemplatesForImport,
|
|
75
|
+
} from '@ossy/platform'
|
|
55
76
|
```
|
|
56
77
|
|
|
57
|
-
|
|
78
|
+
### `ActionService`
|
|
79
|
+
|
|
80
|
+
Registry for `*.action.js` command handlers.
|
|
58
81
|
|
|
59
|
-
|
|
82
|
+
```js
|
|
83
|
+
import { ActionService } from '@ossy/platform'
|
|
60
84
|
|
|
61
|
-
|
|
85
|
+
// Invoke an action from server-side code (bypasses HTTP)
|
|
86
|
+
const result = await ActionService.invoke('orders/create', {
|
|
87
|
+
payload: { ... },
|
|
88
|
+
req: { userId: 'user-123', workspaceId: 'ws-456' },
|
|
89
|
+
})
|
|
62
90
|
|
|
63
|
-
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"entry": "/static/stripe.integration-7f2a.js",
|
|
69
|
-
"credentials": ["STRIPE_SECRET_KEY"]
|
|
70
|
-
}
|
|
71
|
-
]
|
|
72
|
-
}
|
|
91
|
+
// Look up a registered action
|
|
92
|
+
const action = ActionService.get('orders/create') // { id, access, run } | null
|
|
93
|
+
|
|
94
|
+
// List all registered actions
|
|
95
|
+
const all = ActionService.all()
|
|
73
96
|
```
|
|
74
97
|
|
|
75
|
-
###
|
|
98
|
+
### `getSystemResourceTemplates`
|
|
76
99
|
|
|
77
|
-
|
|
100
|
+
Returns all resource templates registered from `*.resource.js` files.
|
|
78
101
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
| `email` | `SES_REGION`, `SES_ACCESS_KEY_ID`, `SES_SECRET_ACCESS_KEY` | `SESClient` from `@aws-sdk/client-ses` |
|
|
102
|
+
```js
|
|
103
|
+
import { getSystemResourceTemplates } from '@ossy/platform'
|
|
82
104
|
|
|
83
|
-
|
|
105
|
+
const templates = getSystemResourceTemplates()
|
|
106
|
+
// [{ id: '@ossy/tool/doc', name: 'Tool Doc', fields: [...] }, ...]
|
|
107
|
+
```
|
|
84
108
|
|
|
85
|
-
|
|
109
|
+
## Primitives
|
|
86
110
|
|
|
87
|
-
|
|
88
|
-
import { IntegrationService } from '@ossy/platform/integrations'
|
|
111
|
+
The platform is built around file conventions called **primitives**. Each primitive is a file with a specific naming pattern that the build pipeline auto-discovers.
|
|
89
112
|
|
|
90
|
-
|
|
91
|
-
|
|
113
|
+
**→ See [PRIMITIVES.md](./PRIMITIVES.md) for the complete reference.**
|
|
114
|
+
|
|
115
|
+
| Primitive | Pattern | Purpose |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| Page | `*.page.jsx` | Routable UI (SSR + hydration) |
|
|
118
|
+
| API | `*.api.js` | HTTP endpoint (any method) |
|
|
119
|
+
| Task | `*.task.js` | Event-driven or scheduled async work |
|
|
120
|
+
| Action | `*.action.js` | Named command, auto-exposed at `POST /actions/:id` |
|
|
121
|
+
| Integration | `*.integration.js` | Third-party client connected at startup |
|
|
122
|
+
| Email | `*.email.jsx` | Transactional React email template |
|
|
123
|
+
| Component | `*.component.jsx` | Injectable UI fragment |
|
|
124
|
+
| Resource | `*.resource.js` | Custom document-type schema |
|
|
125
|
+
| Aggregate | `*.aggregate.js` | Event-sourced domain object |
|
|
126
|
+
| Startup | `*.startup.js` | One-time boot hook |
|
|
127
|
+
|
|
128
|
+
## Request lifecycle
|
|
92
129
|
|
|
93
|
-
// Retrieve a connected client by id.
|
|
94
|
-
const client = IntegrationService.get('email') // SESClient | null
|
|
95
130
|
```
|
|
131
|
+
Incoming request
|
|
132
|
+
│
|
|
133
|
+
├─ POST /actions/:id ──► ActionService.invoke() ──► action.run({ payload, sdk, log, integrations, req })
|
|
134
|
+
│
|
|
135
|
+
├─ Match API route ──► api.handle(req, res)
|
|
136
|
+
│
|
|
137
|
+
└─ Match page route ──► page.render(props) ──► HTML response
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Requests that do not match an API route or page route receive `404 Not Found`.
|
|
141
|
+
|
|
142
|
+
## Related packages
|
|
143
|
+
|
|
144
|
+
| Package | Purpose |
|
|
145
|
+
|---|---|
|
|
146
|
+
| [`@ossy/app`](../app) | Build pipeline — discovers primitives, bundles them, writes `manifest.json` |
|
|
147
|
+
| [`@ossy/event-store`](../event-store) | Event sourcing primitives (`Aggregate`, `EventStore`) |
|
|
148
|
+
| [`@ossy/email`](../email) | Email renderer and `email.integration.js` |
|
|
149
|
+
| [`@ossy/observability`](../observability) | Structured logger and metrics |
|
|
150
|
+
| [`@ossy/router`](../router) | URL matching used by the platform server |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/platform",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.36.1",
|
|
4
4
|
"description": "Ossy application server runtime",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
"./resources": "./src/resources/index.js",
|
|
22
22
|
"./definition": "./src/Definition.js",
|
|
23
23
|
"./integrations": "./src/integration.service.js",
|
|
24
|
-
"./test": "./src/test/index.js"
|
|
24
|
+
"./test": "./src/test/index.js",
|
|
25
|
+
"./test/e2e-runner.js": "./src/test/e2e-runner.js",
|
|
26
|
+
"./test/playwright.config.js": "./src/test/playwright.config.js"
|
|
25
27
|
},
|
|
26
28
|
"scripts": {
|
|
27
29
|
"start": "PORT=3003 node -e \"import('./src/server.js').then(m => m.startServer())\"",
|
|
@@ -36,13 +38,13 @@
|
|
|
36
38
|
"@aws-sdk/s3-request-presigner": "^3.1057.0",
|
|
37
39
|
"@aws-sdk/util-create-request": "^3.972.26",
|
|
38
40
|
"@aws-sdk/util-format-url": "^3.972.17",
|
|
39
|
-
"@ossy/event-store": "^1.
|
|
40
|
-
"@ossy/observability": "^1.
|
|
41
|
-
"@ossy/policies": "^1.
|
|
42
|
-
"@ossy/router": "^1.
|
|
43
|
-
"@ossy/sdk": "^1.
|
|
44
|
-
"@ossy/tokens": "^1.
|
|
45
|
-
"@ossy/users": "^1.
|
|
41
|
+
"@ossy/event-store": "^1.5.1",
|
|
42
|
+
"@ossy/observability": "^1.5.1",
|
|
43
|
+
"@ossy/policies": "^1.10.1",
|
|
44
|
+
"@ossy/router": "^1.37.1",
|
|
45
|
+
"@ossy/sdk": "^1.37.1",
|
|
46
|
+
"@ossy/tokens": "^1.10.1",
|
|
47
|
+
"@ossy/users": "^1.10.1",
|
|
46
48
|
"cookie-parser": "^1.4.7",
|
|
47
49
|
"dotenv": ">=16.0.0 <18.0.0",
|
|
48
50
|
"express": ">=5.0.0 <6.0.0",
|
|
@@ -52,6 +54,7 @@
|
|
|
52
54
|
},
|
|
53
55
|
"devDependencies": {
|
|
54
56
|
"@jest/globals": "^30.2.0",
|
|
57
|
+
"@playwright/test": ">=1.40.0",
|
|
55
58
|
"casual": "^1.6.2",
|
|
56
59
|
"jest": "^30.2.0"
|
|
57
60
|
},
|
|
@@ -59,5 +62,5 @@
|
|
|
59
62
|
"src",
|
|
60
63
|
"Dockerfile"
|
|
61
64
|
],
|
|
62
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "d5b71d16ec56c819401c7a74f95d12b2347ca3c4"
|
|
63
66
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright test runner for `*.e2e.js` primitive modules.
|
|
3
|
+
*
|
|
4
|
+
* Usage in a Playwright spec file:
|
|
5
|
+
*
|
|
6
|
+
* import { registerE2eTest } from '@ossy/platform/test'
|
|
7
|
+
* import * as mod from '@ossy/authentication/src/sign-in.e2e.js'
|
|
8
|
+
*
|
|
9
|
+
* registerE2eTest(mod)
|
|
10
|
+
*
|
|
11
|
+
* Or for automatic discovery of all installed @ossy/* package e2e tests:
|
|
12
|
+
*
|
|
13
|
+
* import { registerInstalledE2eTests } from '@ossy/platform/test'
|
|
14
|
+
* await registerInstalledE2eTests(import.meta.url)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs'
|
|
18
|
+
import path from 'node:path'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
import { test, expect } from '@playwright/test'
|
|
21
|
+
import { signUpAndGetToken, verifySignIn } from './e2e.util.js'
|
|
22
|
+
|
|
23
|
+
export { signUpAndGetToken, verifySignIn }
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extended Playwright test fixture that injects shared e2e helpers.
|
|
27
|
+
*
|
|
28
|
+
* Test functions receive all standard Playwright fixtures plus:
|
|
29
|
+
* - `signUpAndGetToken(firstName?, lastName?)` — signs up a user and returns `{ email, userId, token }`
|
|
30
|
+
* - `verifySignIn(token)` — calls the verify-sign-in endpoint
|
|
31
|
+
* - `expect` — Playwright's enhanced expect
|
|
32
|
+
*/
|
|
33
|
+
export const e2eTest = test.extend({
|
|
34
|
+
signUpAndGetToken: async ({}, use) => { await use(signUpAndGetToken) },
|
|
35
|
+
verifySignIn: async ({}, use) => { await use(verifySignIn) },
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Register a single `*.e2e.js` module as a Playwright test suite.
|
|
40
|
+
*
|
|
41
|
+
* The module must export:
|
|
42
|
+
* - `id` {string} Unique test id (e.g. `'authentication/sign-in'`)
|
|
43
|
+
* - `feature` {string} Grouping label (e.g. `'authentication'`)
|
|
44
|
+
* - `requires` {string[]} Dependencies (e.g. `['server', 'database']`)
|
|
45
|
+
* - `default` {Function} Async test function receiving Playwright fixtures
|
|
46
|
+
*
|
|
47
|
+
* @param {{ id: string, feature?: string, requires?: string[], default: Function }} mod
|
|
48
|
+
*/
|
|
49
|
+
export function registerE2eTest(mod) {
|
|
50
|
+
const { id, feature, default: testFn } = mod
|
|
51
|
+
if (typeof id !== 'string' || !id) throw new Error(`e2e module is missing a string "id" export`)
|
|
52
|
+
if (typeof testFn !== 'function') throw new Error(`e2e module "${id}" must default-export a function`)
|
|
53
|
+
|
|
54
|
+
e2eTest.describe(feature || id, () => {
|
|
55
|
+
e2eTest(id, async (fixtures) => {
|
|
56
|
+
await testFn({ ...fixtures, expect })
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Walk `node_modules` relative to `fromUrl` (typically `import.meta.url` of
|
|
63
|
+
* the calling spec file) and register all `*.e2e.js` files found in packages
|
|
64
|
+
* that declare `"ossy": { "src": "..." }` in their `package.json`.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} fromUrl `import.meta.url` of the calling spec file.
|
|
67
|
+
*/
|
|
68
|
+
export async function registerInstalledE2eTests(fromUrl) {
|
|
69
|
+
const callerDir = path.dirname(fileURLToPath(fromUrl))
|
|
70
|
+
const nmDir = findNodeModules(callerDir)
|
|
71
|
+
if (!nmDir) return
|
|
72
|
+
|
|
73
|
+
const e2ePaths = discoverE2eFiles(nmDir)
|
|
74
|
+
for (const filePath of e2ePaths) {
|
|
75
|
+
const mod = await import(filePath)
|
|
76
|
+
registerE2eTest(mod)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function findNodeModules(startDir) {
|
|
81
|
+
let dir = startDir
|
|
82
|
+
for (let i = 0; i < 10; i++) {
|
|
83
|
+
const candidate = path.join(dir, 'node_modules')
|
|
84
|
+
if (fs.existsSync(candidate)) return candidate
|
|
85
|
+
const parent = path.dirname(dir)
|
|
86
|
+
if (parent === dir) break
|
|
87
|
+
dir = parent
|
|
88
|
+
}
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function discoverE2eFiles(nmDir) {
|
|
93
|
+
const results = []
|
|
94
|
+
const E2E_PATTERN = /\.e2e\.(mjs|cjs|js)$/
|
|
95
|
+
|
|
96
|
+
const tryPackageDir = (pkgDir) => {
|
|
97
|
+
const pkgJsonPath = path.join(pkgDir, 'package.json')
|
|
98
|
+
if (!fs.existsSync(pkgJsonPath)) return
|
|
99
|
+
let pkg
|
|
100
|
+
try { pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')) } catch { return }
|
|
101
|
+
if (!pkg.ossy?.src) return
|
|
102
|
+
|
|
103
|
+
const srcDir = path.resolve(pkgDir, pkg.ossy.src)
|
|
104
|
+
walk(srcDir, (filePath) => {
|
|
105
|
+
if (E2E_PATTERN.test(path.basename(filePath))) results.push(filePath)
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const walk = (dir, cb) => {
|
|
110
|
+
if (!fs.existsSync(dir)) return
|
|
111
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
112
|
+
const full = path.join(dir, entry.name)
|
|
113
|
+
if (entry.isDirectory()) walk(full, cb)
|
|
114
|
+
else if (entry.isFile()) cb(full)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const entry of fs.readdirSync(nmDir, { withFileTypes: true })) {
|
|
119
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
|
|
120
|
+
if (entry.name.startsWith('@')) {
|
|
121
|
+
const scopeDir = path.join(nmDir, entry.name)
|
|
122
|
+
if (!fs.existsSync(scopeDir)) continue
|
|
123
|
+
for (const scoped of fs.readdirSync(scopeDir, { withFileTypes: true })) {
|
|
124
|
+
if (scoped.isDirectory() || scoped.isSymbolicLink()) {
|
|
125
|
+
tryPackageDir(path.join(scopeDir, scoped.name))
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
tryPackageDir(path.join(nmDir, entry.name))
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return results
|
|
134
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { MongoClient } from 'mongodb'
|
|
2
|
+
|
|
3
|
+
const DB_URL = process.env.DB_URL ?? 'mongodb://localhost:27017/'
|
|
4
|
+
const DB_NAME = process.env.DB_NAME ?? 'ossy-local'
|
|
5
|
+
export const API_URL = process.env.OSSY_API_URL ?? process.env.API_URL ?? 'http://localhost:3001/api/v0'
|
|
6
|
+
export const APP_URL = process.env.OSSY_APP_URL ?? process.env.BASE_URL ?? 'http://localhost:3002'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Polls MongoDB until a Verification token appears for the given userId,
|
|
10
|
+
* or throws if it doesn't arrive within the timeout.
|
|
11
|
+
*
|
|
12
|
+
* Uses directConnection=true so the driver doesn't follow the replica-set
|
|
13
|
+
* member list (which uses Docker-internal hostnames like "mongodb:27017"
|
|
14
|
+
* even when connecting via localhost port-forwarding).
|
|
15
|
+
*/
|
|
16
|
+
async function getVerificationToken(db, userId, timeoutMs = 10000) {
|
|
17
|
+
const eventstore = db.collection('eventstore')
|
|
18
|
+
const deadline = Date.now() + timeoutMs
|
|
19
|
+
while (Date.now() < deadline) {
|
|
20
|
+
const event = await eventstore.findOne({
|
|
21
|
+
aggregateType: 'Token',
|
|
22
|
+
type: 'Created',
|
|
23
|
+
'payload.type': 'Verification',
|
|
24
|
+
createdBy: userId,
|
|
25
|
+
}, { sort: { _id: -1 } })
|
|
26
|
+
if (event?.payload?.token) return event.payload.token
|
|
27
|
+
await new Promise(r => setTimeout(r, 200))
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Verification token for user ${userId} not found within ${timeoutMs}ms`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Polls MongoDB until a SignedUp event for the given email is found.
|
|
34
|
+
*/
|
|
35
|
+
async function getUserIdByEmail(db, email, timeoutMs = 10000) {
|
|
36
|
+
const eventstore = db.collection('eventstore')
|
|
37
|
+
const deadline = Date.now() + timeoutMs
|
|
38
|
+
while (Date.now() < deadline) {
|
|
39
|
+
const event = await eventstore.findOne({
|
|
40
|
+
aggregateType: 'User',
|
|
41
|
+
type: 'SignedUp',
|
|
42
|
+
'payload.email': email,
|
|
43
|
+
}, { sort: { _id: -1 } })
|
|
44
|
+
if (event?.aggregateId) return event.aggregateId
|
|
45
|
+
await new Promise(r => setTimeout(r, 200))
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`SignedUp event for ${email} not found within ${timeoutMs}ms`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Polls MongoDB until the workspace aggregate for the given userId appears.
|
|
52
|
+
* Not fatal if it doesn't arrive — tests that rely on workspace state should
|
|
53
|
+
* handle the null return.
|
|
54
|
+
*/
|
|
55
|
+
async function waitForWorkspaceAggregate(db, userId, timeoutMs = 15000) {
|
|
56
|
+
const aggregates = db.collection('aggregates')
|
|
57
|
+
const deadline = Date.now() + timeoutMs
|
|
58
|
+
while (Date.now() < deadline) {
|
|
59
|
+
const workspace = await aggregates.findOne({
|
|
60
|
+
type: 'Workspace',
|
|
61
|
+
'state.users': userId,
|
|
62
|
+
})
|
|
63
|
+
if (workspace) return workspace
|
|
64
|
+
await new Promise(r => setTimeout(r, 300))
|
|
65
|
+
}
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Signs up a new user via the API and returns `{ email, userId, token }`.
|
|
71
|
+
*
|
|
72
|
+
* Waits for the workspace aggregate to be built so tests that rely on
|
|
73
|
+
* `useWorkspaces()` for the post-login redirect will find a workspace
|
|
74
|
+
* immediately. Uses a unique email per call to avoid cross-run conflicts.
|
|
75
|
+
*/
|
|
76
|
+
export async function signUpAndGetToken(firstName = 'Test', lastName = 'User') {
|
|
77
|
+
const email = `e2e-${Date.now()}@ossy.local`
|
|
78
|
+
const client = new MongoClient(DB_URL, { directConnection: true })
|
|
79
|
+
try {
|
|
80
|
+
await client.connect()
|
|
81
|
+
const db = client.db(DB_NAME)
|
|
82
|
+
|
|
83
|
+
const res = await fetch(`${API_URL}/users/sign-up`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'content-type': 'application/json' },
|
|
86
|
+
body: JSON.stringify({ email, firstName, lastName }),
|
|
87
|
+
})
|
|
88
|
+
if (!res.ok) throw new Error(`Sign-up failed with status ${res.status}`)
|
|
89
|
+
|
|
90
|
+
const userId = await getUserIdByEmail(db, email)
|
|
91
|
+
const token = await getVerificationToken(db, userId)
|
|
92
|
+
await waitForWorkspaceAggregate(db, userId)
|
|
93
|
+
|
|
94
|
+
return { email, userId, token }
|
|
95
|
+
} finally {
|
|
96
|
+
await client.close()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Calls the verify-sign-in endpoint and returns the raw Set-Cookie header.
|
|
102
|
+
*/
|
|
103
|
+
export async function verifySignIn(token) {
|
|
104
|
+
const res = await fetch(`${API_URL}/users/verify-sign-in?token=${token}`)
|
|
105
|
+
if (!res.ok) throw new Error(`Verify sign-in failed with status ${res.status}`)
|
|
106
|
+
const cookie = res.headers.get('set-cookie')
|
|
107
|
+
if (!cookie) throw new Error('No Set-Cookie header in verify-sign-in response')
|
|
108
|
+
return cookie
|
|
109
|
+
}
|
package/src/test/index.js
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable Playwright configuration for Ossy feature-package e2e tests.
|
|
3
|
+
*
|
|
4
|
+
* Consuming projects can extend this config:
|
|
5
|
+
*
|
|
6
|
+
* // playwright.config.js
|
|
7
|
+
* import base from '@ossy/platform/test/playwright.config.js'
|
|
8
|
+
* export default { ...base, testDir: './tests' }
|
|
9
|
+
*
|
|
10
|
+
* Environment variables (all optional — sensible defaults provided):
|
|
11
|
+
* OSSY_APP_URL Base URL of the running web client (default: http://localhost:3002)
|
|
12
|
+
* OSSY_API_URL Base URL of the API server (default: http://localhost:3001/api/v0)
|
|
13
|
+
* DB_URL MongoDB connection string (default: mongodb://localhost:27017/)
|
|
14
|
+
* DB_NAME MongoDB database name (default: ossy-local)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
|
18
|
+
export default {
|
|
19
|
+
retries: process.env.CI ? 1 : 0,
|
|
20
|
+
use: {
|
|
21
|
+
baseURL: process.env.OSSY_APP_URL ?? process.env.BASE_URL ?? 'http://localhost:3002',
|
|
22
|
+
},
|
|
23
|
+
reporter: [['list']],
|
|
24
|
+
env: {
|
|
25
|
+
OSSY_APP_URL: process.env.OSSY_APP_URL ?? process.env.BASE_URL ?? 'http://localhost:3002',
|
|
26
|
+
OSSY_API_URL: process.env.OSSY_API_URL ?? process.env.API_URL ?? 'http://localhost:3001/api/v0',
|
|
27
|
+
DB_URL: process.env.DB_URL ?? 'mongodb://localhost:27017/',
|
|
28
|
+
DB_NAME: process.env.DB_NAME ?? 'ossy-local',
|
|
29
|
+
},
|
|
30
|
+
}
|