@apistudio/apim-cli
Version:
CLI for API Management Products
190 lines (178 loc) • 5.45 kB
text/typescript
/**
* Copyright IBM Corp. 2024, 2025
*/
import { LogWrapper } from '../../service/log-wrapper.js';
import {
TestExecutionResult,
RunFilteredSummary as TestRunFilteredSummary,
ExecutionFilteredResult,
AssertionSummary,
} from '../../models/interface.js';
import {
filterSensitiveData,
generateCSV,
generatePDF,
} from '../../helpers/helper.js';
export class TestExecutionReport {
private getResponse = (execution: TestExecutionResult) => {
if (execution.response.stream) {
return execution.response.stream.toString();
} else if (execution.response.data) {
return execution.response.data;
} else {
return 'Response unavailable';
}
};
private formatExecution = (
execution: TestExecutionResult,
): ExecutionFilteredResult | null => {
try {
return {
id: execution.id,
name: execution.itemName,
url: execution.request?.endpoint,
method: execution.request?.method,
header: execution.request?.headers,
time: execution.completedAt - execution.startedAt || 0,
responseCode: {
code: execution.response?.status || 408,
name: execution.response?.statusText || 'Request Timed out',
time: execution.response?.responseTime || 0,
size: execution.response?.responseSize || 0,
},
response: this.getResponse(execution),
responseHeaders: execution.response.headers || null,
allTests: execution.assertions?.map(
({
error,
actualValue,
expectedValue,
assertion,
action,
key,
metadata,
skipped,
}) => ({
[assertion]: error
? {
status: false,
skipped,
error,
actualValue,
expectedValue,
action,
key,
metadata,
}
: {
status: true,
skipped,
actualValue,
expectedValue,
action,
key,
metadata,
},
}),
),
};
} catch (error) {
LogWrapper.logError(
'0013',
`formatting execution with id ${execution.id}`,
(error as Error).message,
);
return null;
}
};
private getExecutionResults(
executions: TestExecutionResult[],
): ExecutionFilteredResult[] {
try {
LogWrapper.logDebug('0003', 'Formatting execution results.');
return executions
.map(this.formatExecution)
.filter((result) => result !== null);
} catch (e) {
LogWrapper.logError(
'0013',
'processing executions',
(e as Error).message,
);
return [];
}
}
private createFilteredSummary(
collectionId: string,
collectionName: string,
assertionSummary: AssertionSummary[],
results: ExecutionFilteredResult[],
startedAt: number,
completedAt: number,
metadata?: { name: string; version: string; namespace: string },
): TestRunFilteredSummary {
LogWrapper.logDebug(
'0003',
'Creating filtered summary from execution results.',
);
const totalAssertions = assertionSummary.reduce(
(count, item) => count + (item.assertions?.length || 0),
0,
);
const totalFailedAssertions = assertionSummary.reduce((count, item) => {
return (
count +
item.assertions.reduce((innerCount, assertion) => {
return innerCount + (assertion.error ? 1 : 0);
}, 0)
);
}, 0);
const summary: TestRunFilteredSummary = {
id: collectionId,
name: `${collectionName} Collection`,
timestamp: completedAt,
envMetadata: metadata ?? undefined,
totalPass: totalAssertions - totalFailedAssertions,
status: 'finished', // TODO Need to confirm the criteria for marking test as failed
startedAt: startedAt,
totalFail: totalFailedAssertions,
totalTime: completedAt - startedAt,
results: results,
};
return summary;
}
public collectReport(
collectionId: string,
collectionName: string,
assertionSummary: AssertionSummary[],
executions: TestExecutionResult[],
startedAt: number,
completedAt: number,
metadata?: { name: string; version: string; namespace: string },
): TestRunFilteredSummary {
LogWrapper.logInfo('0215', `${collectionName}`);
const results = this.getExecutionResults(executions);
const filteredSummary = this.createFilteredSummary(
collectionId,
collectionName,
assertionSummary,
results,
startedAt,
completedAt,
metadata,
);
return filteredSummary;
}
public getReport(summary: TestRunFilteredSummary[], format: 'PDF' | 'CSV') {
// Any environment variables, tokens, or request/response fields marked
// as secret, sensitive, or under a specified key pattern (e.g., {}SECRET{}, password, token, etc.)
// shall not be included in any exported report.
const cleanSummary = summary.map((item) => filterSensitiveData(item));
// The report shall be exportable in PDF and CSV formats.
if (format === 'CSV') {
return generateCSV(cleanSummary);
} else {
return generatePDF(cleanSummary);
}
}
}