plum-e2e 1.3.7 → 2.1.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 +61 -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/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/schema.prisma +118 -10
- package/backend/routes/auth.routes.js +96 -0
- package/backend/routes/settings.routes.js +44 -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/services/reportService.js +96 -4
- package/backend/services/settingsService.js +18 -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 +19 -6
- 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/users.js +52 -0
- package/frontend/src/lib/components/layout/Nav.svelte +116 -4
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +243 -29
- 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 +9 -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/settings/+page.svelte +586 -5
- 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,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;
|
|
@@ -18,25 +18,61 @@
|
|
|
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);
|
|
40
76
|
}
|
|
41
77
|
});
|
|
42
78
|
|
|
@@ -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;
|
|
@@ -0,0 +1,122 @@
|
|
|
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 testRunService = require('../services/testRunService');
|
|
22
|
+
|
|
23
|
+
router.get('/', jwtAuth, async (req, res, next) => {
|
|
24
|
+
try {
|
|
25
|
+
const page = Math.max(1, parseInt(req.query.page) || 1);
|
|
26
|
+
const limit = Math.min(200, Math.max(1, parseInt(req.query.limit) || 20));
|
|
27
|
+
const { q, sortBy, sortOrder } = req.query;
|
|
28
|
+
const result = await testRunService.getAll({ page, limit, q, sortBy, sortOrder });
|
|
29
|
+
res.json(result);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
next(e);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
router.get('/:id', jwtAuth, async (req, res, next) => {
|
|
36
|
+
try {
|
|
37
|
+
const run = await testRunService.getById(req.params.id);
|
|
38
|
+
if (!run) return res.status(404).json({ error: 'Test run not found' });
|
|
39
|
+
res.json({ run });
|
|
40
|
+
} catch (e) {
|
|
41
|
+
next(e);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
router.post('/', jwtAuth, async (req, res, next) => {
|
|
46
|
+
try {
|
|
47
|
+
const { title, caseIds } = req.body;
|
|
48
|
+
if (!title) return res.status(400).json({ error: 'title is required' });
|
|
49
|
+
const run = await testRunService.create({ title, caseIds, createdById: req.user.userId });
|
|
50
|
+
res.status(201).json({ run });
|
|
51
|
+
} catch (e) {
|
|
52
|
+
next(e);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
router.put('/:id', jwtAuth, async (req, res, next) => {
|
|
57
|
+
try {
|
|
58
|
+
const { title, status, caseIds } = req.body;
|
|
59
|
+
const run = await testRunService.update(req.params.id, { title, status, caseIds });
|
|
60
|
+
res.json({ run });
|
|
61
|
+
} catch (e) {
|
|
62
|
+
next(e);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
router.post('/:id/duplicate', jwtAuth, async (req, res, next) => {
|
|
67
|
+
try {
|
|
68
|
+
const run = await testRunService.duplicate(req.params.id, { createdById: req.user.userId });
|
|
69
|
+
if (!run) return res.status(404).json({ error: 'Test run not found' });
|
|
70
|
+
res.status(201).json({ run });
|
|
71
|
+
} catch (e) {
|
|
72
|
+
next(e);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
router.delete('/:id', jwtAuth, async (req, res, next) => {
|
|
77
|
+
try {
|
|
78
|
+
await testRunService.remove(req.params.id);
|
|
79
|
+
res.json({ ok: true });
|
|
80
|
+
} catch (e) {
|
|
81
|
+
next(e);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
router.put('/entries/:entryId/assign', jwtAuth, async (req, res, next) => {
|
|
86
|
+
try {
|
|
87
|
+
const { userId } = req.body;
|
|
88
|
+
const entry = await testRunService.assignEntry(req.params.entryId, { userId: userId ?? null });
|
|
89
|
+
res.json({ entry });
|
|
90
|
+
} catch (e) {
|
|
91
|
+
next(e);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
router.post('/entries/:entryId/result', jwtAuth, async (req, res, next) => {
|
|
96
|
+
try {
|
|
97
|
+
const { status, notes } = req.body;
|
|
98
|
+
if (!status) return res.status(400).json({ error: 'status is required' });
|
|
99
|
+
const entry = await testRunService.updateEntry(req.params.entryId, {
|
|
100
|
+
status,
|
|
101
|
+
notes,
|
|
102
|
+
executedById: req.user.userId
|
|
103
|
+
});
|
|
104
|
+
res.json({ entry });
|
|
105
|
+
} catch (e) {
|
|
106
|
+
next(e);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
router.post('/:id/reorder', jwtAuth, async (req, res, next) => {
|
|
111
|
+
try {
|
|
112
|
+
const { entryIds } = req.body;
|
|
113
|
+
if (!Array.isArray(entryIds))
|
|
114
|
+
return res.status(400).json({ error: 'entryIds must be an array' });
|
|
115
|
+
await testRunService.reorderEntries(req.params.id, entryIds);
|
|
116
|
+
res.json({ ok: true });
|
|
117
|
+
} catch (e) {
|
|
118
|
+
next(e);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
module.exports = router;
|
|
@@ -0,0 +1,92 @@
|
|
|
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 testSuiteService = require('../services/testSuiteService');
|
|
22
|
+
|
|
23
|
+
router.get('/', jwtAuth, async (req, res, next) => {
|
|
24
|
+
try {
|
|
25
|
+
if (req.query.withCases === 'true') {
|
|
26
|
+
const suites = await testSuiteService.getAllWithCases();
|
|
27
|
+
return res.json({ suites });
|
|
28
|
+
}
|
|
29
|
+
if (req.query.q) {
|
|
30
|
+
const results = await testSuiteService.search(req.query.q);
|
|
31
|
+
return res.json(results);
|
|
32
|
+
}
|
|
33
|
+
const page = Math.max(1, parseInt(req.query.page) || 1);
|
|
34
|
+
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || 20));
|
|
35
|
+
const { sortBy, sortOrder } = req.query;
|
|
36
|
+
const result = await testSuiteService.getAll({ page, limit, sortBy, sortOrder });
|
|
37
|
+
res.json(result);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
next(e);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
router.get('/:id', jwtAuth, async (req, res, next) => {
|
|
44
|
+
try {
|
|
45
|
+
const suite = await testSuiteService.getById(req.params.id);
|
|
46
|
+
if (!suite) return res.status(404).json({ error: 'Suite not found' });
|
|
47
|
+
res.json({ suite });
|
|
48
|
+
} catch (e) {
|
|
49
|
+
next(e);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
router.post('/', jwtAuth, async (req, res, next) => {
|
|
54
|
+
try {
|
|
55
|
+
const { name, description, priority } = req.body;
|
|
56
|
+
if (!name) return res.status(400).json({ error: 'name is required' });
|
|
57
|
+
const suite = await testSuiteService.create({
|
|
58
|
+
name,
|
|
59
|
+
description,
|
|
60
|
+
priority,
|
|
61
|
+
createdById: req.user.userId
|
|
62
|
+
});
|
|
63
|
+
res.status(201).json({ suite });
|
|
64
|
+
} catch (e) {
|
|
65
|
+
next(e);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
router.put('/:id', jwtAuth, async (req, res, next) => {
|
|
70
|
+
try {
|
|
71
|
+
const { name, description, priority } = req.body;
|
|
72
|
+
const suite = await testSuiteService.update(req.params.id, {
|
|
73
|
+
name,
|
|
74
|
+
description,
|
|
75
|
+
priority
|
|
76
|
+
});
|
|
77
|
+
res.json({ suite });
|
|
78
|
+
} catch (e) {
|
|
79
|
+
next(e);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
router.delete('/:id', jwtAuth, async (req, res, next) => {
|
|
84
|
+
try {
|
|
85
|
+
await testSuiteService.remove(req.params.id);
|
|
86
|
+
res.json({ ok: true });
|
|
87
|
+
} catch (e) {
|
|
88
|
+
next(e);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
module.exports = router;
|
|
@@ -0,0 +1,67 @@
|
|
|
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 { requireAdmin } = require('../middleware/requireAdmin');
|
|
22
|
+
const userService = require('../services/userService');
|
|
23
|
+
|
|
24
|
+
router.get('/members', jwtAuth, async (req, res, next) => {
|
|
25
|
+
try {
|
|
26
|
+
const users = await userService.getMembers();
|
|
27
|
+
res.json({ users });
|
|
28
|
+
} catch (e) {
|
|
29
|
+
next(e);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
router.get('/', jwtAuth, requireAdmin, async (req, res, next) => {
|
|
34
|
+
try {
|
|
35
|
+
const users = await userService.getAll();
|
|
36
|
+
res.json({ users });
|
|
37
|
+
} catch (e) {
|
|
38
|
+
next(e);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
router.post('/', jwtAuth, requireAdmin, async (req, res, next) => {
|
|
43
|
+
try {
|
|
44
|
+
const { name, email, password, role = 'user' } = req.body;
|
|
45
|
+
if (!name || !email || !password)
|
|
46
|
+
return res.status(400).json({ error: 'name, email and password are required' });
|
|
47
|
+
if (!['admin', 'user'].includes(role))
|
|
48
|
+
return res.status(400).json({ error: 'role must be admin or user' });
|
|
49
|
+
const user = await userService.createUser({ name, email, password, role });
|
|
50
|
+
res.status(201).json({ user });
|
|
51
|
+
} catch (e) {
|
|
52
|
+
next(e);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
router.delete('/:id', jwtAuth, requireAdmin, async (req, res, next) => {
|
|
57
|
+
try {
|
|
58
|
+
if (req.params.id === req.user.userId)
|
|
59
|
+
return res.status(400).json({ error: 'Cannot delete your own account' });
|
|
60
|
+
await userService.deleteUser(req.params.id);
|
|
61
|
+
res.json({ ok: true });
|
|
62
|
+
} catch (e) {
|
|
63
|
+
next(e);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
module.exports = router;
|
|
@@ -64,12 +64,13 @@ async function main() {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const pascal = toPascalCase(rawName);
|
|
67
|
+
const base = pascal.endsWith('Page') ? pascal.slice(0, -4) : pascal;
|
|
67
68
|
const kebab = toKebabCase(pascal);
|
|
68
69
|
const suiteTag = `@suite-${kebab}`;
|
|
69
70
|
const testTag = `@test-${kebab}-1`;
|
|
70
71
|
|
|
71
72
|
const featurePath = path.join(testsRoot, 'features', `${pascal}.feature`);
|
|
72
|
-
const pagePath = path.join(testsRoot, 'pages', `${
|
|
73
|
+
const pagePath = path.join(testsRoot, 'pages', `${base}Page.ts`);
|
|
73
74
|
const stepsPath = path.join(testsRoot, 'step_definitions', `${pascal}Steps.ts`);
|
|
74
75
|
|
|
75
76
|
const conflicts = [featurePath, pagePath, stepsPath].filter(fs.existsSync);
|
|
@@ -95,7 +96,7 @@ Feature: ${pascal}
|
|
|
95
96
|
|
|
96
97
|
const page = `import { page } from '../utils/browser';
|
|
97
98
|
|
|
98
|
-
export class ${
|
|
99
|
+
export class ${base}Page {
|
|
99
100
|
static async goTo() {
|
|
100
101
|
await page().goto(process.env.BASE_URL as string);
|
|
101
102
|
}
|
|
@@ -111,18 +112,18 @@ export class ${pascal}Page {
|
|
|
111
112
|
`;
|
|
112
113
|
|
|
113
114
|
const steps = `import { Given, When, Then } from '@cucumber/cucumber';
|
|
114
|
-
import { ${
|
|
115
|
+
import { ${base}Page } from '../pages/${base}Page';
|
|
115
116
|
|
|
116
117
|
Given('I am on the ${pascal} page', async () => {
|
|
117
|
-
await ${
|
|
118
|
+
await ${base}Page.goTo();
|
|
118
119
|
});
|
|
119
120
|
|
|
120
121
|
When('I perform an action', async () => {
|
|
121
|
-
await ${
|
|
122
|
+
await ${base}Page.performAction();
|
|
122
123
|
});
|
|
123
124
|
|
|
124
125
|
Then('I should see the expected result', async () => {
|
|
125
|
-
await ${
|
|
126
|
+
await ${base}Page.verifyResult();
|
|
126
127
|
});
|
|
127
128
|
`;
|
|
128
129
|
|