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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
-- AlterTable Project: add test repository settings and sequence counters
|
|
2
|
+
ALTER TABLE "Project" ADD COLUMN "testCasePrefix" TEXT NOT NULL DEFAULT 'TC';
|
|
3
|
+
ALTER TABLE "Project" ADD COLUMN "testSuitePrefix" TEXT NOT NULL DEFAULT 'TS';
|
|
4
|
+
ALTER TABLE "Project" ADD COLUMN "caseSeqNext" INTEGER NOT NULL DEFAULT 0;
|
|
5
|
+
ALTER TABLE "Project" ADD COLUMN "suiteSeqNext" INTEGER NOT NULL DEFAULT 0;
|
|
6
|
+
|
|
7
|
+
-- AlterTable Report: add relation to TestCaseHistory (no schema change needed here, FK is on history side)
|
|
8
|
+
|
|
9
|
+
-- CreateTable User
|
|
10
|
+
CREATE TABLE "User" (
|
|
11
|
+
"id" TEXT NOT NULL,
|
|
12
|
+
"name" TEXT NOT NULL,
|
|
13
|
+
"email" TEXT NOT NULL,
|
|
14
|
+
"password" TEXT NOT NULL,
|
|
15
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
16
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
17
|
+
|
|
18
|
+
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
-- CreateTable TestSuite
|
|
22
|
+
CREATE TABLE "TestSuite" (
|
|
23
|
+
"id" TEXT NOT NULL,
|
|
24
|
+
"displayId" TEXT NOT NULL,
|
|
25
|
+
"name" TEXT NOT NULL,
|
|
26
|
+
"description" TEXT NOT NULL DEFAULT '',
|
|
27
|
+
"priority" TEXT NOT NULL DEFAULT 'Medium',
|
|
28
|
+
"createdById" TEXT NOT NULL,
|
|
29
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
30
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
31
|
+
|
|
32
|
+
CONSTRAINT "TestSuite_pkey" PRIMARY KEY ("id")
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
-- CreateTable TestCase
|
|
36
|
+
CREATE TABLE "TestCase" (
|
|
37
|
+
"id" TEXT NOT NULL,
|
|
38
|
+
"displayId" TEXT NOT NULL,
|
|
39
|
+
"title" TEXT NOT NULL,
|
|
40
|
+
"description" TEXT NOT NULL DEFAULT '',
|
|
41
|
+
"priority" TEXT NOT NULL DEFAULT 'Medium',
|
|
42
|
+
"automatedTag" TEXT,
|
|
43
|
+
"isAutomated" BOOLEAN NOT NULL DEFAULT false,
|
|
44
|
+
"suiteId" TEXT NOT NULL,
|
|
45
|
+
"createdById" TEXT NOT NULL,
|
|
46
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
47
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
48
|
+
|
|
49
|
+
CONSTRAINT "TestCase_pkey" PRIMARY KEY ("id")
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
-- CreateTable TestStep
|
|
53
|
+
CREATE TABLE "TestStep" (
|
|
54
|
+
"id" TEXT NOT NULL,
|
|
55
|
+
"caseId" TEXT NOT NULL,
|
|
56
|
+
"action" TEXT NOT NULL,
|
|
57
|
+
"testData" TEXT NOT NULL DEFAULT '',
|
|
58
|
+
"expectedOutput" TEXT NOT NULL DEFAULT '',
|
|
59
|
+
"order" INTEGER NOT NULL,
|
|
60
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
61
|
+
|
|
62
|
+
CONSTRAINT "TestStep_pkey" PRIMARY KEY ("id")
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
-- CreateTable TestRun
|
|
66
|
+
CREATE TABLE "TestRun" (
|
|
67
|
+
"id" TEXT NOT NULL,
|
|
68
|
+
"title" TEXT NOT NULL,
|
|
69
|
+
"status" TEXT NOT NULL DEFAULT 'draft',
|
|
70
|
+
"createdById" TEXT NOT NULL,
|
|
71
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
72
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
73
|
+
|
|
74
|
+
CONSTRAINT "TestRun_pkey" PRIMARY KEY ("id")
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
-- CreateTable TestRunEntry
|
|
78
|
+
CREATE TABLE "TestRunEntry" (
|
|
79
|
+
"id" TEXT NOT NULL,
|
|
80
|
+
"runId" TEXT NOT NULL,
|
|
81
|
+
"caseId" TEXT NOT NULL,
|
|
82
|
+
"status" TEXT NOT NULL DEFAULT 'pending',
|
|
83
|
+
"notes" TEXT NOT NULL DEFAULT '',
|
|
84
|
+
"order" INTEGER NOT NULL DEFAULT 0,
|
|
85
|
+
"executedById" TEXT,
|
|
86
|
+
"executedAt" TIMESTAMP(3),
|
|
87
|
+
|
|
88
|
+
CONSTRAINT "TestRunEntry_pkey" PRIMARY KEY ("id")
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
-- CreateTable TestCaseHistory
|
|
92
|
+
CREATE TABLE "TestCaseHistory" (
|
|
93
|
+
"id" TEXT NOT NULL,
|
|
94
|
+
"caseId" TEXT NOT NULL,
|
|
95
|
+
"runId" TEXT,
|
|
96
|
+
"reportId" INTEGER,
|
|
97
|
+
"result" TEXT NOT NULL,
|
|
98
|
+
"source" TEXT NOT NULL DEFAULT 'manual',
|
|
99
|
+
"notes" TEXT NOT NULL DEFAULT '',
|
|
100
|
+
"executedById" TEXT,
|
|
101
|
+
"executedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
102
|
+
|
|
103
|
+
CONSTRAINT "TestCaseHistory_pkey" PRIMARY KEY ("id")
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
-- CreateIndex
|
|
107
|
+
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
|
108
|
+
CREATE UNIQUE INDEX "TestSuite_displayId_key" ON "TestSuite"("displayId");
|
|
109
|
+
CREATE UNIQUE INDEX "TestCase_displayId_key" ON "TestCase"("displayId");
|
|
110
|
+
|
|
111
|
+
-- AddForeignKey
|
|
112
|
+
ALTER TABLE "TestSuite" ADD CONSTRAINT "TestSuite_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
113
|
+
|
|
114
|
+
-- AddForeignKey
|
|
115
|
+
ALTER TABLE "TestCase" ADD CONSTRAINT "TestCase_suiteId_fkey" FOREIGN KEY ("suiteId") REFERENCES "TestSuite"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
116
|
+
ALTER TABLE "TestCase" ADD CONSTRAINT "TestCase_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
117
|
+
|
|
118
|
+
-- AddForeignKey
|
|
119
|
+
ALTER TABLE "TestStep" ADD CONSTRAINT "TestStep_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "TestCase"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
120
|
+
|
|
121
|
+
-- AddForeignKey
|
|
122
|
+
ALTER TABLE "TestRun" ADD CONSTRAINT "TestRun_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
123
|
+
|
|
124
|
+
-- AddForeignKey
|
|
125
|
+
ALTER TABLE "TestRunEntry" ADD CONSTRAINT "TestRunEntry_runId_fkey" FOREIGN KEY ("runId") REFERENCES "TestRun"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
126
|
+
ALTER TABLE "TestRunEntry" ADD CONSTRAINT "TestRunEntry_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "TestCase"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
127
|
+
ALTER TABLE "TestRunEntry" ADD CONSTRAINT "TestRunEntry_executedById_fkey" FOREIGN KEY ("executedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
128
|
+
|
|
129
|
+
-- AddForeignKey
|
|
130
|
+
ALTER TABLE "TestCaseHistory" ADD CONSTRAINT "TestCaseHistory_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "TestCase"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
131
|
+
ALTER TABLE "TestCaseHistory" ADD CONSTRAINT "TestCaseHistory_runId_fkey" FOREIGN KEY ("runId") REFERENCES "TestRun"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
132
|
+
ALTER TABLE "TestCaseHistory" ADD CONSTRAINT "TestCaseHistory_reportId_fkey" FOREIGN KEY ("reportId") REFERENCES "Report"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
133
|
+
ALTER TABLE "TestCaseHistory" ADD CONSTRAINT "TestCaseHistory_executedById_fkey" FOREIGN KEY ("executedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- AlterTable Project: add webhook and public URL fields
|
|
2
|
+
ALTER TABLE "Project" ADD COLUMN "discordWebhookUrl" TEXT NOT NULL DEFAULT '';
|
|
3
|
+
ALTER TABLE "Project" ADD COLUMN "slackWebhookUrl" TEXT NOT NULL DEFAULT '';
|
|
4
|
+
ALTER TABLE "Project" ADD COLUMN "notifyPublicUrl" TEXT NOT NULL DEFAULT '';
|
|
5
|
+
|
|
6
|
+
-- AlterTable CronJob: add notification toggles
|
|
7
|
+
ALTER TABLE "CronJob" ADD COLUMN "notifyDiscord" BOOLEAN NOT NULL DEFAULT false;
|
|
8
|
+
ALTER TABLE "CronJob" ADD COLUMN "notifySlack" BOOLEAN NOT NULL DEFAULT false;
|
|
@@ -44,6 +44,8 @@ model CronJob {
|
|
|
44
44
|
enabled Boolean @default(true)
|
|
45
45
|
runnerId String?
|
|
46
46
|
runnerIds String @default("built-in")
|
|
47
|
+
notifyDiscord Boolean @default(false)
|
|
48
|
+
notifySlack Boolean @default(false)
|
|
47
49
|
runner Runner? @relation(fields: [runnerId], references: [id], onDelete: SetNull)
|
|
48
50
|
createdAt DateTime @default(now())
|
|
49
51
|
updatedAt DateTime @updatedAt
|
|
@@ -51,23 +53,134 @@ model CronJob {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
model Report {
|
|
54
|
-
id Int
|
|
56
|
+
id Int @id @default(autoincrement())
|
|
55
57
|
status String
|
|
56
58
|
tags String
|
|
57
|
-
triggerType String
|
|
58
|
-
runners Int
|
|
59
|
-
browser String
|
|
59
|
+
triggerType String @default("manual-trigger")
|
|
60
|
+
runners Int @default(1)
|
|
61
|
+
browser String @default("chromium")
|
|
60
62
|
runnerName String?
|
|
61
63
|
runnerId String?
|
|
62
|
-
runner Runner?
|
|
64
|
+
runner Runner? @relation(fields: [runnerId], references: [id], onDelete: SetNull)
|
|
63
65
|
cronJobId Int?
|
|
64
|
-
cronJob CronJob?
|
|
66
|
+
cronJob CronJob? @relation(fields: [cronJobId], references: [id], onDelete: SetNull)
|
|
65
67
|
content Json
|
|
66
|
-
createdAt DateTime
|
|
68
|
+
createdAt DateTime @default(now())
|
|
69
|
+
testHistory TestCaseHistory[]
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
model Project {
|
|
70
|
-
id
|
|
71
|
-
name
|
|
72
|
-
logoUrl
|
|
73
|
+
id Int @id @default(autoincrement())
|
|
74
|
+
name String @default("")
|
|
75
|
+
logoUrl String @default("")
|
|
76
|
+
testCasePrefix String @default("TC")
|
|
77
|
+
testSuitePrefix String @default("TS")
|
|
78
|
+
caseSeqNext Int @default(0)
|
|
79
|
+
suiteSeqNext Int @default(0)
|
|
80
|
+
discordWebhookUrl String @default("")
|
|
81
|
+
slackWebhookUrl String @default("")
|
|
82
|
+
notifyPublicUrl String @default("")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
model User {
|
|
86
|
+
id String @id @default(cuid())
|
|
87
|
+
name String
|
|
88
|
+
email String @unique
|
|
89
|
+
password String
|
|
90
|
+
role String @default("user")
|
|
91
|
+
createdAt DateTime @default(now())
|
|
92
|
+
updatedAt DateTime @updatedAt
|
|
93
|
+
|
|
94
|
+
suites TestSuite[]
|
|
95
|
+
cases TestCase[]
|
|
96
|
+
runs TestRun[]
|
|
97
|
+
executedEntries TestRunEntry[] @relation("entryExecutor")
|
|
98
|
+
assignedEntries TestRunEntry[] @relation("entryAssignee")
|
|
99
|
+
caseHistories TestCaseHistory[]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
model TestSuite {
|
|
103
|
+
id String @id @default(cuid())
|
|
104
|
+
displayId String @unique
|
|
105
|
+
name String
|
|
106
|
+
description String @default("")
|
|
107
|
+
priority String @default("Medium")
|
|
108
|
+
createdById String
|
|
109
|
+
createdBy User @relation(fields: [createdById], references: [id])
|
|
110
|
+
createdAt DateTime @default(now())
|
|
111
|
+
updatedAt DateTime @updatedAt
|
|
112
|
+
cases TestCase[]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
model TestCase {
|
|
116
|
+
id String @id @default(cuid())
|
|
117
|
+
displayId String @unique
|
|
118
|
+
title String
|
|
119
|
+
description String @default("")
|
|
120
|
+
priority String @default("Medium")
|
|
121
|
+
isAutomated Boolean @default(false)
|
|
122
|
+
suiteId String
|
|
123
|
+
suite TestSuite @relation(fields: [suiteId], references: [id], onDelete: Cascade)
|
|
124
|
+
createdById String
|
|
125
|
+
createdBy User @relation(fields: [createdById], references: [id])
|
|
126
|
+
createdAt DateTime @default(now())
|
|
127
|
+
updatedAt DateTime @updatedAt
|
|
128
|
+
steps TestStep[]
|
|
129
|
+
runEntries TestRunEntry[]
|
|
130
|
+
history TestCaseHistory[]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
model TestStep {
|
|
134
|
+
id String @id @default(cuid())
|
|
135
|
+
caseId String
|
|
136
|
+
case TestCase @relation(fields: [caseId], references: [id], onDelete: Cascade)
|
|
137
|
+
action String
|
|
138
|
+
testData String @default("")
|
|
139
|
+
expectedOutput String @default("")
|
|
140
|
+
order Int
|
|
141
|
+
createdAt DateTime @default(now())
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
model TestRun {
|
|
145
|
+
id String @id @default(cuid())
|
|
146
|
+
title String
|
|
147
|
+
status String @default("backlog")
|
|
148
|
+
createdById String
|
|
149
|
+
createdBy User @relation(fields: [createdById], references: [id])
|
|
150
|
+
createdAt DateTime @default(now())
|
|
151
|
+
updatedAt DateTime @updatedAt
|
|
152
|
+
entries TestRunEntry[]
|
|
153
|
+
history TestCaseHistory[]
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
model TestRunEntry {
|
|
157
|
+
id String @id @default(cuid())
|
|
158
|
+
runId String
|
|
159
|
+
run TestRun @relation(fields: [runId], references: [id], onDelete: Cascade)
|
|
160
|
+
caseId String
|
|
161
|
+
case TestCase @relation(fields: [caseId], references: [id], onDelete: Cascade)
|
|
162
|
+
status String @default("pending")
|
|
163
|
+
notes String @default("")
|
|
164
|
+
order Int @default(0)
|
|
165
|
+
executedById String?
|
|
166
|
+
executedBy User? @relation("entryExecutor", fields: [executedById], references: [id])
|
|
167
|
+
executedAt DateTime?
|
|
168
|
+
assignedToId String?
|
|
169
|
+
assignedTo User? @relation("entryAssignee", fields: [assignedToId], references: [id])
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
model TestCaseHistory {
|
|
173
|
+
id String @id @default(cuid())
|
|
174
|
+
caseId String
|
|
175
|
+
case TestCase @relation(fields: [caseId], references: [id], onDelete: Cascade)
|
|
176
|
+
runId String?
|
|
177
|
+
run TestRun? @relation(fields: [runId], references: [id], onDelete: SetNull)
|
|
178
|
+
reportId Int?
|
|
179
|
+
report Report? @relation(fields: [reportId], references: [id], onDelete: SetNull)
|
|
180
|
+
result String
|
|
181
|
+
source String @default("manual")
|
|
182
|
+
notes String @default("")
|
|
183
|
+
executedById String?
|
|
184
|
+
executedBy User? @relation(fields: [executedById], references: [id])
|
|
185
|
+
executedAt DateTime @default(now())
|
|
73
186
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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 express = require('express');
|
|
19
|
+
const router = express.Router();
|
|
20
|
+
const userService = require('../services/userService');
|
|
21
|
+
const { jwtAuth } = require('../middleware/jwtAuth');
|
|
22
|
+
|
|
23
|
+
router.get('/needs-setup', async (req, res, next) => {
|
|
24
|
+
try {
|
|
25
|
+
const setup = await userService.needsSetup();
|
|
26
|
+
res.json({ needsSetup: setup });
|
|
27
|
+
} catch (e) {
|
|
28
|
+
next(e);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
router.post('/setup', async (req, res, next) => {
|
|
33
|
+
try {
|
|
34
|
+
const setup = await userService.needsSetup();
|
|
35
|
+
if (!setup) return res.status(403).json({ error: 'Setup already complete' });
|
|
36
|
+
const { name, email, password } = req.body;
|
|
37
|
+
if (!name || !email || !password)
|
|
38
|
+
return res.status(400).json({ error: 'name, email and password are required' });
|
|
39
|
+
const user = await userService.createUser({ name, email, password, role: 'admin' });
|
|
40
|
+
res.status(201).json({ user });
|
|
41
|
+
} catch (e) {
|
|
42
|
+
next(e);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
router.post('/login', async (req, res, next) => {
|
|
47
|
+
try {
|
|
48
|
+
const { email, password } = req.body;
|
|
49
|
+
if (!email || !password)
|
|
50
|
+
return res.status(400).json({ error: 'email and password are required' });
|
|
51
|
+
const result = await userService.login({ email, password });
|
|
52
|
+
if (!result) return res.status(401).json({ error: 'Invalid credentials' });
|
|
53
|
+
res.json(result);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
next(e);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
router.get('/me', jwtAuth, async (req, res, next) => {
|
|
60
|
+
try {
|
|
61
|
+
const user = await userService.getById(req.user.userId);
|
|
62
|
+
if (!user) return res.status(404).json({ error: 'User not found' });
|
|
63
|
+
res.json({ user });
|
|
64
|
+
} catch (e) {
|
|
65
|
+
next(e);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
router.post('/change-password', jwtAuth, async (req, res, next) => {
|
|
70
|
+
try {
|
|
71
|
+
const { currentPassword, newPassword } = req.body;
|
|
72
|
+
if (!currentPassword || !newPassword)
|
|
73
|
+
return res.status(400).json({ error: 'currentPassword and newPassword are required' });
|
|
74
|
+
const result = await userService.updatePassword(req.user.userId, {
|
|
75
|
+
currentPassword,
|
|
76
|
+
newPassword
|
|
77
|
+
});
|
|
78
|
+
if (!result.ok) return res.status(400).json({ error: result.error });
|
|
79
|
+
res.json({ ok: true });
|
|
80
|
+
} catch (e) {
|
|
81
|
+
next(e);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
router.put('/update-profile', jwtAuth, async (req, res, next) => {
|
|
86
|
+
try {
|
|
87
|
+
const { name, email } = req.body;
|
|
88
|
+
const result = await userService.updateProfile(req.user.userId, { name, email });
|
|
89
|
+
if (!result.ok) return res.status(400).json({ error: result.error });
|
|
90
|
+
res.json({ user: result.user });
|
|
91
|
+
} catch (e) {
|
|
92
|
+
next(e);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
module.exports = router;
|
|
@@ -34,6 +34,15 @@ router.get('/ping', authGuard, (req, res) => {
|
|
|
34
34
|
res.json({ ok: true, mode: process.env.PLUM_MODE || 'server' });
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
// Graceful shutdown for node-mode processes (no-op on the primary server)
|
|
38
|
+
router.post('/shutdown', authGuard, (req, res) => {
|
|
39
|
+
if (process.env.PLUM_MODE !== 'node') {
|
|
40
|
+
return res.status(403).json({ error: 'Not a node runner' });
|
|
41
|
+
}
|
|
42
|
+
res.json({ ok: true });
|
|
43
|
+
setTimeout(() => process.exit(0), 200);
|
|
44
|
+
});
|
|
45
|
+
|
|
37
46
|
// Start a remote test job
|
|
38
47
|
router.post('/execute', authGuard, (req, res) => {
|
|
39
48
|
const { tags, browser = 'chromium', workers = 1, tests = null } = req.body;
|
|
@@ -64,6 +64,16 @@ router.put('/:id', async (req, res) => {
|
|
|
64
64
|
|
|
65
65
|
router.delete('/:id', async (req, res) => {
|
|
66
66
|
try {
|
|
67
|
+
const runner = await runnerService.getById(req.params.id);
|
|
68
|
+
if (runner) {
|
|
69
|
+
try {
|
|
70
|
+
await fetch(`${runner.url}/api/shutdown`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { Authorization: `Bearer ${runner.token}` },
|
|
73
|
+
signal: AbortSignal.timeout(3000)
|
|
74
|
+
});
|
|
75
|
+
} catch {}
|
|
76
|
+
}
|
|
67
77
|
await runnerService.remove(req.params.id);
|
|
68
78
|
res.json({ message: 'Runner deleted' });
|
|
69
79
|
} catch (e) {
|
|
@@ -18,25 +18,88 @@
|
|
|
18
18
|
const express = require('express');
|
|
19
19
|
const router = express.Router();
|
|
20
20
|
const settingsService = require('../services/settingsService');
|
|
21
|
+
const testSuiteService = require('../services/testSuiteService');
|
|
22
|
+
const testCaseService = require('../services/testCaseService');
|
|
23
|
+
const { jwtAuth } = require('../middleware/jwtAuth');
|
|
21
24
|
|
|
22
|
-
router.get('/project', async (req, res) => {
|
|
25
|
+
router.get('/project', async (req, res, next) => {
|
|
23
26
|
try {
|
|
24
27
|
const project = await settingsService.getProject();
|
|
25
28
|
res.json(project);
|
|
26
|
-
} catch (
|
|
27
|
-
|
|
28
|
-
res.status(500).json({ error: 'Failed to fetch project settings' });
|
|
29
|
+
} catch (e) {
|
|
30
|
+
next(e);
|
|
29
31
|
}
|
|
30
32
|
});
|
|
31
33
|
|
|
32
|
-
router.post('/project', async (req, res) => {
|
|
34
|
+
router.post('/project', async (req, res, next) => {
|
|
33
35
|
try {
|
|
34
36
|
const { name, logoUrl } = req.body;
|
|
35
37
|
const project = await settingsService.updateProject({ name, logoUrl });
|
|
36
38
|
res.json(project);
|
|
37
|
-
} catch (
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
} catch (e) {
|
|
40
|
+
next(e);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
router.get('/test-prefixes', jwtAuth, async (req, res, next) => {
|
|
45
|
+
try {
|
|
46
|
+
const prefixes = await settingsService.getTestPrefixes();
|
|
47
|
+
res.json(prefixes);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
next(e);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
router.post('/test-prefixes', jwtAuth, async (req, res, next) => {
|
|
54
|
+
try {
|
|
55
|
+
const { testCasePrefix, testSuitePrefix } = req.body;
|
|
56
|
+
const project = await settingsService.updateTestPrefixes({ testCasePrefix, testSuitePrefix });
|
|
57
|
+
res.json(project);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
next(e);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
router.post('/test-prefixes/migrate', jwtAuth, async (req, res, next) => {
|
|
64
|
+
try {
|
|
65
|
+
const { testCasePrefix, testSuitePrefix } = req.body;
|
|
66
|
+
const results = {};
|
|
67
|
+
if (testCasePrefix) {
|
|
68
|
+
results.cases = await testCaseService.migratePrefix(testCasePrefix);
|
|
69
|
+
}
|
|
70
|
+
if (testSuitePrefix) {
|
|
71
|
+
results.suites = await testSuiteService.migratePrefix(testSuitePrefix);
|
|
72
|
+
}
|
|
73
|
+
res.json({ ok: true, ...results });
|
|
74
|
+
} catch (e) {
|
|
75
|
+
next(e);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
router.get('/integrations', async (req, res, next) => {
|
|
80
|
+
try {
|
|
81
|
+
const webhooks = await settingsService.getWebhooks();
|
|
82
|
+
res.json(webhooks);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
next(e);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
router.post('/integrations', async (req, res, next) => {
|
|
89
|
+
try {
|
|
90
|
+
const { discordWebhookUrl, slackWebhookUrl, notifyPublicUrl } = req.body;
|
|
91
|
+
const project = await settingsService.updateWebhooks({
|
|
92
|
+
discordWebhookUrl,
|
|
93
|
+
slackWebhookUrl,
|
|
94
|
+
notifyPublicUrl
|
|
95
|
+
});
|
|
96
|
+
res.json({
|
|
97
|
+
discordWebhookUrl: project.discordWebhookUrl,
|
|
98
|
+
slackWebhookUrl: project.slackWebhookUrl,
|
|
99
|
+
notifyPublicUrl: project.notifyPublicUrl
|
|
100
|
+
});
|
|
101
|
+
} catch (e) {
|
|
102
|
+
next(e);
|
|
40
103
|
}
|
|
41
104
|
});
|
|
42
105
|
|
|
@@ -0,0 +1,80 @@
|
|
|
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 express = require('express');
|
|
19
|
+
const router = express.Router();
|
|
20
|
+
const { jwtAuth } = require('../middleware/jwtAuth');
|
|
21
|
+
const testCaseService = require('../services/testCaseService');
|
|
22
|
+
|
|
23
|
+
router.get('/:id', jwtAuth, async (req, res, next) => {
|
|
24
|
+
try {
|
|
25
|
+
const tc = await testCaseService.getById(req.params.id);
|
|
26
|
+
if (!tc) return res.status(404).json({ error: 'Test case not found' });
|
|
27
|
+
res.json({ testCase: tc });
|
|
28
|
+
} catch (e) {
|
|
29
|
+
next(e);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
router.post('/', jwtAuth, async (req, res, next) => {
|
|
34
|
+
try {
|
|
35
|
+
const { suiteId, title, description, priority } = req.body;
|
|
36
|
+
if (!suiteId || !title)
|
|
37
|
+
return res.status(400).json({ error: 'suiteId and title are required' });
|
|
38
|
+
const testCase = await testCaseService.create({
|
|
39
|
+
suiteId,
|
|
40
|
+
title,
|
|
41
|
+
description,
|
|
42
|
+
priority,
|
|
43
|
+
createdById: req.user.userId
|
|
44
|
+
});
|
|
45
|
+
res.status(201).json({ testCase });
|
|
46
|
+
} catch (e) {
|
|
47
|
+
next(e);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
router.put('/:id', jwtAuth, async (req, res, next) => {
|
|
52
|
+
try {
|
|
53
|
+
const { title, description, priority } = req.body;
|
|
54
|
+
const testCase = await testCaseService.update(req.params.id, { title, description, priority });
|
|
55
|
+
res.json({ testCase });
|
|
56
|
+
} catch (e) {
|
|
57
|
+
next(e);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
router.put('/:id/steps', jwtAuth, async (req, res, next) => {
|
|
62
|
+
try {
|
|
63
|
+
const { steps } = req.body;
|
|
64
|
+
const saved = await testCaseService.upsertSteps(req.params.id, steps);
|
|
65
|
+
res.json({ steps: saved });
|
|
66
|
+
} catch (e) {
|
|
67
|
+
next(e);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
router.delete('/:id', jwtAuth, async (req, res, next) => {
|
|
72
|
+
try {
|
|
73
|
+
await testCaseService.remove(req.params.id);
|
|
74
|
+
res.json({ ok: true });
|
|
75
|
+
} catch (e) {
|
|
76
|
+
next(e);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
module.exports = router;
|