plum-e2e 2.4.1 → 2.4.3
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/backend/app.js +7 -0
- package/backend/middleware/jwtAuth.js +14 -2
- package/backend/scripts/manage-runners.mjs +2 -2
- package/backend/services/reportService.js +6 -3
- package/backend/services/runnerService.js +14 -3
- package/bin/plum.js +3 -3
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +18 -2
- package/package.json +1 -1
package/backend/app.js
CHANGED
|
@@ -47,4 +47,11 @@ if (process.env.PLUM_MODE !== 'node') {
|
|
|
47
47
|
app.use('/trigger', require('./routes/trigger.routes'));
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// Global JSON error handler — Express's default sends HTML, which breaks JSON clients
|
|
51
|
+
// eslint-disable-next-line no-unused-vars
|
|
52
|
+
app.use((err, req, res, next) => {
|
|
53
|
+
console.error(err);
|
|
54
|
+
res.status(err.status || 500).json({ error: err.message || 'Internal server error' });
|
|
55
|
+
});
|
|
56
|
+
|
|
50
57
|
module.exports = app;
|
|
@@ -40,12 +40,24 @@ function jwtAuth(req, res, next) {
|
|
|
40
40
|
if (!auth || !auth.startsWith('Bearer ')) {
|
|
41
41
|
return res.status(401).json({ error: 'Unauthorized' });
|
|
42
42
|
}
|
|
43
|
+
let payload;
|
|
43
44
|
try {
|
|
44
|
-
|
|
45
|
-
next();
|
|
45
|
+
payload = verifyToken(auth.slice(7));
|
|
46
46
|
} catch {
|
|
47
47
|
return res.status(401).json({ error: 'Invalid or expired token' });
|
|
48
48
|
}
|
|
49
|
+
// Confirm the user still exists — catches stale JWTs after a DB reset
|
|
50
|
+
prisma.user
|
|
51
|
+
.findUnique({ where: { id: payload.userId }, select: { id: true } })
|
|
52
|
+
.then((user) => {
|
|
53
|
+
if (!user) return res.status(401).json({ error: 'Session expired. Please log in again.' });
|
|
54
|
+
req.user = payload;
|
|
55
|
+
next();
|
|
56
|
+
})
|
|
57
|
+
.catch(() => {
|
|
58
|
+
req.user = payload;
|
|
59
|
+
next();
|
|
60
|
+
});
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
module.exports = { jwtAuth };
|
|
@@ -57,10 +57,10 @@ function resolveNodeUrl(url) {
|
|
|
57
57
|
const u = new URL(url);
|
|
58
58
|
if (u.hostname === 'localhost' || u.hostname === '127.0.0.1') {
|
|
59
59
|
u.hostname = 'host.docker.internal';
|
|
60
|
-
return u.toString();
|
|
61
60
|
}
|
|
61
|
+
return u.toString().replace(/\/+$/, '');
|
|
62
62
|
} catch {}
|
|
63
|
-
return url;
|
|
63
|
+
return url.replace(/\/+$/, '');
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
async function fetchRunners() {
|
|
@@ -297,10 +297,12 @@ const saveReport = async ({
|
|
|
297
297
|
browser,
|
|
298
298
|
runnerName,
|
|
299
299
|
runnerId,
|
|
300
|
-
testRunId
|
|
300
|
+
testRunId,
|
|
301
|
+
forceFail = false
|
|
301
302
|
}) => {
|
|
302
303
|
const normTrigger = normaliseTrigger(triggerType);
|
|
303
|
-
const { features, status } = processCucumberJson(rawCucumberJson);
|
|
304
|
+
const { features, status: derivedStatus } = processCucumberJson(rawCucumberJson);
|
|
305
|
+
const status = forceFail ? 'FAIL' : derivedStatus;
|
|
304
306
|
const cronJobId = await resolveCronJobId(normTrigger);
|
|
305
307
|
|
|
306
308
|
const report = await prisma.report.create({
|
|
@@ -374,7 +376,8 @@ const saveCombinedReport = async ({
|
|
|
374
376
|
browser,
|
|
375
377
|
runnerName: runners.map((r) => r.name).join(', '),
|
|
376
378
|
runnerId: null,
|
|
377
|
-
testRunId: testRunId ?? null
|
|
379
|
+
testRunId: testRunId ?? null,
|
|
380
|
+
forceFail: reports.some((r) => r === null)
|
|
378
381
|
});
|
|
379
382
|
};
|
|
380
383
|
|
|
@@ -25,8 +25,10 @@ const prisma = require('./prisma');
|
|
|
25
25
|
|
|
26
26
|
const getAll = () => prisma.runner.findMany({ orderBy: { createdAt: 'asc' } });
|
|
27
27
|
|
|
28
|
+
const normaliseUrl = (url) => (url ?? '').replace(/\/+$/, '');
|
|
29
|
+
|
|
28
30
|
const create = ({ name, url, token, browser = 'chromium' }) =>
|
|
29
|
-
prisma.runner.create({ data: { name, url, token, browser } });
|
|
31
|
+
prisma.runner.create({ data: { name, url: normaliseUrl(url), token, browser } });
|
|
30
32
|
|
|
31
33
|
async function remove(id) {
|
|
32
34
|
// Scrub the deleted runner from any cron job's runnerIds string before
|
|
@@ -45,7 +47,11 @@ async function remove(id) {
|
|
|
45
47
|
return prisma.runner.delete({ where: { id } });
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
const update = (id, data) =>
|
|
50
|
+
const update = (id, data) =>
|
|
51
|
+
prisma.runner.update({
|
|
52
|
+
where: { id },
|
|
53
|
+
data: { ...data, ...(data.url && { url: normaliseUrl(data.url) }) }
|
|
54
|
+
});
|
|
49
55
|
|
|
50
56
|
const getById = (id) => prisma.runner.findUnique({ where: { id } });
|
|
51
57
|
|
|
@@ -61,7 +67,12 @@ async function probe({ url, token }) {
|
|
|
61
67
|
headers: { Authorization: `Bearer ${token}` },
|
|
62
68
|
signal: AbortSignal.timeout(5000)
|
|
63
69
|
});
|
|
64
|
-
return { ok:
|
|
70
|
+
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
|
|
71
|
+
const body = await res.json();
|
|
72
|
+
if (!body.ok || body.mode !== 'node') {
|
|
73
|
+
return { ok: false, error: 'URL does not point to a Plum runner node' };
|
|
74
|
+
}
|
|
75
|
+
return { ok: true, latency: Date.now() - start };
|
|
65
76
|
} catch (e) {
|
|
66
77
|
return { ok: false, error: e.message };
|
|
67
78
|
}
|
package/bin/plum.js
CHANGED
|
@@ -318,9 +318,9 @@ async function serverStart() {
|
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
clack.log.info(pc.
|
|
322
|
-
|
|
323
|
-
clack.outro(pc.
|
|
321
|
+
clack.log.info(`UI: ${pc.cyan(`http://localhost:${cfg.frontendPort}`)}`);
|
|
322
|
+
clack.log.info(`API: ${pc.cyan(`http://localhost:${cfg.backendPort}`)}`);
|
|
323
|
+
clack.outro(pc.green('Plum is running. Use "plum server stop" to shut down.'));
|
|
324
324
|
}
|
|
325
325
|
|
|
326
326
|
async function serverReconfig() {
|
|
@@ -714,14 +714,30 @@
|
|
|
714
714
|
{/if}
|
|
715
715
|
|
|
716
716
|
{#each cronJobs as name}
|
|
717
|
-
<
|
|
717
|
+
<a
|
|
718
|
+
href="/scheduled-tests"
|
|
719
|
+
class="run-card cron-run"
|
|
720
|
+
transition:fly={{ x: -4, duration: 160 }}
|
|
721
|
+
>
|
|
718
722
|
<span class="run-card-dot pulse-pass"></span>
|
|
719
723
|
<div class="run-card-info">
|
|
720
724
|
<span class="run-card-label">{name}</span>
|
|
721
725
|
<span class="run-card-meta">Scheduled run</span>
|
|
722
726
|
</div>
|
|
723
727
|
<span class="run-card-badge cron-badge">Scheduled</span>
|
|
724
|
-
|
|
728
|
+
<svg
|
|
729
|
+
width="13"
|
|
730
|
+
height="13"
|
|
731
|
+
viewBox="0 0 24 24"
|
|
732
|
+
fill="none"
|
|
733
|
+
stroke="currentColor"
|
|
734
|
+
stroke-width="2"
|
|
735
|
+
stroke-linecap="round"
|
|
736
|
+
class="run-card-arrow"
|
|
737
|
+
>
|
|
738
|
+
<line x1="5" y1="12" x2="19" y2="12" /><polyline points="12 5 19 12 12 19" />
|
|
739
|
+
</svg>
|
|
740
|
+
</a>
|
|
725
741
|
{/each}
|
|
726
742
|
|
|
727
743
|
{#if !anyRunning}
|