plum-e2e 1.3.7 → 2.2.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 +111 -3
- package/backend/app.js +5 -0
- package/backend/config/scripts/create-test.mjs +172 -0
- package/backend/config/scripts/generate-report.js +2 -1
- package/backend/lib/runnerProcess.js +50 -4
- package/backend/logs/runner-cmqneqerz0000qq01i5ap2rvl.log +22 -0
- package/backend/logs/runner-cmqnfv7kr0000r101aeocm8eu.log +20 -0
- package/backend/logs/runner-cmqnfvb560001r101qoi0phau.log +43 -0
- package/backend/logs/runner-cmqnfvlm20002r101gsyqb837.log +20 -0
- package/backend/logs/runner-cmqnfvqfy0003r101fh41pzx3.log +20 -0
- package/backend/logs/runner-cmqnfvvwo0004r101q4dtqxd2.log +20 -0
- package/backend/middleware/jwtAuth.js +33 -0
- package/backend/middleware/requireAdmin.js +25 -0
- package/backend/package.json +2 -0
- package/backend/prisma/migrations/20260618000000_add_test_repository/migration.sql +133 -0
- package/backend/prisma/migrations/20260618000001_add_user_roles/migration.sql +3 -0
- package/backend/prisma/migrations/20260618000002_drop_automated_tag/migration.sql +2 -0
- package/backend/prisma/migrations/20260618000003_entry_assignee/migration.sql +2 -0
- package/backend/prisma/migrations/20260621000000_add_notifications/migration.sql +8 -0
- package/backend/prisma/schema.prisma +123 -10
- package/backend/routes/auth.routes.js +96 -0
- package/backend/routes/node.routes.js +9 -0
- package/backend/routes/runners.routes.js +10 -0
- package/backend/routes/settings.routes.js +71 -8
- package/backend/routes/test-cases.routes.js +80 -0
- package/backend/routes/test-runs.routes.js +122 -0
- package/backend/routes/test-suites.routes.js +92 -0
- package/backend/routes/users.routes.js +67 -0
- package/backend/scripts/create-test.js +7 -6
- package/backend/scripts/manage-runners.mjs +49 -8
- package/backend/server.js +22 -1
- package/backend/services/cronService.js +91 -7
- package/backend/services/notificationService.js +163 -0
- package/backend/services/reportService.js +96 -4
- package/backend/services/settingsService.js +46 -2
- package/backend/services/testCaseService.js +139 -0
- package/backend/services/testRunService.js +203 -0
- package/backend/services/testSuiteService.js +191 -0
- package/backend/services/userService.js +114 -0
- package/backend/websockets/socketHandler.js +96 -7
- package/bin/plum.js +105 -9
- package/frontend/src/lib/api/auth.js +69 -0
- package/frontend/src/lib/api/repository.js +256 -0
- package/frontend/src/lib/api/schedules.js +5 -1
- package/frontend/src/lib/api/settings.js +15 -0
- package/frontend/src/lib/api/users.js +52 -0
- package/frontend/src/lib/components/layout/Nav.svelte +116 -4
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +321 -31
- package/frontend/src/lib/components/ui/Modal.svelte +8 -1
- package/frontend/src/lib/constants.js +2 -0
- package/frontend/src/lib/stores/auth.js +60 -0
- package/frontend/src/lib/stores/runner.js +11 -2
- package/frontend/src/routes/+layout.svelte +32 -4
- package/frontend/src/routes/+page.svelte +1 -1
- package/frontend/src/routes/login/+page.svelte +209 -0
- package/frontend/src/routes/scheduled-tests/+page.svelte +65 -7
- package/frontend/src/routes/settings/+page.svelte +677 -6
- package/frontend/src/routes/setup/+page.svelte +249 -0
- package/frontend/src/routes/test-repository/+page.svelte +1379 -0
- package/frontend/src/routes/test-repository/runs/[id]/+page.svelte +1549 -0
- package/frontend/src/routes/test-repository/suites/[id]/+page.svelte +1490 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
A ready-to-use E2E test automation environment built on <a href="https://playwright.dev">Playwright</a> + <a href="https://cucumber.io">Cucumber</a>.<br/>
|
|
11
|
-
Write tests in Gherkin, run them from the CLI or UI,
|
|
11
|
+
Write tests in Gherkin, run them from the CLI or UI, view reports, and manage your entire test case repository — all in one place.
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
---
|
|
@@ -113,7 +113,23 @@ The first time you run it, Plum asks a few questions (press Enter to accept the
|
|
|
113
113
|
| **Frontend (UI) port** | `5173` | Host port for the web UI |
|
|
114
114
|
| **Primary public URL** | `http://<your-ip>:<port>` | The address you give runner nodes (see [Runner Setup](#4-runner-setup)) |
|
|
115
115
|
|
|
116
|
-
Your answers are saved to `.plum-server.json`, so the next `plum server start` reuses them without asking.
|
|
116
|
+
Your answers are saved to `.plum-server.json`, so the next `plum server start` reuses them without asking.
|
|
117
|
+
|
|
118
|
+
### First-user setup
|
|
119
|
+
|
|
120
|
+
On the very first start, Plum detects that no user accounts exist and prompts you to create one:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
No users found — create your first account to get started.
|
|
124
|
+
✔ Your name … Jane Smith
|
|
125
|
+
✔ Email address … jane@example.com
|
|
126
|
+
✔ Password …
|
|
127
|
+
✓ Account created for jane@example.com. You can now log in.
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
After that, open the UI at the frontend port it prints (default **http://localhost:5173**) and sign in. Additional users can be invited from **Settings → Account** or the **Settings → Users** page.
|
|
131
|
+
|
|
132
|
+
> If you skip the CLI prompt, visit `http://localhost:5173/setup` in your browser to create the first account.
|
|
117
133
|
|
|
118
134
|
> Docker must be running before you use this command. Plum builds and starts the backend, database, and UI automatically.
|
|
119
135
|
|
|
@@ -292,7 +308,49 @@ plum run-test --browser firefox # run in a specific browser
|
|
|
292
308
|
|
|
293
309
|
---
|
|
294
310
|
|
|
295
|
-
## 4.
|
|
311
|
+
## 4. Test Repository
|
|
312
|
+
|
|
313
|
+
Plum includes a built-in test case management system accessible from the **Test Repository** tab in the UI.
|
|
314
|
+
|
|
315
|
+
### Test Suites and Cases
|
|
316
|
+
|
|
317
|
+
Organise test cases into **suites**. Each suite and case gets an auto-assigned ID (e.g. `TS-001`, `TC-001`). The prefix is configurable in **Settings → Repository**.
|
|
318
|
+
|
|
319
|
+
Each test case has:
|
|
320
|
+
|
|
321
|
+
- **Title** and **Description**
|
|
322
|
+
- **Priority** — Critical, High, Medium, or Low
|
|
323
|
+
- **Test Steps** — an ordered table with columns: Action, Test Data, Expected Output
|
|
324
|
+
- **Automated tag** — a Cucumber `@tag` name that links this case to an automated scenario (e.g. `test-login-1`)
|
|
325
|
+
- **History** — a timeline of every result, from manual test runs and automated builds
|
|
326
|
+
|
|
327
|
+
### Linking automated tests
|
|
328
|
+
|
|
329
|
+
Set the **Automated tag** on a test case to match a Cucumber `@tag` in your feature files. After every automated run, Plum scans the results and:
|
|
330
|
+
|
|
331
|
+
1. Marks matching cases as **automated**
|
|
332
|
+
2. Records a pass/fail entry in the case's history
|
|
333
|
+
|
|
334
|
+
```gherkin
|
|
335
|
+
@test-login-1
|
|
336
|
+
Scenario: User can log in with valid credentials
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
If `TC-042` has `automatedTag = test-login-1`, it will be marked automated and updated after each build — no manual linking required.
|
|
340
|
+
|
|
341
|
+
### Test Runs
|
|
342
|
+
|
|
343
|
+
Create a **Test Run** (e.g. "Sprint 12 regression") and drag test cases from the suite browser into the run. Then switch to **Execute** mode to step through each case and mark it pass / fail / blocked / skip.
|
|
344
|
+
|
|
345
|
+
Results are recorded in the case's history and the run's progress bar updates in real time.
|
|
346
|
+
|
|
347
|
+
### Migrating IDs
|
|
348
|
+
|
|
349
|
+
If you change the test ID prefix (e.g. from `TC` to `CASE`), use **Settings → Repository → Run Migration** to rename all existing IDs at once. Cucumber tags in your code are intentionally left unchanged — update those separately.
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## 5. Runner Setup
|
|
296
354
|
|
|
297
355
|
Runners are additional machines that execute tests in parallel alongside the primary server, letting you distribute a large suite across many nodes. A node runs as a plain Node process — **no Docker required**.
|
|
298
356
|
|
|
@@ -384,6 +442,56 @@ Stops the node started from the current folder. You can also stop individual run
|
|
|
384
442
|
|
|
385
443
|
---
|
|
386
444
|
|
|
445
|
+
## 6. Notifications (Discord & Slack)
|
|
446
|
+
|
|
447
|
+
Plum can post a pass/fail summary to a Discord channel or Slack workspace after every automated or manual test run. The notification includes the job name, overall result, scenario counts, browser, tags, and a link to the full report.
|
|
448
|
+
|
|
449
|
+
### Step 1 — Get a webhook URL
|
|
450
|
+
|
|
451
|
+
**Discord:**
|
|
452
|
+
|
|
453
|
+
1. Open your Discord server, right-click the target channel → **Edit Channel**
|
|
454
|
+
2. Go to **Integrations → Webhooks → New Webhook**
|
|
455
|
+
3. Copy the webhook URL
|
|
456
|
+
|
|
457
|
+
**Slack:**
|
|
458
|
+
|
|
459
|
+
1. Go to your workspace's [Slack App directory](https://api.slack.com/apps) → **Create New App → From scratch**
|
|
460
|
+
2. Under **Features**, choose **Incoming Webhooks** and activate them
|
|
461
|
+
3. Click **Add New Webhook to Workspace**, choose the channel, and copy the webhook URL
|
|
462
|
+
|
|
463
|
+
### Step 2 — Configure in Plum
|
|
464
|
+
|
|
465
|
+
1. Open the Plum UI and go to **Settings → Integrations**
|
|
466
|
+
2. Paste your webhook URL(s) into the **Discord Webhook URL** and/or **Slack Webhook URL** fields
|
|
467
|
+
3. Set **Public URL** to the base address of your Plum instance (e.g. `http://192.168.1.5:5173`). This is used to generate the "View Report" link in the notification. Leave it blank if you don't want report links included.
|
|
468
|
+
4. Click **Save Integrations**
|
|
469
|
+
|
|
470
|
+
### Step 3 — Enable notifications
|
|
471
|
+
|
|
472
|
+
**Scheduled runs:**
|
|
473
|
+
|
|
474
|
+
Open **Scheduled Tests**, click **Edit** (or **New Job**) on a job, and check the **Discord** and/or **Slack** boxes that appear at the bottom of the form. Each job has its own toggle so you can notify only on the jobs that matter.
|
|
475
|
+
|
|
476
|
+
> The notification toggles only appear if the corresponding webhook URL is configured in Settings.
|
|
477
|
+
|
|
478
|
+
**Manual runs:**
|
|
479
|
+
|
|
480
|
+
When at least one webhook is configured, small **Discord** and **Slack** buttons appear in the runner panel at the bottom of every page. Click a button to highlight it — the notification will fire when the run finishes.
|
|
481
|
+
|
|
482
|
+
### What the notification contains
|
|
483
|
+
|
|
484
|
+
| Field | Example |
|
|
485
|
+
| ----------- | ------------------------------------------------------------ |
|
|
486
|
+
| Job / Run | `nightly-login-suite` or `Manual Run` |
|
|
487
|
+
| Status | ✅ PASS or ❌ FAIL |
|
|
488
|
+
| Results | `42 / 45 passed` |
|
|
489
|
+
| Browser | `chromium` |
|
|
490
|
+
| Tags | `@suite-login` |
|
|
491
|
+
| Report link | Button / link to the full HTML report (if Public URL is set) |
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
387
495
|
## Command Reference
|
|
388
496
|
|
|
389
497
|
| Command | Description |
|
package/backend/app.js
CHANGED
|
@@ -39,6 +39,11 @@ if (process.env.PLUM_MODE !== 'node') {
|
|
|
39
39
|
app.use('/settings', require('./routes/settings.routes'));
|
|
40
40
|
app.use('/backup', require('./routes/backup.routes'));
|
|
41
41
|
app.use('/runners', require('./routes/runners.routes'));
|
|
42
|
+
app.use('/auth', require('./routes/auth.routes'));
|
|
43
|
+
app.use('/users', require('./routes/users.routes'));
|
|
44
|
+
app.use('/test-suites', require('./routes/test-suites.routes'));
|
|
45
|
+
app.use('/test-cases', require('./routes/test-cases.routes'));
|
|
46
|
+
app.use('/test-runs', require('./routes/test-runs.routes'));
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
module.exports = app;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of Plum.
|
|
3
|
+
*
|
|
4
|
+
* Plum is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* Plum is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU General Public License
|
|
15
|
+
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as clack from '@clack/prompts';
|
|
19
|
+
import pc from 'picocolors';
|
|
20
|
+
import fs from 'fs';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
|
|
23
|
+
const testsRoot = process.env.TESTS_ROOT || path.join(process.cwd(), 'tests');
|
|
24
|
+
|
|
25
|
+
/* ------------------------------------------------------------------ */
|
|
26
|
+
/* Helpers */
|
|
27
|
+
/* ------------------------------------------------------------------ */
|
|
28
|
+
|
|
29
|
+
function toPascalCase(str) {
|
|
30
|
+
return str
|
|
31
|
+
.replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase())
|
|
32
|
+
.replace(/^(.)/, (c) => c.toUpperCase());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toKebabCase(str) {
|
|
36
|
+
return str
|
|
37
|
+
.replace(/([A-Z])/g, (c) => `-${c.toLowerCase()}`)
|
|
38
|
+
.replace(/[-_\s]+/g, '-')
|
|
39
|
+
.replace(/^-/, '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Strip a trailing "Page" suffix so "CheckoutPage" → base "Checkout",
|
|
43
|
+
// then we append "Page" ourselves — avoiding "CheckoutPagePage".
|
|
44
|
+
function stripPageSuffix(pascal) {
|
|
45
|
+
return pascal.endsWith('Page') ? pascal.slice(0, -4) : pascal;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ------------------------------------------------------------------ */
|
|
49
|
+
/* File generators */
|
|
50
|
+
/* ------------------------------------------------------------------ */
|
|
51
|
+
|
|
52
|
+
function generateFeature(pascal, kebab, suiteTag, testTag) {
|
|
53
|
+
return `${suiteTag}
|
|
54
|
+
Feature: ${pascal}
|
|
55
|
+
|
|
56
|
+
${testTag}
|
|
57
|
+
Scenario: Example scenario
|
|
58
|
+
Given I am on the ${pascal} page
|
|
59
|
+
When I perform an action
|
|
60
|
+
Then I should see the expected result
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function generatePage(base) {
|
|
65
|
+
return `import { page } from '../utils/browser';
|
|
66
|
+
|
|
67
|
+
export class ${base}Page {
|
|
68
|
+
static async goTo() {
|
|
69
|
+
await page().goto(process.env.BASE_URL as string);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static async performAction() {
|
|
73
|
+
// TODO: implement
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static async verifyResult() {
|
|
77
|
+
// TODO: implement
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function generateSteps(pascal, base, kebab) {
|
|
84
|
+
return `import { Given, When, Then } from '@cucumber/cucumber';
|
|
85
|
+
import { ${base}Page } from '../pages/${base}Page';
|
|
86
|
+
|
|
87
|
+
Given('I am on the ${pascal} page', async () => {
|
|
88
|
+
await ${base}Page.goTo();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
When('I perform an action', async () => {
|
|
92
|
+
await ${base}Page.performAction();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
Then('I should see the expected result', async () => {
|
|
96
|
+
await ${base}Page.verifyResult();
|
|
97
|
+
});
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* ------------------------------------------------------------------ */
|
|
102
|
+
/* Main */
|
|
103
|
+
/* ------------------------------------------------------------------ */
|
|
104
|
+
|
|
105
|
+
async function main() {
|
|
106
|
+
clack.intro(pc.bgMagenta(pc.white(' 🟣 Plum — Create Test ')));
|
|
107
|
+
|
|
108
|
+
const rawName = await clack.text({
|
|
109
|
+
message: 'Feature name',
|
|
110
|
+
placeholder: 'Checkout, LoginPage, Cart…',
|
|
111
|
+
hint: 'Creates a .feature, Steps.ts and Page.ts',
|
|
112
|
+
validate: (v) => (!v.trim() ? 'Feature name is required' : undefined)
|
|
113
|
+
});
|
|
114
|
+
if (clack.isCancel(rawName)) {
|
|
115
|
+
clack.cancel('Cancelled.');
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const pascal = toPascalCase(rawName.trim());
|
|
120
|
+
const base = stripPageSuffix(pascal); // "CheckoutPage" → "Checkout", "Cart" → "Cart"
|
|
121
|
+
const kebab = toKebabCase(pascal);
|
|
122
|
+
const suiteTag = `@suite-${kebab}`;
|
|
123
|
+
const testTag = `@test-${kebab}-1`;
|
|
124
|
+
|
|
125
|
+
const featurePath = path.join(testsRoot, 'features', `${pascal}.feature`);
|
|
126
|
+
const pagePath = path.join(testsRoot, 'pages', `${base}Page.ts`);
|
|
127
|
+
const stepsPath = path.join(testsRoot, 'step_definitions', `${pascal}Steps.ts`);
|
|
128
|
+
|
|
129
|
+
const conflicts = [featurePath, pagePath, stepsPath].filter(fs.existsSync);
|
|
130
|
+
if (conflicts.length > 0) {
|
|
131
|
+
clack.log.error('The following files already exist:');
|
|
132
|
+
conflicts.forEach((f) => clack.log.warn(` ${path.relative(process.cwd(), f)}`));
|
|
133
|
+
clack.cancel('Delete them first or choose a different name.');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const s = clack.spinner();
|
|
138
|
+
s.start('Generating files…');
|
|
139
|
+
|
|
140
|
+
fs.mkdirSync(path.join(testsRoot, 'features'), { recursive: true });
|
|
141
|
+
fs.mkdirSync(path.join(testsRoot, 'pages'), { recursive: true });
|
|
142
|
+
fs.mkdirSync(path.join(testsRoot, 'step_definitions'), { recursive: true });
|
|
143
|
+
|
|
144
|
+
fs.writeFileSync(featurePath, generateFeature(pascal, kebab, suiteTag, testTag), 'utf8');
|
|
145
|
+
fs.writeFileSync(pagePath, generatePage(base), 'utf8');
|
|
146
|
+
fs.writeFileSync(stepsPath, generateSteps(pascal, base, kebab), 'utf8');
|
|
147
|
+
|
|
148
|
+
s.stop(pc.green('✓ Files created'));
|
|
149
|
+
|
|
150
|
+
const rel = (p) => path.relative(process.cwd(), p);
|
|
151
|
+
|
|
152
|
+
clack.note(
|
|
153
|
+
[
|
|
154
|
+
`${pc.dim('Feature:')} ${pc.white(rel(featurePath))}`,
|
|
155
|
+
`${pc.dim('Page:')} ${pc.white(rel(pagePath))}`,
|
|
156
|
+
`${pc.dim('Steps:')} ${pc.white(rel(stepsPath))}`,
|
|
157
|
+
'',
|
|
158
|
+
`${pc.dim('Suite tag:')} ${pc.cyan(suiteTag)}`,
|
|
159
|
+
`${pc.dim('Test tag:')} ${pc.cyan(testTag)}`,
|
|
160
|
+
'',
|
|
161
|
+
`${pc.bold('Run with:')} ${pc.cyan(`plum run-test ${testTag}`)}`
|
|
162
|
+
].join('\n'),
|
|
163
|
+
`${pascal} scaffold`
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
clack.outro(pc.magenta('Done! Fill in your scenarios and implement the page methods.'));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
main().catch((err) => {
|
|
170
|
+
clack.log.error(err.message);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
});
|
|
@@ -50,7 +50,8 @@ if (process.env.PLUM_MODE === 'node') process.exit(0);
|
|
|
50
50
|
nodeCount,
|
|
51
51
|
browser: process.env.BROWSER || 'chromium',
|
|
52
52
|
runnerName: process.env.RUNNER_NAME || null,
|
|
53
|
-
runnerId: process.env.RUNNER_ID || null
|
|
53
|
+
runnerId: process.env.RUNNER_ID || null,
|
|
54
|
+
testRunId: process.env.TEST_RUN_ID || null
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
console.log(`Report saved to database (id: ${saved.id})`);
|
|
@@ -49,6 +49,34 @@ function saveRegistry(registry) {
|
|
|
49
49
|
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2), 'utf8');
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Returns the PID of the process listening on the given TCP port, or null.
|
|
54
|
+
* Uses lsof on macOS/Linux and netstat on Windows.
|
|
55
|
+
*/
|
|
56
|
+
function findPidOnPort(port) {
|
|
57
|
+
const portStr = String(port);
|
|
58
|
+
try {
|
|
59
|
+
if (process.platform === 'win32') {
|
|
60
|
+
const out = execSync('netstat -ano', { encoding: 'utf8' });
|
|
61
|
+
for (const line of out.split('\n')) {
|
|
62
|
+
const upper = line.toUpperCase();
|
|
63
|
+
if (upper.includes(`:${portStr}`) && upper.includes('LISTENING')) {
|
|
64
|
+
const parts = line.trim().split(/\s+/);
|
|
65
|
+
const pid = parseInt(parts[parts.length - 1], 10);
|
|
66
|
+
if (!isNaN(pid) && pid > 0) return pid;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
const out = execSync(`lsof -i :${portStr} -t -sTCP:LISTEN`, { encoding: 'utf8' }).trim();
|
|
71
|
+
if (out) {
|
|
72
|
+
const pid = parseInt(out.split('\n')[0].trim(), 10);
|
|
73
|
+
if (!isNaN(pid) && pid > 0) return pid;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch {}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
52
80
|
// Signal 0 performs the OS-level permission/existence check without actually
|
|
53
81
|
// delivering a signal — the portable way to ask "is this pid alive?".
|
|
54
82
|
function isAlive(pid) {
|
|
@@ -128,7 +156,13 @@ function startNode({ id, port, token }) {
|
|
|
128
156
|
|
|
129
157
|
const child = spawn(process.execPath, [SERVER_PATH], {
|
|
130
158
|
cwd: BACKEND_DIR,
|
|
131
|
-
env: {
|
|
159
|
+
env: {
|
|
160
|
+
...process.env,
|
|
161
|
+
NODE_TOKEN: token,
|
|
162
|
+
PLUM_MODE: 'node',
|
|
163
|
+
PORT: String(port),
|
|
164
|
+
RUNNER_ID: String(id)
|
|
165
|
+
},
|
|
132
166
|
detached: true,
|
|
133
167
|
stdio: ['ignore', out, out],
|
|
134
168
|
windowsHide: true
|
|
@@ -146,14 +180,25 @@ function startNode({ id, port, token }) {
|
|
|
146
180
|
/**
|
|
147
181
|
* Stops the managed process for a runner. Returns true if a live process was
|
|
148
182
|
* signalled, false if nothing was running.
|
|
183
|
+
*
|
|
184
|
+
* Falls back to port-based PID discovery when the registry entry is missing or
|
|
185
|
+
* its PID is stale, using the port stored in the entry or an explicit fallback.
|
|
149
186
|
*/
|
|
150
|
-
function stopNode(id) {
|
|
187
|
+
function stopNode(id, fallbackPort = null) {
|
|
151
188
|
const registry = loadRegistry();
|
|
152
189
|
const entry = registry[id];
|
|
153
190
|
let signalled = false;
|
|
154
|
-
|
|
191
|
+
|
|
192
|
+
let pid = entry?.pid && isAlive(entry.pid) ? entry.pid : null;
|
|
193
|
+
|
|
194
|
+
if (!pid) {
|
|
195
|
+
const port = fallbackPort ?? (entry?.port ? Number(entry.port) : null);
|
|
196
|
+
if (port) pid = findPidOnPort(port);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (pid) {
|
|
155
200
|
try {
|
|
156
|
-
process.kill(
|
|
201
|
+
process.kill(pid, 'SIGTERM');
|
|
157
202
|
signalled = true;
|
|
158
203
|
} catch {}
|
|
159
204
|
}
|
|
@@ -171,6 +216,7 @@ module.exports = {
|
|
|
171
216
|
isAlive,
|
|
172
217
|
isLocalUrl,
|
|
173
218
|
parsePort,
|
|
219
|
+
findPidOnPort,
|
|
174
220
|
pruneDead,
|
|
175
221
|
statusOf,
|
|
176
222
|
prepareEnv,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This file is part of Plum.
|
|
3
|
+
|
|
4
|
+
Plum is free software: you can redistribute it and/or modify
|
|
5
|
+
it under the terms of the GNU General Public License as published by
|
|
6
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
(at your option) any later version.
|
|
8
|
+
|
|
9
|
+
Plum is distributed in the hope that it will be useful,
|
|
10
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
You should have received a copy of the GNU General Public License
|
|
15
|
+
along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
|
+
*/
|
|
17
|
+
📂 Loading tests from: /Users/silverlunah/Projects/plum/backend/tests
|
|
18
|
+
Backend running on port 3002 (node/runner mode)
|
|
19
|
+
📂 Loading tests from: /Users/silverlunah/Projects/plum/backend/tests
|
|
20
|
+
Backend running on port 3002 (node/runner mode)
|
|
21
|
+
(node:14993) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
|
|
22
|
+
(Use `node --trace-deprecation ...` to show where the warning was created)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This file is part of Plum.
|
|
3
|
+
|
|
4
|
+
Plum is free software: you can redistribute it and/or modify
|
|
5
|
+
it under the terms of the GNU General Public License as published by
|
|
6
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
(at your option) any later version.
|
|
8
|
+
|
|
9
|
+
Plum is distributed in the hope that it will be useful,
|
|
10
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
You should have received a copy of the GNU General Public License
|
|
15
|
+
along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
|
+
*/
|
|
17
|
+
📂 Loading tests from: /Users/silverlunah/Projects/plum/backend/tests
|
|
18
|
+
Backend running on port 3002 (node/runner mode)
|
|
19
|
+
(node:23570) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
|
|
20
|
+
(Use `node --trace-deprecation ...` to show where the warning was created)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This file is part of Plum.
|
|
3
|
+
|
|
4
|
+
Plum is free software: you can redistribute it and/or modify
|
|
5
|
+
it under the terms of the GNU General Public License as published by
|
|
6
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
(at your option) any later version.
|
|
8
|
+
|
|
9
|
+
Plum is distributed in the hope that it will be useful,
|
|
10
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
You should have received a copy of the GNU General Public License
|
|
15
|
+
along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
|
+
*/
|
|
17
|
+
📂 Loading tests from: /Users/silverlunah/Projects/plum/backend/tests
|
|
18
|
+
node:events:487
|
|
19
|
+
throw er; // Unhandled 'error' event
|
|
20
|
+
^
|
|
21
|
+
|
|
22
|
+
Error: listen EADDRINUSE: address already in use :::3002
|
|
23
|
+
at Server.setupListenHandle [as _listen2] (node:net:2008:16)
|
|
24
|
+
at listenInCluster (node:net:2065:12)
|
|
25
|
+
at Server.listen (node:net:2170:7)
|
|
26
|
+
at start (/Users/silverlunah/Projects/plum/backend/server.js:54:9)
|
|
27
|
+
at Object.<anonymous> (/Users/silverlunah/Projects/plum/backend/server.js:120:1)
|
|
28
|
+
at Module._compile (node:internal/modules/cjs/loader:1829:14)
|
|
29
|
+
at Module._extensions..js (node:internal/modules/cjs/loader:1969:10)
|
|
30
|
+
at Module.load (node:internal/modules/cjs/loader:1552:32)
|
|
31
|
+
at Module._load (node:internal/modules/cjs/loader:1354:12)
|
|
32
|
+
at wrapModuleLoad (node:internal/modules/cjs/loader:255:19)
|
|
33
|
+
Emitted 'error' event on Server instance at:
|
|
34
|
+
at emitErrorNT (node:net:2044:8)
|
|
35
|
+
at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
|
|
36
|
+
code: 'EADDRINUSE',
|
|
37
|
+
errno: -48,
|
|
38
|
+
syscall: 'listen',
|
|
39
|
+
address: '::',
|
|
40
|
+
port: 3002
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Node.js v25.9.0
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This file is part of Plum.
|
|
3
|
+
|
|
4
|
+
Plum is free software: you can redistribute it and/or modify
|
|
5
|
+
it under the terms of the GNU General Public License as published by
|
|
6
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
(at your option) any later version.
|
|
8
|
+
|
|
9
|
+
Plum is distributed in the hope that it will be useful,
|
|
10
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
You should have received a copy of the GNU General Public License
|
|
15
|
+
along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
|
+
*/
|
|
17
|
+
📂 Loading tests from: /Users/silverlunah/Projects/plum/backend/tests
|
|
18
|
+
Backend running on port 3003 (node/runner mode)
|
|
19
|
+
(node:23686) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
|
|
20
|
+
(Use `node --trace-deprecation ...` to show where the warning was created)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This file is part of Plum.
|
|
3
|
+
|
|
4
|
+
Plum is free software: you can redistribute it and/or modify
|
|
5
|
+
it under the terms of the GNU General Public License as published by
|
|
6
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
(at your option) any later version.
|
|
8
|
+
|
|
9
|
+
Plum is distributed in the hope that it will be useful,
|
|
10
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
You should have received a copy of the GNU General Public License
|
|
15
|
+
along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
|
+
*/
|
|
17
|
+
📂 Loading tests from: /Users/silverlunah/Projects/plum/backend/tests
|
|
18
|
+
Backend running on port 3004 (node/runner mode)
|
|
19
|
+
(node:23733) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
|
|
20
|
+
(Use `node --trace-deprecation ...` to show where the warning was created)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This file is part of Plum.
|
|
3
|
+
|
|
4
|
+
Plum is free software: you can redistribute it and/or modify
|
|
5
|
+
it under the terms of the GNU General Public License as published by
|
|
6
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
(at your option) any later version.
|
|
8
|
+
|
|
9
|
+
Plum is distributed in the hope that it will be useful,
|
|
10
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
You should have received a copy of the GNU General Public License
|
|
15
|
+
along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
|
+
*/
|
|
17
|
+
📂 Loading tests from: /Users/silverlunah/Projects/plum/backend/tests
|
|
18
|
+
Backend running on port 3005 (node/runner mode)
|
|
19
|
+
(node:23776) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
|
|
20
|
+
(Use `node --trace-deprecation ...` to show where the warning was created)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of Plum.
|
|
3
|
+
*
|
|
4
|
+
* Plum is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* Plum is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU General Public License
|
|
15
|
+
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { verifyToken } = require('../services/userService');
|
|
19
|
+
|
|
20
|
+
function jwtAuth(req, res, next) {
|
|
21
|
+
const auth = req.headers.authorization;
|
|
22
|
+
if (!auth || !auth.startsWith('Bearer ')) {
|
|
23
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
req.user = verifyToken(auth.slice(7));
|
|
27
|
+
next();
|
|
28
|
+
} catch {
|
|
29
|
+
return res.status(401).json({ error: 'Invalid or expired token' });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { jwtAuth };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of Plum.
|
|
3
|
+
*
|
|
4
|
+
* Plum is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* Plum is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU General Public License
|
|
15
|
+
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
function requireAdmin(req, res, next) {
|
|
19
|
+
if (req.user?.role !== 'admin') {
|
|
20
|
+
return res.status(403).json({ error: 'Admin access required' });
|
|
21
|
+
}
|
|
22
|
+
next();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { requireAdmin };
|
package/backend/package.json
CHANGED
|
@@ -25,12 +25,14 @@
|
|
|
25
25
|
"@clack/prompts": "^1.5.1",
|
|
26
26
|
"@cucumber/cucumber": "^11.2.0",
|
|
27
27
|
"@prisma/client": "^6.19.3",
|
|
28
|
+
"bcryptjs": "^2.4.3",
|
|
28
29
|
"chai": "^4.3.6",
|
|
29
30
|
"chai-soft-assert": "^0.0.5",
|
|
30
31
|
"chokidar": "^5.0.0",
|
|
31
32
|
"cors": "^2.8.5",
|
|
32
33
|
"dotenv": "^16.4.7",
|
|
33
34
|
"express": "^4.21.2",
|
|
35
|
+
"jsonwebtoken": "^9.0.2",
|
|
34
36
|
"node-cron": "^3.0.3",
|
|
35
37
|
"picocolors": "^1.1.1",
|
|
36
38
|
"playwright": "^1.50.1",
|