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.
Files changed (62) hide show
  1. package/README.md +111 -3
  2. package/backend/app.js +5 -0
  3. package/backend/config/scripts/create-test.mjs +172 -0
  4. package/backend/config/scripts/generate-report.js +2 -1
  5. package/backend/lib/runnerProcess.js +50 -4
  6. package/backend/logs/runner-cmqneqerz0000qq01i5ap2rvl.log +22 -0
  7. package/backend/logs/runner-cmqnfv7kr0000r101aeocm8eu.log +20 -0
  8. package/backend/logs/runner-cmqnfvb560001r101qoi0phau.log +43 -0
  9. package/backend/logs/runner-cmqnfvlm20002r101gsyqb837.log +20 -0
  10. package/backend/logs/runner-cmqnfvqfy0003r101fh41pzx3.log +20 -0
  11. package/backend/logs/runner-cmqnfvvwo0004r101q4dtqxd2.log +20 -0
  12. package/backend/middleware/jwtAuth.js +33 -0
  13. package/backend/middleware/requireAdmin.js +25 -0
  14. package/backend/package.json +2 -0
  15. package/backend/prisma/migrations/20260618000000_add_test_repository/migration.sql +133 -0
  16. package/backend/prisma/migrations/20260618000001_add_user_roles/migration.sql +3 -0
  17. package/backend/prisma/migrations/20260618000002_drop_automated_tag/migration.sql +2 -0
  18. package/backend/prisma/migrations/20260618000003_entry_assignee/migration.sql +2 -0
  19. package/backend/prisma/migrations/20260621000000_add_notifications/migration.sql +8 -0
  20. package/backend/prisma/schema.prisma +123 -10
  21. package/backend/routes/auth.routes.js +96 -0
  22. package/backend/routes/node.routes.js +9 -0
  23. package/backend/routes/runners.routes.js +10 -0
  24. package/backend/routes/settings.routes.js +71 -8
  25. package/backend/routes/test-cases.routes.js +80 -0
  26. package/backend/routes/test-runs.routes.js +122 -0
  27. package/backend/routes/test-suites.routes.js +92 -0
  28. package/backend/routes/users.routes.js +67 -0
  29. package/backend/scripts/create-test.js +7 -6
  30. package/backend/scripts/manage-runners.mjs +49 -8
  31. package/backend/server.js +22 -1
  32. package/backend/services/cronService.js +91 -7
  33. package/backend/services/notificationService.js +163 -0
  34. package/backend/services/reportService.js +96 -4
  35. package/backend/services/settingsService.js +46 -2
  36. package/backend/services/testCaseService.js +139 -0
  37. package/backend/services/testRunService.js +203 -0
  38. package/backend/services/testSuiteService.js +191 -0
  39. package/backend/services/userService.js +114 -0
  40. package/backend/websockets/socketHandler.js +96 -7
  41. package/bin/plum.js +105 -9
  42. package/frontend/src/lib/api/auth.js +69 -0
  43. package/frontend/src/lib/api/repository.js +256 -0
  44. package/frontend/src/lib/api/schedules.js +5 -1
  45. package/frontend/src/lib/api/settings.js +15 -0
  46. package/frontend/src/lib/api/users.js +52 -0
  47. package/frontend/src/lib/components/layout/Nav.svelte +116 -4
  48. package/frontend/src/lib/components/layout/RunnerPanel.svelte +321 -31
  49. package/frontend/src/lib/components/ui/Modal.svelte +8 -1
  50. package/frontend/src/lib/constants.js +2 -0
  51. package/frontend/src/lib/stores/auth.js +60 -0
  52. package/frontend/src/lib/stores/runner.js +11 -2
  53. package/frontend/src/routes/+layout.svelte +32 -4
  54. package/frontend/src/routes/+page.svelte +1 -1
  55. package/frontend/src/routes/login/+page.svelte +209 -0
  56. package/frontend/src/routes/scheduled-tests/+page.svelte +65 -7
  57. package/frontend/src/routes/settings/+page.svelte +677 -6
  58. package/frontend/src/routes/setup/+page.svelte +249 -0
  59. package/frontend/src/routes/test-repository/+page.svelte +1379 -0
  60. package/frontend/src/routes/test-repository/runs/[id]/+page.svelte +1549 -0
  61. package/frontend/src/routes/test-repository/suites/[id]/+page.svelte +1490 -0
  62. 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, and view reports — all in one place.
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. When it finishes, open the UI at the frontend port it prints (default **http://localhost:5173**).
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. Runner Setup
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: { ...process.env, NODE_TOKEN: token, PLUM_MODE: 'node', PORT: String(port) },
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
- if (entry?.pid && isAlive(entry.pid)) {
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(entry.pid, 'SIGTERM');
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 };
@@ -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",