@testream/dotnet-reporter 0.5.3 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -2
- package/dist/index.js +191 -20
- package/dist/index.js.map +1 -1
- package/dist/uploader.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -2275,7 +2275,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
2275
2275
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
2276
2276
|
};
|
|
2277
2277
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
2278
|
-
exports.uploadArtifacts = exports.uploadTestRun = exports.detectCIContext = void 0;
|
|
2278
|
+
exports.ensureReportId = exports.mapAttachmentsToTestResults = exports.uploadArtifacts = exports.uploadTestRun = exports.detectCIContext = void 0;
|
|
2279
2279
|
// CTRF types
|
|
2280
2280
|
__exportStar(__nccwpck_require__(827), exports);
|
|
2281
2281
|
var ci_detection_1 = __nccwpck_require__(406);
|
|
@@ -2283,6 +2283,8 @@ Object.defineProperty(exports, "detectCIContext", ({ enumerable: true, get: func
|
|
|
2283
2283
|
var upload_1 = __nccwpck_require__(969);
|
|
2284
2284
|
Object.defineProperty(exports, "uploadTestRun", ({ enumerable: true, get: function () { return upload_1.uploadTestRun; } }));
|
|
2285
2285
|
Object.defineProperty(exports, "uploadArtifacts", ({ enumerable: true, get: function () { return upload_1.uploadArtifacts; } }));
|
|
2286
|
+
Object.defineProperty(exports, "mapAttachmentsToTestResults", ({ enumerable: true, get: function () { return upload_1.mapAttachmentsToTestResults; } }));
|
|
2287
|
+
Object.defineProperty(exports, "ensureReportId", ({ enumerable: true, get: function () { return upload_1.ensureReportId; } }));
|
|
2286
2288
|
//# sourceMappingURL=index.js.map
|
|
2287
2289
|
|
|
2288
2290
|
/***/ }),
|
|
@@ -2293,22 +2295,57 @@ Object.defineProperty(exports, "uploadArtifacts", ({ enumerable: true, get: func
|
|
|
2293
2295
|
"use strict";
|
|
2294
2296
|
|
|
2295
2297
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
2298
|
+
exports.ensureReportId = ensureReportId;
|
|
2296
2299
|
exports.uploadTestRun = uploadTestRun;
|
|
2300
|
+
exports.mapAttachmentsToTestResults = mapAttachmentsToTestResults;
|
|
2297
2301
|
exports.uploadArtifacts = uploadArtifacts;
|
|
2298
2302
|
const DEFAULT_API_URL = 'https://test-manager-backend.fly.dev';
|
|
2303
|
+
function ensureReportId(report) {
|
|
2304
|
+
if (typeof report.reportId === 'string' && report.reportId.trim().length > 0) {
|
|
2305
|
+
return report.reportId;
|
|
2306
|
+
}
|
|
2307
|
+
const reportId = crypto.randomUUID();
|
|
2308
|
+
report.reportId = reportId;
|
|
2309
|
+
return reportId;
|
|
2310
|
+
}
|
|
2311
|
+
function resolveApiUrl(explicitApiUrl) {
|
|
2312
|
+
const fromExplicit = normalizeApiUrl(explicitApiUrl);
|
|
2313
|
+
if (fromExplicit) {
|
|
2314
|
+
return fromExplicit;
|
|
2315
|
+
}
|
|
2316
|
+
const fromEnv = getEnvApiUrl();
|
|
2317
|
+
if (fromEnv) {
|
|
2318
|
+
return fromEnv;
|
|
2319
|
+
}
|
|
2320
|
+
return DEFAULT_API_URL;
|
|
2321
|
+
}
|
|
2322
|
+
function getEnvApiUrl() {
|
|
2323
|
+
if (typeof process === 'undefined' || !process?.env) {
|
|
2324
|
+
return undefined;
|
|
2325
|
+
}
|
|
2326
|
+
return normalizeApiUrl(process.env.TESTREAM_API_URL);
|
|
2327
|
+
}
|
|
2328
|
+
function normalizeApiUrl(value) {
|
|
2329
|
+
if (!value) {
|
|
2330
|
+
return undefined;
|
|
2331
|
+
}
|
|
2332
|
+
const trimmed = value.trim();
|
|
2333
|
+
if (!trimmed) {
|
|
2334
|
+
return undefined;
|
|
2335
|
+
}
|
|
2336
|
+
return trimmed.replace(/\/+$/, '');
|
|
2337
|
+
}
|
|
2299
2338
|
/**
|
|
2300
2339
|
* Upload test run to Testream backend API
|
|
2301
2340
|
*/
|
|
2302
2341
|
async function uploadTestRun(options) {
|
|
2303
|
-
const { report, apiKey
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
report.reportId = crypto.randomUUID();
|
|
2307
|
-
}
|
|
2342
|
+
const { report, apiKey } = options;
|
|
2343
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
2344
|
+
const reportId = ensureReportId(report);
|
|
2308
2345
|
// Build type-safe IngestRequest payload
|
|
2309
2346
|
const ingestPayload = {
|
|
2310
2347
|
report,
|
|
2311
|
-
reportId
|
|
2348
|
+
reportId,
|
|
2312
2349
|
commitSha: options.commitSha,
|
|
2313
2350
|
branch: options.branch,
|
|
2314
2351
|
repositoryUrl: options.repositoryUrl,
|
|
@@ -2337,7 +2374,7 @@ async function uploadTestRun(options) {
|
|
|
2337
2374
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2338
2375
|
return {
|
|
2339
2376
|
success: false,
|
|
2340
|
-
reportId
|
|
2377
|
+
reportId,
|
|
2341
2378
|
error: `Connection failed: ${errorMessage}`,
|
|
2342
2379
|
};
|
|
2343
2380
|
}
|
|
@@ -2346,7 +2383,7 @@ async function uploadTestRun(options) {
|
|
|
2346
2383
|
console.warn('Report already exists (workflow may have been re-run)');
|
|
2347
2384
|
return {
|
|
2348
2385
|
success: true,
|
|
2349
|
-
reportId
|
|
2386
|
+
reportId,
|
|
2350
2387
|
summary: {
|
|
2351
2388
|
passed: report.results.summary.passed,
|
|
2352
2389
|
failed: report.results.summary.failed,
|
|
@@ -2361,7 +2398,7 @@ async function uploadTestRun(options) {
|
|
|
2361
2398
|
const errorText = await response.text();
|
|
2362
2399
|
return {
|
|
2363
2400
|
success: false,
|
|
2364
|
-
reportId
|
|
2401
|
+
reportId,
|
|
2365
2402
|
error: `Upload failed (HTTP ${response.status}): ${errorText}`,
|
|
2366
2403
|
};
|
|
2367
2404
|
}
|
|
@@ -2384,11 +2421,41 @@ async function uploadTestRun(options) {
|
|
|
2384
2421
|
testResults: result.testResults,
|
|
2385
2422
|
};
|
|
2386
2423
|
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Maps CTRF tests to ingested test result IDs in a stable order.
|
|
2426
|
+
* This avoids incorrect artifact linking when multiple tests share the same name.
|
|
2427
|
+
*/
|
|
2428
|
+
function mapAttachmentsToTestResults(tests, ingestedTestResults) {
|
|
2429
|
+
const idsByName = new Map();
|
|
2430
|
+
for (const result of ingestedTestResults) {
|
|
2431
|
+
const queue = idsByName.get(result.name) ?? [];
|
|
2432
|
+
queue.push(result.id);
|
|
2433
|
+
idsByName.set(result.name, queue);
|
|
2434
|
+
}
|
|
2435
|
+
const targets = [];
|
|
2436
|
+
for (const test of tests) {
|
|
2437
|
+
const queue = idsByName.get(test.name);
|
|
2438
|
+
const matchedTestResultId = queue?.shift();
|
|
2439
|
+
if (!test.attachments || test.attachments.length === 0) {
|
|
2440
|
+
continue;
|
|
2441
|
+
}
|
|
2442
|
+
if (!matchedTestResultId) {
|
|
2443
|
+
console.warn(`Skipping artifact upload: could not match test result for "${test.name}"`);
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
targets.push({
|
|
2447
|
+
testResultId: matchedTestResultId,
|
|
2448
|
+
attachments: test.attachments,
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
return targets;
|
|
2452
|
+
}
|
|
2387
2453
|
/**
|
|
2388
2454
|
* Upload test artifacts (screenshots, videos, etc.) to Testream backend API
|
|
2389
2455
|
*/
|
|
2390
2456
|
async function uploadArtifacts(options) {
|
|
2391
|
-
const { reportId, apiKey, testResults
|
|
2457
|
+
const { reportId, apiKey, testResults } = options;
|
|
2458
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
2392
2459
|
let uploadedCount = 0;
|
|
2393
2460
|
for (const testResult of testResults) {
|
|
2394
2461
|
for (const attachment of testResult.attachments) {
|
|
@@ -2430,35 +2497,138 @@ async function uploadSingleArtifact(options) {
|
|
|
2430
2497
|
}
|
|
2431
2498
|
// Read file
|
|
2432
2499
|
let fileBuffer;
|
|
2500
|
+
let sizeBytes;
|
|
2501
|
+
const fileName = path.basename(filePath);
|
|
2502
|
+
const contentType = attachment.contentType || 'application/octet-stream';
|
|
2433
2503
|
try {
|
|
2434
2504
|
fileBuffer = await fs.readFile(filePath);
|
|
2505
|
+
sizeBytes = fileBuffer.byteLength;
|
|
2435
2506
|
}
|
|
2436
2507
|
catch (error) {
|
|
2437
2508
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2438
2509
|
throw new Error(`Failed to read file: ${errorMessage}`);
|
|
2439
2510
|
}
|
|
2440
|
-
|
|
2441
|
-
|
|
2511
|
+
try {
|
|
2512
|
+
return await uploadWithPresignedUrl({
|
|
2513
|
+
testResultId,
|
|
2514
|
+
reportId,
|
|
2515
|
+
apiKey,
|
|
2516
|
+
apiUrl,
|
|
2517
|
+
attachmentName: attachment.name,
|
|
2518
|
+
fileName,
|
|
2519
|
+
contentType,
|
|
2520
|
+
sizeBytes,
|
|
2521
|
+
fileBuffer,
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
catch (error) {
|
|
2525
|
+
if (!(error instanceof LegacyUploadRequiredError)) {
|
|
2526
|
+
throw error;
|
|
2527
|
+
}
|
|
2528
|
+
return await uploadWithLegacyEndpoint({
|
|
2529
|
+
testResultId,
|
|
2530
|
+
attachmentName: attachment.name,
|
|
2531
|
+
reportId,
|
|
2532
|
+
apiKey,
|
|
2533
|
+
apiUrl,
|
|
2534
|
+
fileName,
|
|
2535
|
+
contentType,
|
|
2536
|
+
fileBuffer,
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
class LegacyUploadRequiredError extends Error {
|
|
2541
|
+
constructor() {
|
|
2542
|
+
super('Legacy upload endpoint required');
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
async function uploadWithPresignedUrl(options) {
|
|
2546
|
+
const { testResultId, reportId, apiKey, apiUrl, attachmentName, fileName, contentType, sizeBytes, fileBuffer, } = options;
|
|
2547
|
+
const initPayload = {
|
|
2548
|
+
testResultId,
|
|
2549
|
+
ctrfAttachmentName: attachmentName,
|
|
2550
|
+
fileName,
|
|
2551
|
+
contentType,
|
|
2552
|
+
sizeBytes,
|
|
2553
|
+
};
|
|
2554
|
+
const initResponse = await fetch(`${apiUrl}/api/v1/artifacts/${reportId}/upload-url`, {
|
|
2555
|
+
method: 'POST',
|
|
2556
|
+
headers: {
|
|
2557
|
+
'X-API-KEY': apiKey,
|
|
2558
|
+
'Content-Type': 'application/json',
|
|
2559
|
+
},
|
|
2560
|
+
body: JSON.stringify(initPayload),
|
|
2561
|
+
});
|
|
2562
|
+
if (shouldFallbackToLegacyUpload(initResponse.status)) {
|
|
2563
|
+
throw new LegacyUploadRequiredError();
|
|
2564
|
+
}
|
|
2565
|
+
if (!initResponse.ok) {
|
|
2566
|
+
const errorText = await initResponse.text();
|
|
2567
|
+
throw new Error(`Failed to create direct upload URL (HTTP ${initResponse.status}): ${errorText}`);
|
|
2568
|
+
}
|
|
2569
|
+
const uploadInitResult = (await initResponse.json());
|
|
2570
|
+
const uploadHeaders = { ...(uploadInitResult.requiredHeaders ?? {}) };
|
|
2571
|
+
if (!hasHeaderIgnoreCase(uploadHeaders, 'Content-Type')) {
|
|
2572
|
+
uploadHeaders['Content-Type'] = contentType;
|
|
2573
|
+
}
|
|
2574
|
+
const directUploadResponse = await fetch(uploadInitResult.uploadUrl, {
|
|
2575
|
+
method: 'PUT',
|
|
2576
|
+
headers: uploadHeaders,
|
|
2577
|
+
body: fileBuffer,
|
|
2578
|
+
});
|
|
2579
|
+
if (!directUploadResponse.ok) {
|
|
2580
|
+
const errorText = await directUploadResponse.text();
|
|
2581
|
+
throw new Error(`Direct artifact upload failed (HTTP ${directUploadResponse.status}): ${errorText}`);
|
|
2582
|
+
}
|
|
2583
|
+
const completePayload = {
|
|
2584
|
+
testResultId,
|
|
2585
|
+
ctrfAttachmentName: attachmentName,
|
|
2586
|
+
fileName,
|
|
2587
|
+
contentType,
|
|
2588
|
+
sizeBytes,
|
|
2589
|
+
storageKey: uploadInitResult.storageKey,
|
|
2590
|
+
};
|
|
2591
|
+
const completeResponse = await fetch(`${apiUrl}/api/v1/artifacts/${reportId}/complete`, {
|
|
2592
|
+
method: 'POST',
|
|
2593
|
+
headers: {
|
|
2594
|
+
'X-API-KEY': apiKey,
|
|
2595
|
+
'Content-Type': 'application/json',
|
|
2596
|
+
},
|
|
2597
|
+
body: JSON.stringify(completePayload),
|
|
2598
|
+
});
|
|
2599
|
+
if (!completeResponse.ok) {
|
|
2600
|
+
const errorText = await completeResponse.text();
|
|
2601
|
+
throw new Error(`Failed to finalize artifact upload (HTTP ${completeResponse.status}): ${errorText}`);
|
|
2602
|
+
}
|
|
2603
|
+
return true;
|
|
2604
|
+
}
|
|
2605
|
+
async function uploadWithLegacyEndpoint(options) {
|
|
2606
|
+
const { testResultId, attachmentName, reportId, apiKey, apiUrl, fileName, contentType, fileBuffer } = options;
|
|
2607
|
+
const blob = new Blob([new Uint8Array(fileBuffer)], { type: contentType });
|
|
2442
2608
|
const formData = new FormData();
|
|
2443
2609
|
formData.append('testResultId', testResultId);
|
|
2444
|
-
formData.append('ctrfAttachmentName',
|
|
2445
|
-
formData.append('file', blob,
|
|
2446
|
-
|
|
2447
|
-
const uploadUrl = `${apiUrl}/api/v1/artifacts/${reportId}`;
|
|
2448
|
-
const response = await fetch(uploadUrl, {
|
|
2610
|
+
formData.append('ctrfAttachmentName', attachmentName);
|
|
2611
|
+
formData.append('file', blob, fileName);
|
|
2612
|
+
const response = await fetch(`${apiUrl}/api/v1/artifacts/${reportId}`, {
|
|
2449
2613
|
method: 'POST',
|
|
2450
2614
|
headers: {
|
|
2451
2615
|
'X-API-KEY': apiKey,
|
|
2452
|
-
// Don't set Content-Type - fetch sets it with boundary
|
|
2453
2616
|
},
|
|
2454
2617
|
body: formData,
|
|
2455
2618
|
});
|
|
2456
2619
|
if (!response.ok) {
|
|
2457
2620
|
const errorText = await response.text();
|
|
2458
|
-
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
2621
|
+
throw new Error(`Legacy artifact upload failed (HTTP ${response.status}): ${errorText}`);
|
|
2459
2622
|
}
|
|
2460
2623
|
return true;
|
|
2461
2624
|
}
|
|
2625
|
+
function shouldFallbackToLegacyUpload(statusCode) {
|
|
2626
|
+
return statusCode === 404 || statusCode === 405 || statusCode === 501;
|
|
2627
|
+
}
|
|
2628
|
+
function hasHeaderIgnoreCase(headers, headerName) {
|
|
2629
|
+
const expected = headerName.toLowerCase();
|
|
2630
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === expected);
|
|
2631
|
+
}
|
|
2462
2632
|
//# sourceMappingURL=upload.js.map
|
|
2463
2633
|
|
|
2464
2634
|
/***/ }),
|
|
@@ -3157,6 +3327,7 @@ async function uploadToApi(options) {
|
|
|
3157
3327
|
return (0, shared_types_1.uploadTestRun)({
|
|
3158
3328
|
report: options.report,
|
|
3159
3329
|
apiKey: options.apiKey,
|
|
3330
|
+
apiUrl: options.apiUrl,
|
|
3160
3331
|
commitSha: options.commitSha,
|
|
3161
3332
|
branch: options.branch,
|
|
3162
3333
|
repositoryUrl: options.repositoryUrl,
|