Add Jira Data Center integration with UAT test script and use case docs
This commit is contained in:
343
backend/scripts/jira-uat-test.js
Normal file
343
backend/scripts/jira-uat-test.js
Normal file
@@ -0,0 +1,343 @@
|
||||
#!/usr/bin/env node
|
||||
// ==========================================================================
|
||||
// Jira UAT Test Script
|
||||
// ==========================================================================
|
||||
// Exercises every Jira REST API use case the STEAM Dashboard will run in
|
||||
// production. Run this against the UAT instance before submitting the
|
||||
// ATLSUP Rest API Approval ticket.
|
||||
//
|
||||
// Usage:
|
||||
// cd backend
|
||||
// node scripts/jira-uat-test.js
|
||||
//
|
||||
// Prerequisites:
|
||||
// - backend/.env has JIRA_BASE_URL pointing to UAT
|
||||
// - JIRA_API_USER / JIRA_API_TOKEN set to service account credentials
|
||||
// - JIRA_PROJECT_KEY set to a UAT project your service account can access
|
||||
// - Service account has been granted access to the target space by space owners
|
||||
//
|
||||
// The script logs every API call, response status, and timing to both
|
||||
// console and a log file at backend/scripts/jira-uat-test.log for the
|
||||
// ATLSUP reviewers.
|
||||
// ==========================================================================
|
||||
|
||||
require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const jiraApi = require('../helpers/jiraApi');
|
||||
|
||||
const LOG_FILE = path.join(__dirname, 'jira-uat-test.log');
|
||||
const results = [];
|
||||
let createdIssueKey = null;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Logging
|
||||
// ---------------------------------------------------------------------------
|
||||
function log(level, message, data) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const entry = { timestamp, level, message };
|
||||
if (data !== undefined) entry.data = data;
|
||||
results.push(entry);
|
||||
|
||||
const line = `[${timestamp}] ${level.toUpperCase().padEnd(5)} ${message}`;
|
||||
console.log(line);
|
||||
if (data) {
|
||||
const dataStr = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
||||
console.log(' ' + dataStr.split('\n').join('\n '));
|
||||
}
|
||||
}
|
||||
|
||||
function logPass(testName, data) { log('pass', `PASS: ${testName}`, data); }
|
||||
function logFail(testName, data) { log('fail', `FAIL: ${testName}`, data); }
|
||||
function logInfo(message, data) { log('info', message, data); }
|
||||
function logWarn(message, data) { log('warn', message, data); }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test runner
|
||||
// ---------------------------------------------------------------------------
|
||||
async function runTest(name, fn) {
|
||||
logInfo(`--- Running: ${name} ---`);
|
||||
const start = Date.now();
|
||||
try {
|
||||
await fn();
|
||||
logPass(name, { durationMs: Date.now() - start });
|
||||
return true;
|
||||
} catch (err) {
|
||||
logFail(name, { error: err.message, durationMs: Date.now() - start });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error('Assertion failed: ' + message);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Use Case 1: Connection Test (GET /rest/api/2/myself)
|
||||
// Production use: Admin clicks "Test Connection" button on Jira settings panel
|
||||
// ---------------------------------------------------------------------------
|
||||
async function testConnection() {
|
||||
const result = await jiraApi.testConnection();
|
||||
assert(result.ok, 'Connection test should succeed. Got: ' + JSON.stringify(result));
|
||||
assert(result.user && result.user.name, 'Should return authenticated user name');
|
||||
logInfo('Authenticated as:', result.user);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Use Case 2: Create Issue (POST /rest/api/2/issue)
|
||||
// Production use: User clicks "Create in Jira" from CVE detail panel
|
||||
// ---------------------------------------------------------------------------
|
||||
async function testCreateIssue() {
|
||||
const projectKey = jiraApi.JIRA_PROJECT_KEY;
|
||||
assert(projectKey, 'JIRA_PROJECT_KEY must be set in .env');
|
||||
|
||||
const fields = {
|
||||
project: { key: projectKey },
|
||||
summary: `[UAT TEST] STEAM Dashboard - CVE-2025-0001 - TestVendor - ${new Date().toISOString()}`,
|
||||
issuetype: { name: jiraApi.JIRA_ISSUE_TYPE || 'Task' },
|
||||
description: 'Automated UAT test issue created by STEAM Security Dashboard Jira integration test script. This issue can be safely deleted after review.'
|
||||
};
|
||||
|
||||
logInfo('Creating issue with fields:', { project: fields.project, summary: fields.summary, issuetype: fields.issuetype });
|
||||
|
||||
const result = await jiraApi.createIssue(fields);
|
||||
assert(result.ok, 'Create issue should succeed. Got: ' + JSON.stringify(result));
|
||||
assert(result.data && result.data.key, 'Should return issue key');
|
||||
|
||||
createdIssueKey = result.data.key;
|
||||
logInfo('Created issue:', { key: createdIssueKey, id: result.data.id, self: result.data.self });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Use Case 3: Get Single Issue (GET /rest/api/2/issue/{key}?fields=...)
|
||||
// Production use: User clicks "Sync" on a single Jira ticket row
|
||||
// ---------------------------------------------------------------------------
|
||||
async function testGetIssue() {
|
||||
assert(createdIssueKey, 'Need a created issue key from previous test');
|
||||
|
||||
const result = await jiraApi.getIssue(createdIssueKey);
|
||||
assert(result.ok, 'Get issue should succeed. Got: ' + JSON.stringify(result));
|
||||
|
||||
const issue = result.data;
|
||||
assert(issue.key === createdIssueKey, 'Returned key should match');
|
||||
assert(issue.fields && issue.fields.summary, 'Should have summary field');
|
||||
assert(issue.fields.status, 'Should have status field');
|
||||
|
||||
logInfo('Fetched issue:', {
|
||||
key: issue.key,
|
||||
summary: issue.fields.summary,
|
||||
status: issue.fields.status.name,
|
||||
issuetype: issue.fields.issuetype ? issue.fields.issuetype.name : null
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Use Case 4: Update Issue (PUT /rest/api/2/issue/{key})
|
||||
// Production use: Local ticket edits synced back to Jira (future feature)
|
||||
// ---------------------------------------------------------------------------
|
||||
async function testUpdateIssue() {
|
||||
assert(createdIssueKey, 'Need a created issue key from previous test');
|
||||
|
||||
const result = await jiraApi.updateIssue(createdIssueKey, {
|
||||
summary: `[UAT TEST] STEAM Dashboard - UPDATED - ${new Date().toISOString()}`
|
||||
});
|
||||
assert(result.ok, 'Update issue should succeed (204). Got: ' + JSON.stringify(result));
|
||||
logInfo('Updated issue summary successfully');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Use Case 5: Add Comment (POST /rest/api/2/issue/{key}/comment)
|
||||
// Production use: Dashboard adds audit trail comments to linked Jira tickets
|
||||
// ---------------------------------------------------------------------------
|
||||
async function testAddComment() {
|
||||
assert(createdIssueKey, 'Need a created issue key from previous test');
|
||||
|
||||
const commentBody = `STEAM Dashboard UAT test comment.\nTimestamp: ${new Date().toISOString()}\nThis comment was created by the automated test script.`;
|
||||
|
||||
const result = await jiraApi.addComment(createdIssueKey, commentBody);
|
||||
assert(result.ok, 'Add comment should succeed. Got: ' + JSON.stringify(result));
|
||||
assert(result.data && result.data.id, 'Should return comment ID');
|
||||
|
||||
logInfo('Added comment:', { commentId: result.data.id });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Use Case 6: Get Transitions (GET /rest/api/2/issue/{key}/transitions)
|
||||
// Production use: Dashboard checks available workflow transitions before
|
||||
// attempting to move a ticket to a new status
|
||||
// ---------------------------------------------------------------------------
|
||||
async function testGetTransitions() {
|
||||
assert(createdIssueKey, 'Need a created issue key from previous test');
|
||||
|
||||
const result = await jiraApi.getTransitions(createdIssueKey);
|
||||
assert(result.ok, 'Get transitions should succeed. Got: ' + JSON.stringify(result));
|
||||
|
||||
const transitions = result.data.transitions || [];
|
||||
logInfo('Available transitions:', transitions.map(t => ({ id: t.id, name: t.name, to: t.to ? t.to.name : null })));
|
||||
|
||||
// Store for the transition test
|
||||
return transitions;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Use Case 7: Transition Issue (POST /rest/api/2/issue/{key}/transitions)
|
||||
// Production use: Dashboard moves ticket status (e.g., Open → In Progress)
|
||||
// ---------------------------------------------------------------------------
|
||||
async function testTransitionIssue(transitions) {
|
||||
assert(createdIssueKey, 'Need a created issue key from previous test');
|
||||
|
||||
if (!transitions || transitions.length === 0) {
|
||||
logWarn('No transitions available — skipping transition test. This may be expected depending on the project workflow.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Pick the first available transition
|
||||
const transition = transitions[0];
|
||||
logInfo(`Transitioning to: ${transition.name} (id: ${transition.id})`);
|
||||
|
||||
const result = await jiraApi.transitionIssue(createdIssueKey, transition.id);
|
||||
assert(result.ok, 'Transition should succeed (204). Got: ' + JSON.stringify(result));
|
||||
logInfo('Transition successful');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Use Case 8: JQL Search (POST /rest/api/2/search)
|
||||
// Production use: Bulk sync — fetches all tracked tickets in one request
|
||||
// instead of one GET per ticket (Charter-compliant)
|
||||
// ---------------------------------------------------------------------------
|
||||
async function testJqlSearch() {
|
||||
const projectKey = jiraApi.JIRA_PROJECT_KEY;
|
||||
assert(projectKey, 'JIRA_PROJECT_KEY must be set');
|
||||
|
||||
const jql = `project = ${projectKey} AND updated >= -1h ORDER BY updated DESC`;
|
||||
logInfo('Searching with JQL:', jql);
|
||||
|
||||
const result = await jiraApi.searchIssues(jql, { maxResults: 10 });
|
||||
assert(result.ok, 'Search should succeed. Got: ' + JSON.stringify(result));
|
||||
|
||||
const data = result.data;
|
||||
logInfo('Search results:', {
|
||||
total: data.total,
|
||||
returned: (data.issues || []).length,
|
||||
issues: (data.issues || []).slice(0, 5).map(i => ({
|
||||
key: i.key,
|
||||
summary: i.fields.summary,
|
||||
status: i.fields.status ? i.fields.status.name : null
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Use Case 9: Bulk Key Search (searchIssuesByKeys)
|
||||
// Production use: sync-all endpoint — fetches multiple tickets by key
|
||||
// in a single JQL query
|
||||
// ---------------------------------------------------------------------------
|
||||
async function testBulkKeySearch() {
|
||||
assert(createdIssueKey, 'Need a created issue key from previous test');
|
||||
|
||||
// Search for the issue we created plus a fake key to test partial results
|
||||
const keys = [createdIssueKey, 'FAKE-99999'];
|
||||
logInfo('Bulk searching keys:', keys);
|
||||
|
||||
const result = await jiraApi.searchIssuesByKeys(keys);
|
||||
assert(result.ok, 'Bulk key search should succeed. Got: ' + JSON.stringify(result));
|
||||
|
||||
const found = (result.data.issues || []).map(i => i.key);
|
||||
logInfo('Found issues:', found);
|
||||
assert(found.includes(createdIssueKey), 'Should find the created issue');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Use Case 10: Rate Limit Status Check
|
||||
// Production use: Admin views rate limit usage on the Jira settings panel
|
||||
// ---------------------------------------------------------------------------
|
||||
async function testRateLimitStatus() {
|
||||
const status = jiraApi.getRateLimitStatus();
|
||||
assert(status.daily && typeof status.daily.used === 'number', 'Should have daily usage');
|
||||
assert(status.burst && typeof status.burst.used === 'number', 'Should have burst usage');
|
||||
logInfo('Rate limit status after all tests:', status);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
async function main() {
|
||||
logInfo('=== STEAM Dashboard — Jira UAT Test Run ===');
|
||||
logInfo('Timestamp: ' + new Date().toISOString());
|
||||
logInfo('JIRA_BASE_URL: ' + (process.env.JIRA_BASE_URL || '(not set)'));
|
||||
logInfo('JIRA_AUTH_METHOD: ' + (process.env.JIRA_AUTH_METHOD || 'basic'));
|
||||
logInfo('JIRA_API_USER: ' + (process.env.JIRA_API_USER || '(not set)'));
|
||||
logInfo('JIRA_PROJECT_KEY: ' + (jiraApi.JIRA_PROJECT_KEY || '(not set)'));
|
||||
logInfo('JIRA_ISSUE_TYPE: ' + (jiraApi.JIRA_ISSUE_TYPE || 'Task'));
|
||||
logInfo('JIRA_SKIP_TLS: ' + (process.env.JIRA_SKIP_TLS || 'false'));
|
||||
logInfo('isConfigured: ' + jiraApi.isConfigured);
|
||||
logInfo('');
|
||||
|
||||
if (!jiraApi.isConfigured) {
|
||||
logFail('Pre-flight check', 'Jira API is not configured. Set JIRA_BASE_URL, JIRA_API_USER, and JIRA_API_TOKEN in backend/.env');
|
||||
writeLog();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
let transitions = [];
|
||||
|
||||
// Run tests in order — later tests depend on the created issue
|
||||
if (await runTest('1. Connection Test (GET /myself)', testConnection)) passed++; else failed++;
|
||||
if (await runTest('2. Create Issue (POST /issue)', testCreateIssue)) passed++; else failed++;
|
||||
if (await runTest('3. Get Single Issue (GET /issue/{key})', testGetIssue)) passed++; else failed++;
|
||||
if (await runTest('4. Update Issue (PUT /issue/{key})', testUpdateIssue)) passed++; else failed++;
|
||||
if (await runTest('5. Add Comment (POST /issue/{key}/comment)', testAddComment)) passed++; else failed++;
|
||||
|
||||
if (await runTest('6. Get Transitions (GET /issue/{key}/transitions)', async () => {
|
||||
transitions = await testGetTransitions();
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await runTest('7. Transition Issue (POST /issue/{key}/transitions)', async () => {
|
||||
await testTransitionIssue(transitions);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await runTest('8. JQL Search (POST /search)', testJqlSearch)) passed++; else failed++;
|
||||
if (await runTest('9. Bulk Key Search (searchIssuesByKeys)', testBulkKeySearch)) passed++; else failed++;
|
||||
if (await runTest('10. Rate Limit Status', testRateLimitStatus)) passed++; else failed++;
|
||||
|
||||
logInfo('');
|
||||
logInfo('=== Summary ===');
|
||||
logInfo(`Passed: ${passed} | Failed: ${failed} | Total: ${passed + failed}`);
|
||||
if (createdIssueKey) {
|
||||
logInfo(`Test issue created: ${createdIssueKey} — delete manually after ATLSUP review if desired.`);
|
||||
}
|
||||
logInfo('Rate limit usage:', jiraApi.getRateLimitStatus());
|
||||
|
||||
writeLog();
|
||||
|
||||
if (failed > 0) {
|
||||
console.log('\nSome tests failed. Review the log above and jira-uat-test.log for details.');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('\nAll tests passed. Log saved to backend/scripts/jira-uat-test.log');
|
||||
console.log('Next steps:');
|
||||
console.log(' 1. Submit an ATLSUP Rest API Approval ticket');
|
||||
console.log(' 2. Attach or reference jira-uat-test.log in the ticket');
|
||||
console.log(' 3. Click "Script ran - Review Logs" on the ATLSUP ticket');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
function writeLog() {
|
||||
const lines = results.map(r => {
|
||||
let line = `[${r.timestamp}] ${r.level.toUpperCase().padEnd(5)} ${r.message}`;
|
||||
if (r.data) {
|
||||
line += '\n ' + (typeof r.data === 'string' ? r.data : JSON.stringify(r.data, null, 2)).split('\n').join('\n ');
|
||||
}
|
||||
return line;
|
||||
});
|
||||
fs.writeFileSync(LOG_FILE, lines.join('\n') + '\n', 'utf8');
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Unhandled error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user