This commit refactors existing text utility functions into the application layer for broad reuse and integrates them across the codebase. Initially, these utilities were confined to test code, which limited their application. Changes: - Move text utilities to the application layer. - Centralize text utilities into dedicated files for better maintainability. - Improve robustness of utility functions with added type checks. - Replace duplicated logic with centralized utility functions throughout the codebase. - Expand unit tests to cover refactored code parts.
101 lines
3.9 KiB
TypeScript
101 lines
3.9 KiB
TypeScript
import { test, expect } from 'vitest';
|
|
import { parseApplication } from '@/application/Parser/ApplicationParser';
|
|
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
|
import { shuffle } from '@/application/Common/Shuffle';
|
|
import { indentText } from '@/application/Common/Text/IndentText';
|
|
import { type UrlStatus, formatUrlStatus } from './StatusChecker/UrlStatus';
|
|
import { getUrlStatusesInParallel, type BatchRequestOptions } from './StatusChecker/BatchStatusChecker';
|
|
import { TestExecutionDetailsLogger } from './TestExecutionDetailsLogger';
|
|
import { extractDocumentationUrls } from './DocumentationUrlExtractor';
|
|
|
|
// arrange
|
|
const logger = new TestExecutionDetailsLogger();
|
|
logger.logTestSectionStartDelimiter();
|
|
const app = parseApplication();
|
|
let urls = extractDocumentationUrls({
|
|
logger,
|
|
urlExclusionPatterns: [
|
|
/^https:\/\/archive\.ph/, // Drops HEAD/GET requests via fetch/curl, responding to Postman/Chromium.
|
|
],
|
|
application: app,
|
|
});
|
|
urls = filterUrlsToEnvironmentCheckLimit(urls);
|
|
logger.logLabeledInformation('URLs submitted for testing', urls.length.toString());
|
|
const requestOptions: BatchRequestOptions = {
|
|
domainOptions: {
|
|
sameDomainParallelize: false, // be nice to our third-party servers
|
|
sameDomainDelayInMs: 5 /* sec */ * 1000,
|
|
},
|
|
requestOptions: {
|
|
retryExponentialBaseInMs: 3 /* sec */ * 1000,
|
|
requestTimeoutInMs: 60 /* sec */ * 1000,
|
|
additionalHeaders: { referer: app.projectDetails.homepage },
|
|
randomizeTlsFingerprint: true,
|
|
},
|
|
followOptions: {
|
|
followRedirects: true,
|
|
enableCookies: true,
|
|
},
|
|
};
|
|
logger.logLabeledInformation('HTTP request options', JSON.stringify(requestOptions, null, 2));
|
|
const testTimeoutInMs = urls.length * 60 /* seconds */ * 1000;
|
|
logger.logLabeledInformation('Scheduled test duration', convertMillisecondsToHumanReadableFormat(testTimeoutInMs));
|
|
logger.logTestSectionEndDelimiter();
|
|
test(`all URLs (${urls.length}) should be alive`, async () => {
|
|
// act
|
|
const results = await getUrlStatusesInParallel(urls, requestOptions);
|
|
// assert
|
|
const deadUrls = results.filter((r) => r.code === undefined || !isOkStatusCode(r.code));
|
|
expect(deadUrls).to.have.lengthOf(
|
|
0,
|
|
formatAssertionMessage([createReportForDeadUrlStatuses(deadUrls)]),
|
|
);
|
|
}, testTimeoutInMs);
|
|
|
|
function isOkStatusCode(statusCode: number): boolean {
|
|
return statusCode >= 200 && statusCode < 300;
|
|
}
|
|
|
|
function createReportForDeadUrlStatuses(deadUrlStatuses: readonly UrlStatus[]): string {
|
|
return `\n${deadUrlStatuses.map((status) => indentText(formatUrlStatus(status))).join('\n---\n')}\n`;
|
|
}
|
|
|
|
function filterUrlsToEnvironmentCheckLimit(originalUrls: string[]): string[] {
|
|
const { RANDOMIZED_URL_CHECK_LIMIT } = process.env;
|
|
logger.logLabeledInformation('URL check limit', RANDOMIZED_URL_CHECK_LIMIT || 'Unlimited');
|
|
if (RANDOMIZED_URL_CHECK_LIMIT !== undefined && RANDOMIZED_URL_CHECK_LIMIT !== '') {
|
|
const maxUrlsInTest = parseInt(RANDOMIZED_URL_CHECK_LIMIT, 10);
|
|
if (Number.isNaN(maxUrlsInTest)) {
|
|
throw new Error(`Invalid URL limit: ${RANDOMIZED_URL_CHECK_LIMIT}`);
|
|
}
|
|
if (maxUrlsInTest < originalUrls.length) {
|
|
return shuffle(originalUrls).slice(0, maxUrlsInTest);
|
|
}
|
|
}
|
|
return originalUrls;
|
|
}
|
|
|
|
function convertMillisecondsToHumanReadableFormat(milliseconds: number): string {
|
|
const timeParts: string[] = [];
|
|
const addTimePart = (amount: number, label: string) => {
|
|
if (amount === 0) {
|
|
return;
|
|
}
|
|
timeParts.push(`${amount} ${label}`);
|
|
};
|
|
|
|
const hours = milliseconds / (1000 * 60 * 60);
|
|
const absoluteHours = Math.floor(hours);
|
|
addTimePart(absoluteHours, 'hours');
|
|
|
|
const minutes = (hours - absoluteHours) * 60;
|
|
const absoluteMinutes = Math.floor(minutes);
|
|
addTimePart(absoluteMinutes, 'minutes');
|
|
|
|
const seconds = (minutes - absoluteMinutes) * 60;
|
|
const absoluteSeconds = Math.floor(seconds);
|
|
addTimePart(absoluteSeconds, 'seconds');
|
|
|
|
return timeParts.join(', ');
|
|
}
|