jira-pat 1.0.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/AGENTS.md +218 -0
- package/README.md +64 -0
- package/backend/.env.example +1 -0
- package/backend/__tests__/getJiraClient.test.js +57 -0
- package/backend/__tests__/issues.test.js +565 -0
- package/backend/__tests__/jiraService.test.js +1127 -0
- package/backend/__tests__/projects.test.js +256 -0
- package/backend/coverage/clover.xml +426 -0
- package/backend/coverage/coverage-final.json +4 -0
- package/backend/coverage/lcov-report/base.css +224 -0
- package/backend/coverage/lcov-report/block-navigation.js +87 -0
- package/backend/coverage/lcov-report/favicon.png +0 -0
- package/backend/coverage/lcov-report/index.html +131 -0
- package/backend/coverage/lcov-report/prettify.css +1 -0
- package/backend/coverage/lcov-report/prettify.js +2 -0
- package/backend/coverage/lcov-report/routes/index.html +131 -0
- package/backend/coverage/lcov-report/routes/issues.js.html +823 -0
- package/backend/coverage/lcov-report/routes/projects.js.html +190 -0
- package/backend/coverage/lcov-report/service/index.html +116 -0
- package/backend/coverage/lcov-report/service/jiraService.js.html +1663 -0
- package/backend/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/backend/coverage/lcov-report/sorter.js +210 -0
- package/backend/coverage/lcov.info +707 -0
- package/backend/index.js +38 -0
- package/backend/jest.config.js +11 -0
- package/backend/package-lock.json +5636 -0
- package/backend/package.json +28 -0
- package/backend/routes/issues.js +246 -0
- package/backend/routes/projects.js +35 -0
- package/backend/service/jiraService.js +526 -0
- package/bin/jira.js +92 -0
- package/frontend/.env.example +1 -0
- package/frontend/coverage/base.css +224 -0
- package/frontend/coverage/block-navigation.js +87 -0
- package/frontend/coverage/clover.xml +559 -0
- package/frontend/coverage/components/CreateIssueModal.jsx.html +592 -0
- package/frontend/coverage/components/IssueDetailPanel.jsx.html +1633 -0
- package/frontend/coverage/components/IssueDrawer.jsx.html +550 -0
- package/frontend/coverage/components/IssueTable.jsx.html +571 -0
- package/frontend/coverage/components/SkeletonComponents.jsx.html +223 -0
- package/frontend/coverage/components/ToastContainer.jsx.html +142 -0
- package/frontend/coverage/components/index.html +191 -0
- package/frontend/coverage/coverage-final.json +14 -0
- package/frontend/coverage/favicon.png +0 -0
- package/frontend/coverage/hooks/index.html +161 -0
- package/frontend/coverage/hooks/useFocusTrap.js.html +262 -0
- package/frontend/coverage/hooks/useIssueDrawer.js.html +1000 -0
- package/frontend/coverage/hooks/useIssuesList.js.html +175 -0
- package/frontend/coverage/hooks/useToasts.js.html +142 -0
- package/frontend/coverage/index.html +161 -0
- package/frontend/coverage/prettify.css +1 -0
- package/frontend/coverage/prettify.js +2 -0
- package/frontend/coverage/services/api.js.html +547 -0
- package/frontend/coverage/services/index.html +116 -0
- package/frontend/coverage/sort-arrow-sprite.png +0 -0
- package/frontend/coverage/sorter.js +210 -0
- package/frontend/coverage/utils/index.html +131 -0
- package/frontend/coverage/utils/issueHelpers.jsx.html +334 -0
- package/frontend/coverage/utils/sanitize.js.html +166 -0
- package/frontend/index.html +13 -0
- package/frontend/package-lock.json +3436 -0
- package/frontend/package.json +30 -0
- package/frontend/src/App.jsx +447 -0
- package/frontend/src/__tests__/components/CreateIssueModal.test.jsx +375 -0
- package/frontend/src/__tests__/components/IssueDetailPanel.test.jsx +962 -0
- package/frontend/src/__tests__/components/IssueDrawer.test.jsx +240 -0
- package/frontend/src/__tests__/components/IssueTable.test.jsx +423 -0
- package/frontend/src/__tests__/components/ToastContainer.test.jsx +196 -0
- package/frontend/src/__tests__/hooks/useFocusTrap.test.js +197 -0
- package/frontend/src/__tests__/hooks/useIssueDrawer.test.js +1053 -0
- package/frontend/src/__tests__/hooks/useIssuesList.test.js +175 -0
- package/frontend/src/__tests__/hooks/useToasts.test.js +110 -0
- package/frontend/src/__tests__/services/api.test.js +568 -0
- package/frontend/src/__tests__/setup.js +54 -0
- package/frontend/src/__tests__/utils/issueHelpers.test.jsx +336 -0
- package/frontend/src/__tests__/utils/sanitize.test.js +238 -0
- package/frontend/src/components/CreateIssueModal.jsx +169 -0
- package/frontend/src/components/ErrorBoundary.jsx +52 -0
- package/frontend/src/components/IssueDetailPanel.jsx +517 -0
- package/frontend/src/components/IssueDrawer.jsx +155 -0
- package/frontend/src/components/IssueTable.jsx +162 -0
- package/frontend/src/components/SkeletonComponents.jsx +46 -0
- package/frontend/src/components/StandaloneIssuePage.jsx +176 -0
- package/frontend/src/components/ToastContainer.jsx +19 -0
- package/frontend/src/hooks/useFocusTrap.js +59 -0
- package/frontend/src/hooks/useIssueDrawer.js +305 -0
- package/frontend/src/hooks/useIssuesList.js +30 -0
- package/frontend/src/hooks/useToasts.js +19 -0
- package/frontend/src/index.css +2070 -0
- package/frontend/src/main.jsx +13 -0
- package/frontend/src/services/api.js +154 -0
- package/frontend/src/utils/issueHelpers.jsx +84 -0
- package/frontend/src/utils/sanitize.js +27 -0
- package/frontend/vite.config.js +15 -0
- package/package.json +19 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const request = require('supertest');
|
|
3
|
+
|
|
4
|
+
jest.mock('../service/jiraService');
|
|
5
|
+
jest.mock('express-rate-limit', () => {
|
|
6
|
+
return () => (req, res, next) => next();
|
|
7
|
+
});
|
|
8
|
+
const jiraService = require('../service/jiraService');
|
|
9
|
+
|
|
10
|
+
const createApp = () => {
|
|
11
|
+
const app = express();
|
|
12
|
+
app.use(express.json());
|
|
13
|
+
app.use('/api/issues', require('../routes/issues'));
|
|
14
|
+
return app;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe('issues routes', () => {
|
|
18
|
+
let app;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
app = createApp();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('GET /api/issues', () => {
|
|
26
|
+
it('should return issues list', async () => {
|
|
27
|
+
const mockData = {
|
|
28
|
+
issues: [{ key: 'TEST-1', fields: { summary: 'Test Issue' } }],
|
|
29
|
+
total: 1
|
|
30
|
+
};
|
|
31
|
+
jiraService.searchIssues.mockResolvedValue(mockData);
|
|
32
|
+
|
|
33
|
+
const response = await request(app)
|
|
34
|
+
.get('/api/issues')
|
|
35
|
+
.query({ project: 'TEST' });
|
|
36
|
+
|
|
37
|
+
expect(response.status).toBe(200);
|
|
38
|
+
expect(response.body.issues).toHaveLength(1);
|
|
39
|
+
expect(jiraService.searchIssues).toHaveBeenCalledWith(
|
|
40
|
+
expect.objectContaining({ project: 'TEST' })
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should return empty array when no issues found', async () => {
|
|
45
|
+
jiraService.searchIssues.mockResolvedValue({ issues: [], total: 0 });
|
|
46
|
+
|
|
47
|
+
const response = await request(app).get('/api/issues');
|
|
48
|
+
|
|
49
|
+
expect(response.status).toBe(200);
|
|
50
|
+
expect(response.body.issues).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should pass search parameter to service', async () => {
|
|
54
|
+
jiraService.searchIssues.mockResolvedValue({ issues: [], total: 0 });
|
|
55
|
+
|
|
56
|
+
await request(app).get('/api/issues').query({ search: 'test' });
|
|
57
|
+
|
|
58
|
+
expect(jiraService.searchIssues).toHaveBeenCalledWith(
|
|
59
|
+
expect.objectContaining({ search: 'test' })
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should handle Jira API errors', async () => {
|
|
64
|
+
jiraService.searchIssues.mockRejectedValue(new Error('Jira API Error'));
|
|
65
|
+
|
|
66
|
+
const response = await request(app).get('/api/issues');
|
|
67
|
+
|
|
68
|
+
expect(response.status).toBe(500);
|
|
69
|
+
expect(response.body.error).toBe('Failed to fetch issues');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should handle pagination parameters', async () => {
|
|
73
|
+
jiraService.searchIssues.mockResolvedValue({ issues: [], total: 0 });
|
|
74
|
+
|
|
75
|
+
await request(app)
|
|
76
|
+
.get('/api/issues')
|
|
77
|
+
.query({ startAt: '10', maxResults: '25' });
|
|
78
|
+
|
|
79
|
+
expect(jiraService.searchIssues).toHaveBeenCalledWith(
|
|
80
|
+
expect.objectContaining({ startAt: '10', maxResults: '25' })
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('POST /api/issues', () => {
|
|
86
|
+
it('should create issue with valid data', async () => {
|
|
87
|
+
const mockIssue = { key: 'TEST-100', id: '100' };
|
|
88
|
+
jiraService.createIssue.mockResolvedValue(mockIssue);
|
|
89
|
+
|
|
90
|
+
const response = await request(app)
|
|
91
|
+
.post('/api/issues')
|
|
92
|
+
.send({ projectKey: 'TEST', summary: 'New Issue', issueType: '1' });
|
|
93
|
+
|
|
94
|
+
expect(response.status).toBe(201);
|
|
95
|
+
expect(response.body.key).toBe('TEST-100');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should reject missing summary', async () => {
|
|
99
|
+
const response = await request(app)
|
|
100
|
+
.post('/api/issues')
|
|
101
|
+
.send({ projectKey: 'TEST' });
|
|
102
|
+
|
|
103
|
+
expect(response.status).toBe(400);
|
|
104
|
+
expect(response.body.error).toBe('Summary is required');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should reject empty summary', async () => {
|
|
108
|
+
const response = await request(app)
|
|
109
|
+
.post('/api/issues')
|
|
110
|
+
.send({ projectKey: 'TEST', summary: ' ' });
|
|
111
|
+
|
|
112
|
+
expect(response.status).toBe(400);
|
|
113
|
+
expect(response.body.error).toBe('Summary cannot be empty');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should reject summary over 255 characters', async () => {
|
|
117
|
+
const response = await request(app)
|
|
118
|
+
.post('/api/issues')
|
|
119
|
+
.send({ projectKey: 'TEST', summary: 'a'.repeat(256) });
|
|
120
|
+
|
|
121
|
+
expect(response.status).toBe(400);
|
|
122
|
+
expect(response.body.error).toBe('Summary must be under 255 characters');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should trim summary whitespace', async () => {
|
|
126
|
+
jiraService.createIssue.mockResolvedValue({ key: 'TEST-1' });
|
|
127
|
+
|
|
128
|
+
await request(app)
|
|
129
|
+
.post('/api/issues')
|
|
130
|
+
.send({ projectKey: 'TEST', summary: ' Trimmed Summary ' });
|
|
131
|
+
|
|
132
|
+
expect(jiraService.createIssue).toHaveBeenCalledWith(
|
|
133
|
+
expect.objectContaining({ summary: 'Trimmed Summary' })
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should handle API errors when creating issue', async () => {
|
|
138
|
+
jiraService.createIssue.mockRejectedValue(new Error('Create failed'));
|
|
139
|
+
|
|
140
|
+
const response = await request(app)
|
|
141
|
+
.post('/api/issues')
|
|
142
|
+
.send({ projectKey: 'TEST', summary: 'New Issue', issueType: '1' });
|
|
143
|
+
|
|
144
|
+
expect(response.status).toBe(500);
|
|
145
|
+
expect(response.body.error).toBe('Failed to create issue');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('GET /api/issues/labels/search', () => {
|
|
150
|
+
it('should search labels', async () => {
|
|
151
|
+
jiraService.searchLabels.mockResolvedValue(['bug', 'feature']);
|
|
152
|
+
|
|
153
|
+
const response = await request(app)
|
|
154
|
+
.get('/api/issues/labels/search')
|
|
155
|
+
.query({ query: 'bug' });
|
|
156
|
+
|
|
157
|
+
expect(response.status).toBe(200);
|
|
158
|
+
expect(response.body).toEqual(['bug', 'feature']);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should handle errors', async () => {
|
|
162
|
+
jiraService.searchLabels.mockRejectedValue(new Error('Search failed'));
|
|
163
|
+
|
|
164
|
+
const response = await request(app).get('/api/issues/labels/search');
|
|
165
|
+
|
|
166
|
+
expect(response.status).toBe(500);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('GET /api/issues/image/proxy', () => {
|
|
171
|
+
it('should reject missing url', async () => {
|
|
172
|
+
const response = await request(app).get('/api/issues/image/proxy');
|
|
173
|
+
|
|
174
|
+
expect(response.status).toBe(400);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should handle missing content-type header', async () => {
|
|
178
|
+
const mockStream = {
|
|
179
|
+
pipe: jest.fn((res) => {
|
|
180
|
+
res.end();
|
|
181
|
+
return mockStream;
|
|
182
|
+
})
|
|
183
|
+
};
|
|
184
|
+
jiraService.downloadAttachment.mockResolvedValue({
|
|
185
|
+
data: mockStream,
|
|
186
|
+
headers: {}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const response = await request(app)
|
|
190
|
+
.get('/api/issues/image/proxy')
|
|
191
|
+
.query({ url: 'https://example.com/image.png' });
|
|
192
|
+
|
|
193
|
+
expect(response.status).toBe(200);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should relay content-type header when present', async () => {
|
|
197
|
+
const mockStream = {
|
|
198
|
+
pipe: jest.fn((res) => {
|
|
199
|
+
res.end();
|
|
200
|
+
return mockStream;
|
|
201
|
+
})
|
|
202
|
+
};
|
|
203
|
+
jiraService.downloadAttachment.mockResolvedValue({
|
|
204
|
+
data: mockStream,
|
|
205
|
+
headers: { 'content-type': 'image/png' }
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const response = await request(app)
|
|
209
|
+
.get('/api/issues/image/proxy')
|
|
210
|
+
.query({ url: 'https://example.com/image.png' });
|
|
211
|
+
|
|
212
|
+
expect(response.status).toBe(200);
|
|
213
|
+
expect(response.headers['content-type']).toContain('image/png');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should handle errors', async () => {
|
|
217
|
+
jiraService.downloadAttachment.mockRejectedValue(new Error('Download failed'));
|
|
218
|
+
|
|
219
|
+
const response = await request(app)
|
|
220
|
+
.get('/api/issues/image/proxy')
|
|
221
|
+
.query({ url: 'https://example.com/image.png' });
|
|
222
|
+
|
|
223
|
+
expect(response.status).toBe(500);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('GET /api/issues/:issueKey', () => {
|
|
228
|
+
it('should fetch issue details', async () => {
|
|
229
|
+
const mockIssue = { key: 'TEST-1', fields: { summary: 'Test' } };
|
|
230
|
+
jiraService.getIssueDetail.mockResolvedValue(mockIssue);
|
|
231
|
+
|
|
232
|
+
const response = await request(app).get('/api/issues/TEST-1');
|
|
233
|
+
|
|
234
|
+
expect(response.status).toBe(200);
|
|
235
|
+
expect(response.body.key).toBe('TEST-1');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should reject invalid issue key format', async () => {
|
|
239
|
+
const response = await request(app).get('/api/issues/invalid');
|
|
240
|
+
|
|
241
|
+
expect(response.status).toBe(400);
|
|
242
|
+
expect(response.body.error).toContain('Invalid issue key');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should handle API errors', async () => {
|
|
246
|
+
jiraService.getIssueDetail.mockRejectedValue(new Error('Not found'));
|
|
247
|
+
|
|
248
|
+
const response = await request(app).get('/api/issues/TEST-1');
|
|
249
|
+
|
|
250
|
+
expect(response.status).toBe(500);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('GET /api/issues/:issueKey/comments', () => {
|
|
255
|
+
it('should fetch comments', async () => {
|
|
256
|
+
jiraService.getComments.mockResolvedValue([
|
|
257
|
+
{ id: '1', body: { text: 'Comment' } }
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
const response = await request(app).get('/api/issues/TEST-1/comments');
|
|
261
|
+
|
|
262
|
+
expect(response.status).toBe(200);
|
|
263
|
+
expect(response.body).toHaveLength(1);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should validate issue key', async () => {
|
|
267
|
+
const response = await request(app).get('/api/issues/invalid/comments');
|
|
268
|
+
|
|
269
|
+
expect(response.status).toBe(400);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should handle API errors', async () => {
|
|
273
|
+
jiraService.getComments.mockRejectedValue(new Error('API Error'));
|
|
274
|
+
|
|
275
|
+
const response = await request(app).get('/api/issues/TEST-1/comments');
|
|
276
|
+
|
|
277
|
+
expect(response.status).toBe(500);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe('POST /api/issues/:issueKey/comments', () => {
|
|
282
|
+
it('should add comment', async () => {
|
|
283
|
+
jiraService.addComment.mockResolvedValue({ id: '100', body: {} });
|
|
284
|
+
|
|
285
|
+
const response = await request(app)
|
|
286
|
+
.post('/api/issues/TEST-1/comments')
|
|
287
|
+
.send({ body: 'New comment' });
|
|
288
|
+
|
|
289
|
+
expect(response.status).toBe(201);
|
|
290
|
+
expect(jiraService.addComment).toHaveBeenCalledWith('TEST-1', 'New comment');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should reject empty comment body', async () => {
|
|
294
|
+
const response = await request(app)
|
|
295
|
+
.post('/api/issues/TEST-1/comments')
|
|
296
|
+
.send({ body: ' ' });
|
|
297
|
+
|
|
298
|
+
expect(response.status).toBe(400);
|
|
299
|
+
expect(response.body.error).toBe('Comment body cannot be empty');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should reject missing comment body', async () => {
|
|
303
|
+
const response = await request(app)
|
|
304
|
+
.post('/api/issues/TEST-1/comments')
|
|
305
|
+
.send({});
|
|
306
|
+
|
|
307
|
+
expect(response.status).toBe(400);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should validate issue key', async () => {
|
|
311
|
+
const response = await request(app)
|
|
312
|
+
.post('/api/issues/invalid/comments')
|
|
313
|
+
.send({ body: 'Test' });
|
|
314
|
+
|
|
315
|
+
expect(response.status).toBe(400);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should handle API errors', async () => {
|
|
319
|
+
jiraService.addComment.mockRejectedValue(new Error('Post failed'));
|
|
320
|
+
|
|
321
|
+
const response = await request(app)
|
|
322
|
+
.post('/api/issues/TEST-1/comments')
|
|
323
|
+
.send({ body: 'Comment' });
|
|
324
|
+
|
|
325
|
+
expect(response.status).toBe(500);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe('GET /api/issues/:issueKey/transitions', () => {
|
|
330
|
+
it('should fetch transitions', async () => {
|
|
331
|
+
jiraService.getTransitions.mockResolvedValue([
|
|
332
|
+
{ id: '1', name: 'To Do' },
|
|
333
|
+
{ id: '2', name: 'In Progress' }
|
|
334
|
+
]);
|
|
335
|
+
|
|
336
|
+
const response = await request(app).get('/api/issues/TEST-1/transitions');
|
|
337
|
+
|
|
338
|
+
expect(response.status).toBe(200);
|
|
339
|
+
expect(response.body).toHaveLength(2);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should validate issue key', async () => {
|
|
343
|
+
const response = await request(app).get('/api/issues/invalid/transitions');
|
|
344
|
+
|
|
345
|
+
expect(response.status).toBe(400);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should handle API errors', async () => {
|
|
349
|
+
jiraService.getTransitions.mockRejectedValue(new Error('API Error'));
|
|
350
|
+
|
|
351
|
+
const response = await request(app).get('/api/issues/TEST-1/transitions');
|
|
352
|
+
|
|
353
|
+
expect(response.status).toBe(500);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe('POST /api/issues/:issueKey/transitions', () => {
|
|
358
|
+
it('should transition issue', async () => {
|
|
359
|
+
jiraService.transitionIssue.mockResolvedValue({ success: true });
|
|
360
|
+
|
|
361
|
+
const response = await request(app)
|
|
362
|
+
.post('/api/issues/TEST-1/transitions')
|
|
363
|
+
.send({ transitionId: '3' });
|
|
364
|
+
|
|
365
|
+
expect(response.status).toBe(200);
|
|
366
|
+
expect(response.body.success).toBe(true);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should reject missing transitionId', async () => {
|
|
370
|
+
const response = await request(app)
|
|
371
|
+
.post('/api/issues/TEST-1/transitions')
|
|
372
|
+
.send({});
|
|
373
|
+
|
|
374
|
+
expect(response.status).toBe(400);
|
|
375
|
+
expect(response.body.error).toBe('transitionId is required');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should validate issue key', async () => {
|
|
379
|
+
const response = await request(app)
|
|
380
|
+
.post('/api/issues/invalid/transitions')
|
|
381
|
+
.send({ transitionId: '1' });
|
|
382
|
+
|
|
383
|
+
expect(response.status).toBe(400);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('should handle API errors', async () => {
|
|
387
|
+
jiraService.transitionIssue.mockRejectedValue(new Error('Transition failed'));
|
|
388
|
+
|
|
389
|
+
const response = await request(app)
|
|
390
|
+
.post('/api/issues/TEST-1/transitions')
|
|
391
|
+
.send({ transitionId: '3' });
|
|
392
|
+
|
|
393
|
+
expect(response.status).toBe(500);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
describe('GET /api/issues/:issueKey/assignable', () => {
|
|
398
|
+
it('should fetch assignable users', async () => {
|
|
399
|
+
jiraService.getAssignableUsers.mockResolvedValue([
|
|
400
|
+
{ accountId: '1', displayName: 'John' }
|
|
401
|
+
]);
|
|
402
|
+
|
|
403
|
+
const response = await request(app).get('/api/issues/TEST-1/assignable');
|
|
404
|
+
|
|
405
|
+
expect(response.status).toBe(200);
|
|
406
|
+
expect(response.body).toHaveLength(1);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('should pass query parameter', async () => {
|
|
410
|
+
jiraService.getAssignableUsers.mockResolvedValue([]);
|
|
411
|
+
|
|
412
|
+
await request(app)
|
|
413
|
+
.get('/api/issues/TEST-1/assignable')
|
|
414
|
+
.query({ query: 'john' });
|
|
415
|
+
|
|
416
|
+
expect(jiraService.getAssignableUsers).toHaveBeenCalledWith('TEST-1', 'john');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should validate issue key', async () => {
|
|
420
|
+
const response = await request(app).get('/api/issues/invalid/assignable');
|
|
421
|
+
|
|
422
|
+
expect(response.status).toBe(400);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('should handle API errors', async () => {
|
|
426
|
+
jiraService.getAssignableUsers.mockRejectedValue(new Error('API Error'));
|
|
427
|
+
|
|
428
|
+
const response = await request(app).get('/api/issues/TEST-1/assignable');
|
|
429
|
+
|
|
430
|
+
expect(response.status).toBe(500);
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe('PUT /api/issues/:issueKey/assignee', () => {
|
|
435
|
+
it('should assign issue', async () => {
|
|
436
|
+
jiraService.assignIssue.mockResolvedValue({ success: true });
|
|
437
|
+
|
|
438
|
+
const response = await request(app)
|
|
439
|
+
.put('/api/issues/TEST-1/assignee')
|
|
440
|
+
.send({ accountId: 'user123' });
|
|
441
|
+
|
|
442
|
+
expect(response.status).toBe(200);
|
|
443
|
+
expect(jiraService.assignIssue).toHaveBeenCalledWith('TEST-1', 'user123');
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('should validate issue key', async () => {
|
|
447
|
+
const response = await request(app)
|
|
448
|
+
.put('/api/issues/invalid/assignee')
|
|
449
|
+
.send({ accountId: 'user123' });
|
|
450
|
+
|
|
451
|
+
expect(response.status).toBe(400);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('should handle API errors', async () => {
|
|
455
|
+
jiraService.assignIssue.mockRejectedValue(new Error('Assign failed'));
|
|
456
|
+
|
|
457
|
+
const response = await request(app)
|
|
458
|
+
.put('/api/issues/TEST-1/assignee')
|
|
459
|
+
.send({ accountId: 'user123' });
|
|
460
|
+
|
|
461
|
+
expect(response.status).toBe(500);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
describe('POST /api/issues/:issueKey/attachments', () => {
|
|
466
|
+
it('should upload attachment', async () => {
|
|
467
|
+
jiraService.attachFile.mockResolvedValue([{ filename: 'test.pdf' }]);
|
|
468
|
+
|
|
469
|
+
const response = await request(app)
|
|
470
|
+
.post('/api/issues/TEST-1/attachments')
|
|
471
|
+
.attach('file', Buffer.from('test'), 'test.pdf');
|
|
472
|
+
|
|
473
|
+
expect(response.status).toBe(201);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('should reject missing file', async () => {
|
|
477
|
+
const response = await request(app)
|
|
478
|
+
.post('/api/issues/TEST-1/attachments')
|
|
479
|
+
.field('other', 'data');
|
|
480
|
+
|
|
481
|
+
expect(response.status).toBe(400);
|
|
482
|
+
expect(response.body.error).toBe('No file uploaded');
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('should validate issue key', async () => {
|
|
486
|
+
const response = await request(app)
|
|
487
|
+
.post('/api/issues/invalid/attachments')
|
|
488
|
+
.attach('file', Buffer.from('test'), 'test.pdf');
|
|
489
|
+
|
|
490
|
+
expect(response.status).toBe(400);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('should handle API errors', async () => {
|
|
494
|
+
jiraService.attachFile.mockRejectedValue(new Error('Upload failed'));
|
|
495
|
+
|
|
496
|
+
const response = await request(app)
|
|
497
|
+
.post('/api/issues/TEST-1/attachments')
|
|
498
|
+
.attach('file', Buffer.from('test'), 'test.pdf');
|
|
499
|
+
|
|
500
|
+
expect(response.status).toBe(500);
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
describe('PUT /api/issues/:issueKey', () => {
|
|
505
|
+
it('should update issue fields', async () => {
|
|
506
|
+
jiraService.updateIssue.mockResolvedValue({ success: true });
|
|
507
|
+
|
|
508
|
+
const response = await request(app)
|
|
509
|
+
.put('/api/issues/TEST-1')
|
|
510
|
+
.send({ fields: { summary: 'Updated' } });
|
|
511
|
+
|
|
512
|
+
expect(response.status).toBe(200);
|
|
513
|
+
expect(response.body.success).toBe(true);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('should reject missing fields', async () => {
|
|
517
|
+
const response = await request(app)
|
|
518
|
+
.put('/api/issues/TEST-1')
|
|
519
|
+
.send({});
|
|
520
|
+
|
|
521
|
+
expect(response.status).toBe(400);
|
|
522
|
+
expect(response.body.error).toBe('fields are required');
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('should validate issue key', async () => {
|
|
526
|
+
const response = await request(app)
|
|
527
|
+
.put('/api/issues/invalid')
|
|
528
|
+
.send({ fields: {} });
|
|
529
|
+
|
|
530
|
+
expect(response.status).toBe(400);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('should handle API errors', async () => {
|
|
534
|
+
jiraService.updateIssue.mockRejectedValue(new Error('Update failed'));
|
|
535
|
+
|
|
536
|
+
const response = await request(app)
|
|
537
|
+
.put('/api/issues/TEST-1')
|
|
538
|
+
.send({ fields: { summary: 'Test' } });
|
|
539
|
+
|
|
540
|
+
expect(response.status).toBe(500);
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
describe('Input Validation', () => {
|
|
545
|
+
it('should validate issue key with numbers', async () => {
|
|
546
|
+
const response = await request(app).get('/api/issues/TEST123');
|
|
547
|
+
|
|
548
|
+
expect(response.status).toBe(400);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('should validate issue key with special chars', async () => {
|
|
552
|
+
const response = await request(app).get('/api/issues/TEST-1;DROP');
|
|
553
|
+
|
|
554
|
+
expect(response.status).toBe(400);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('should accept valid issue keys', async () => {
|
|
558
|
+
jiraService.getIssueDetail.mockResolvedValue({ key: 'TEST-123' });
|
|
559
|
+
|
|
560
|
+
const response = await request(app).get('/api/issues/TEST-123');
|
|
561
|
+
|
|
562
|
+
expect(response.status).toBe(200);
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
});
|