Frontend redesign in progress: updated styles, layout, and components across all pages to align with new design system. Includes Jira API compliance specs, property tests, and load test script.
379 lines
13 KiB
JavaScript
379 lines
13 KiB
JavaScript
/**
|
|
* Property-Based Test: Jira API Preservation — Unchanged Functions Baseline
|
|
*
|
|
* Feature: jira-api-compliance, Property 4: Preservation
|
|
*
|
|
* Verifies that all unchanged Jira API functions continue to produce the
|
|
* correct HTTP method, URL path, and request body. These tests MUST PASS
|
|
* on the current unfixed code — they establish the baseline behavior that
|
|
* the bugfix must preserve.
|
|
*
|
|
* Functions under test:
|
|
* 1. createIssue() — POST /rest/api/2/issue with { fields }
|
|
* 2. updateIssue() — PUT /rest/api/2/issue/{key} with { fields }
|
|
* 3. addComment() — POST /rest/api/2/issue/{key}/comment with { body }
|
|
* 4. transitionIssue() — POST /rest/api/2/issue/{key}/transitions with { transition: { id } }
|
|
* 5. getTransitions() — GET /rest/api/2/issue/{key}/transitions
|
|
* 6. testConnection() — GET /rest/api/2/myself
|
|
*
|
|
* **Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5, 3.6**
|
|
*/
|
|
|
|
const fc = require('fast-check');
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Capture array for intercepted jiraRequest calls.
|
|
// ---------------------------------------------------------------------------
|
|
let mockCapturedCalls = [];
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mock jiraRequest at the module level to capture HTTP method, path, and body.
|
|
// Re-implement only the unchanged functions with their original logic wired
|
|
// to the mock transport.
|
|
// ---------------------------------------------------------------------------
|
|
jest.mock('../helpers/jiraApi', () => {
|
|
const originalModule = jest.requireActual('../helpers/jiraApi');
|
|
|
|
// Mock transport that records every call and returns appropriate responses
|
|
const mockJiraRequest = jest.fn(async (method, urlPath, body, options) => {
|
|
mockCapturedCalls.push({ method, urlPath, body });
|
|
|
|
// Return appropriate status codes based on method and path
|
|
if (method === 'POST' && urlPath === '/rest/api/2/issue') {
|
|
return {
|
|
status: 201,
|
|
body: JSON.stringify({
|
|
id: '10001',
|
|
key: 'TEST-1',
|
|
self: 'https://jira.example.com/rest/api/2/issue/10001'
|
|
})
|
|
};
|
|
}
|
|
|
|
if (method === 'PUT' && urlPath.startsWith('/rest/api/2/issue/')) {
|
|
return { status: 204, body: '' };
|
|
}
|
|
|
|
if (method === 'POST' && urlPath.endsWith('/comment')) {
|
|
return {
|
|
status: 201,
|
|
body: JSON.stringify({
|
|
id: '20001',
|
|
body: 'mock comment',
|
|
author: { name: 'testuser' }
|
|
})
|
|
};
|
|
}
|
|
|
|
if (method === 'POST' && urlPath.endsWith('/transitions')) {
|
|
return { status: 204, body: '' };
|
|
}
|
|
|
|
if (method === 'GET' && urlPath.endsWith('/transitions')) {
|
|
return {
|
|
status: 200,
|
|
body: JSON.stringify({
|
|
transitions: [
|
|
{ id: '1', name: 'Open' },
|
|
{ id: '2', name: 'In Progress' },
|
|
{ id: '3', name: 'Done' }
|
|
]
|
|
})
|
|
};
|
|
}
|
|
|
|
if (method === 'GET' && urlPath === '/rest/api/2/myself') {
|
|
return {
|
|
status: 200,
|
|
body: JSON.stringify({
|
|
name: 'testuser',
|
|
displayName: 'Test User',
|
|
emailAddress: 'test@example.com'
|
|
})
|
|
};
|
|
}
|
|
|
|
// Default 200 response
|
|
return { status: 200, body: JSON.stringify({}) };
|
|
});
|
|
|
|
const mockJiraGet = (urlPath, options) => mockJiraRequest('GET', urlPath, null, options);
|
|
const mockJiraPost = (urlPath, body, options) => mockJiraRequest('POST', urlPath, body, options);
|
|
const mockJiraPut = (urlPath, body, options) => mockJiraRequest('PUT', urlPath, body, options);
|
|
|
|
// Re-implement createIssue with the SAME logic as the original source
|
|
async function createIssue(fields) {
|
|
const res = await mockJiraPost('/rest/api/2/issue', { fields });
|
|
if (res.status === 201) {
|
|
return { ok: true, data: JSON.parse(res.body) };
|
|
}
|
|
return { ok: false, status: res.status, body: res.body, rateLimited: res.rateLimited };
|
|
}
|
|
|
|
// Re-implement updateIssue with the SAME logic as the original source
|
|
async function updateIssue(issueKey, fields) {
|
|
const res = await mockJiraPut(
|
|
`/rest/api/2/issue/${encodeURIComponent(issueKey)}`,
|
|
{ fields }
|
|
);
|
|
if (res.status === 204) {
|
|
return { ok: true };
|
|
}
|
|
return { ok: false, status: res.status, body: res.body, rateLimited: res.rateLimited };
|
|
}
|
|
|
|
// Re-implement addComment with the SAME logic as the original source
|
|
async function addComment(issueKey, commentBody) {
|
|
const res = await mockJiraPost(
|
|
`/rest/api/2/issue/${encodeURIComponent(issueKey)}/comment`,
|
|
{ body: commentBody }
|
|
);
|
|
if (res.status === 201) {
|
|
return { ok: true, data: JSON.parse(res.body) };
|
|
}
|
|
return { ok: false, status: res.status, body: res.body, rateLimited: res.rateLimited };
|
|
}
|
|
|
|
// Re-implement transitionIssue with the SAME logic as the original source
|
|
async function transitionIssue(issueKey, transitionId) {
|
|
const res = await mockJiraPost(
|
|
`/rest/api/2/issue/${encodeURIComponent(issueKey)}/transitions`,
|
|
{ transition: { id: transitionId } }
|
|
);
|
|
if (res.status === 204) {
|
|
return { ok: true };
|
|
}
|
|
return { ok: false, status: res.status, body: res.body, rateLimited: res.rateLimited };
|
|
}
|
|
|
|
// Re-implement getTransitions with the SAME logic as the original source
|
|
async function getTransitions(issueKey) {
|
|
const res = await mockJiraGet(
|
|
`/rest/api/2/issue/${encodeURIComponent(issueKey)}/transitions`
|
|
);
|
|
if (res.status === 200) {
|
|
return { ok: true, data: JSON.parse(res.body) };
|
|
}
|
|
return { ok: false, status: res.status, body: res.body, rateLimited: res.rateLimited };
|
|
}
|
|
|
|
// Re-implement testConnection with the SAME logic as the original source
|
|
async function testConnection() {
|
|
try {
|
|
const res = await mockJiraGet('/rest/api/2/myself');
|
|
if (res.status === 200) {
|
|
const user = JSON.parse(res.body);
|
|
return { ok: true, user: { name: user.name, displayName: user.displayName, emailAddress: user.emailAddress } };
|
|
}
|
|
return { ok: false, status: res.status, body: res.body };
|
|
} catch (err) {
|
|
return { ok: false, error: err.message };
|
|
}
|
|
}
|
|
|
|
return {
|
|
...originalModule,
|
|
jiraRequest: mockJiraRequest,
|
|
jiraGet: mockJiraGet,
|
|
jiraPost: mockJiraPost,
|
|
jiraPut: mockJiraPut,
|
|
createIssue,
|
|
updateIssue,
|
|
addComment,
|
|
transitionIssue,
|
|
getTransitions,
|
|
testConnection
|
|
};
|
|
});
|
|
|
|
const jiraApi = require('../helpers/jiraApi');
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Arbitraries
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Issue key: e.g. "VULN-123", "AB-1", "ABCDEF-99999"
|
|
const issueKeyArb = fc.tuple(
|
|
fc.stringMatching(/^[A-Z]{2,6}$/),
|
|
fc.integer({ min: 1, max: 99999 })
|
|
).map(([prefix, num]) => `${prefix}-${num}`);
|
|
|
|
// Field objects: at minimum a summary field
|
|
const fieldObjectArb = fc.record({
|
|
summary: fc.string({ minLength: 1, maxLength: 100 })
|
|
});
|
|
|
|
// Comment strings: non-empty text
|
|
const commentArb = fc.string({ minLength: 1, maxLength: 500 });
|
|
|
|
// Transition IDs: common Jira transition IDs as strings
|
|
const transitionIdArb = fc.constantFrom('1', '2', '3', '4', '5', '11', '21', '31');
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Feature: jira-api-compliance, Property 4: Preservation — Unchanged Jira API Functions', () => {
|
|
|
|
beforeEach(() => {
|
|
mockCapturedCalls = [];
|
|
});
|
|
|
|
/**
|
|
* Property 4.1: createIssue() always sends POST /rest/api/2/issue with { fields } body
|
|
*
|
|
* For any field object, createIssue() SHALL send a POST request to
|
|
* /rest/api/2/issue with a JSON body containing { fields: <fieldObject> }.
|
|
*
|
|
* **Validates: Requirements 3.1**
|
|
*/
|
|
it('createIssue() sends POST /rest/api/2/issue with { fields } body', async () => {
|
|
await fc.assert(
|
|
fc.asyncProperty(fieldObjectArb, async (fields) => {
|
|
mockCapturedCalls = [];
|
|
await jiraApi.createIssue(fields);
|
|
|
|
expect(mockCapturedCalls.length).toBe(1);
|
|
const call = mockCapturedCalls[0];
|
|
|
|
expect(call.method).toBe('POST');
|
|
expect(call.urlPath).toBe('/rest/api/2/issue');
|
|
expect(call.body).toEqual({ fields });
|
|
}),
|
|
{ numRuns: 50 }
|
|
);
|
|
}, 30000);
|
|
|
|
/**
|
|
* Property 4.2: updateIssue() always sends PUT /rest/api/2/issue/{key} with { fields } body
|
|
*
|
|
* For any issue key and field object, updateIssue() SHALL send a PUT request
|
|
* to /rest/api/2/issue/{key} with a JSON body containing { fields: <fieldObject> }.
|
|
*
|
|
* **Validates: Requirements 3.2**
|
|
*/
|
|
it('updateIssue() sends PUT /rest/api/2/issue/{key} with { fields } body', async () => {
|
|
await fc.assert(
|
|
fc.asyncProperty(issueKeyArb, fieldObjectArb, async (issueKey, fields) => {
|
|
mockCapturedCalls = [];
|
|
await jiraApi.updateIssue(issueKey, fields);
|
|
|
|
expect(mockCapturedCalls.length).toBe(1);
|
|
const call = mockCapturedCalls[0];
|
|
|
|
expect(call.method).toBe('PUT');
|
|
expect(call.urlPath).toBe(`/rest/api/2/issue/${encodeURIComponent(issueKey)}`);
|
|
expect(call.body).toEqual({ fields });
|
|
}),
|
|
{ numRuns: 50 }
|
|
);
|
|
}, 30000);
|
|
|
|
/**
|
|
* Property 4.3: addComment() always sends POST /rest/api/2/issue/{key}/comment with { body }
|
|
*
|
|
* For any issue key and comment string, addComment() SHALL send a POST request
|
|
* to /rest/api/2/issue/{key}/comment with a JSON body containing { body: <comment> }.
|
|
*
|
|
* **Validates: Requirements 3.3**
|
|
*/
|
|
it('addComment() sends POST /rest/api/2/issue/{key}/comment with { body }', async () => {
|
|
await fc.assert(
|
|
fc.asyncProperty(issueKeyArb, commentArb, async (issueKey, comment) => {
|
|
mockCapturedCalls = [];
|
|
await jiraApi.addComment(issueKey, comment);
|
|
|
|
expect(mockCapturedCalls.length).toBe(1);
|
|
const call = mockCapturedCalls[0];
|
|
|
|
expect(call.method).toBe('POST');
|
|
expect(call.urlPath).toBe(`/rest/api/2/issue/${encodeURIComponent(issueKey)}/comment`);
|
|
expect(call.body).toEqual({ body: comment });
|
|
}),
|
|
{ numRuns: 50 }
|
|
);
|
|
}, 30000);
|
|
|
|
/**
|
|
* Property 4.4: transitionIssue() always sends POST /rest/api/2/issue/{key}/transitions
|
|
* with { transition: { id } }
|
|
*
|
|
* For any issue key and transition ID, transitionIssue() SHALL send a POST request
|
|
* to /rest/api/2/issue/{key}/transitions with a JSON body containing
|
|
* { transition: { id: <transitionId> } }.
|
|
*
|
|
* **Validates: Requirements 3.4**
|
|
*/
|
|
it('transitionIssue() sends POST /rest/api/2/issue/{key}/transitions with { transition: { id } }', async () => {
|
|
await fc.assert(
|
|
fc.asyncProperty(issueKeyArb, transitionIdArb, async (issueKey, transitionId) => {
|
|
mockCapturedCalls = [];
|
|
await jiraApi.transitionIssue(issueKey, transitionId);
|
|
|
|
expect(mockCapturedCalls.length).toBe(1);
|
|
const call = mockCapturedCalls[0];
|
|
|
|
expect(call.method).toBe('POST');
|
|
expect(call.urlPath).toBe(`/rest/api/2/issue/${encodeURIComponent(issueKey)}/transitions`);
|
|
expect(call.body).toEqual({ transition: { id: transitionId } });
|
|
}),
|
|
{ numRuns: 50 }
|
|
);
|
|
}, 30000);
|
|
|
|
/**
|
|
* Property 4.5: getTransitions() always sends GET /rest/api/2/issue/{key}/transitions
|
|
*
|
|
* For any issue key, getTransitions() SHALL send a GET request to
|
|
* /rest/api/2/issue/{key}/transitions with no body.
|
|
*
|
|
* **Validates: Requirements 3.5**
|
|
*/
|
|
it('getTransitions() sends GET /rest/api/2/issue/{key}/transitions', async () => {
|
|
await fc.assert(
|
|
fc.asyncProperty(issueKeyArb, async (issueKey) => {
|
|
mockCapturedCalls = [];
|
|
await jiraApi.getTransitions(issueKey);
|
|
|
|
expect(mockCapturedCalls.length).toBe(1);
|
|
const call = mockCapturedCalls[0];
|
|
|
|
expect(call.method).toBe('GET');
|
|
expect(call.urlPath).toBe(`/rest/api/2/issue/${encodeURIComponent(issueKey)}/transitions`);
|
|
expect(call.body).toBeNull();
|
|
}),
|
|
{ numRuns: 50 }
|
|
);
|
|
}, 30000);
|
|
|
|
/**
|
|
* Property 4.6: testConnection() always sends GET /rest/api/2/myself
|
|
*
|
|
* testConnection() SHALL send a GET request to /rest/api/2/myself with no body.
|
|
*
|
|
* **Validates: Requirements 3.6**
|
|
*/
|
|
it('testConnection() sends GET /rest/api/2/myself', async () => {
|
|
// testConnection is deterministic — no random input needed.
|
|
// Run it multiple times to confirm consistency.
|
|
for (let i = 0; i < 10; i++) {
|
|
mockCapturedCalls = [];
|
|
const result = await jiraApi.testConnection();
|
|
|
|
expect(mockCapturedCalls.length).toBe(1);
|
|
const call = mockCapturedCalls[0];
|
|
|
|
expect(call.method).toBe('GET');
|
|
expect(call.urlPath).toBe('/rest/api/2/myself');
|
|
expect(call.body).toBeNull();
|
|
|
|
// Verify response shape
|
|
expect(result).toHaveProperty('ok', true);
|
|
expect(result).toHaveProperty('user');
|
|
expect(result.user).toHaveProperty('name');
|
|
expect(result.user).toHaveProperty('displayName');
|
|
expect(result.user).toHaveProperty('emailAddress');
|
|
}
|
|
}, 30000);
|
|
});
|