#!/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); });