@testream/dotnet-reporter 0.5.4 → 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/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, apiUrl = DEFAULT_API_URL } = options;
2304
- // Ensure reportId exists
2305
- if (!report.reportId) {
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: report.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: report.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: report.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: report.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, apiUrl = DEFAULT_API_URL } = options;
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
- // Create native FormData with Blob
2441
- const blob = new Blob([new Uint8Array(fileBuffer)], { type: attachment.contentType });
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', attachment.name);
2445
- formData.append('file', blob, path.basename(filePath));
2446
- // Upload to API
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,