From edbe18fb079641b1e49fe44de68be76aee7873f3 Mon Sep 17 00:00:00 2001 From: BrandonLWhite Date: Thu, 5 Feb 2026 09:53:39 -0600 Subject: [PATCH 1/7] fix: update action usage to correct repository path in examples --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8fbecdb..48693a0 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The official `datadog-ci` CLI requires installing a large npm package with many ```yaml - name: Upload coverage to Datadog - uses: your-org/datadog-upload-code-coverage-action@v1 + uses: python-build-tools/datadog-upload-code-coverage-action@v1 with: api-key: ${{ secrets.DD_API_KEY }} files: '**/coverage/*.xml' @@ -40,7 +40,7 @@ jobs: run: npm test -- --coverage - name: Upload coverage to Datadog - uses: your-org/datadog-upload-code-coverage-action@v1 + uses: python-build-tools/datadog-upload-code-coverage-action@v1 with: api-key: ${{ secrets.DD_API_KEY }} site: 'datadoghq.com' From 4c5d63424661dc656c2072252e9323050c5007e4 Mon Sep 17 00:00:00 2001 From: BrandonLWhite Date: Thu, 5 Feb 2026 09:58:42 -0600 Subject: [PATCH 2/7] test: add fallback handling for unknown XML and JSON formats in file-finder tests --- src/__tests__/file-finder.test.ts | 26 ++++++++++++++++++++++++++ src/__tests__/github-context.test.ts | 13 +++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/__tests__/file-finder.test.ts b/src/__tests__/file-finder.test.ts index 1108d46..9c85607 100644 --- a/src/__tests__/file-finder.test.ts +++ b/src/__tests__/file-finder.test.ts @@ -220,5 +220,31 @@ describe('file-finder', () => { followSymbolicLinks: true, }); }); + + it('should fallback to cobertura format for unknown XML files', async () => { + // Create an XML file that doesn't match any specific format + const unknownXmlPath = path.join(testDir, 'unknown-format.xml'); + fs.writeFileSync(unknownXmlPath, 'data'); + + mockGlob.create.mockResolvedValue(createMockGlobber([unknownXmlPath])); + + const files = await findCoverageFiles('**/*.xml'); + + expect(files).toHaveLength(1); + expect(files[0].format).toBe('cobertura'); + }); + + it('should fallback to json format for unknown JSON files', async () => { + // Create a JSON file that doesn't match any specific format pattern + const unknownJsonPath = path.join(testDir, 'unknown.json'); + fs.writeFileSync(unknownJsonPath, '{"data": "test"}'); + + mockGlob.create.mockResolvedValue(createMockGlobber([unknownJsonPath])); + + const files = await findCoverageFiles('**/*.json'); + + expect(files).toHaveLength(1); + expect(files[0].format).toBe('json'); + }); }); }); diff --git a/src/__tests__/github-context.test.ts b/src/__tests__/github-context.test.ts index f1bec32..d40f6e5 100644 --- a/src/__tests__/github-context.test.ts +++ b/src/__tests__/github-context.test.ts @@ -132,6 +132,19 @@ describe('github-context', () => { expect(result.workspacePath).toBe(process.cwd()); }); + it('should fallback to default serverUrl when context serverUrl is empty', () => { + setContext({ + serverUrl: '', + repo: { owner: 'owner', repo: 'repo' }, + runId: 123, + }); + + const result = getGitHubContext(); + + expect(result.repositoryUrl).toBe('https://github.com/owner/repo.git'); + expect(result.pipelineUrl).toBe('https://github.com/owner/repo/actions/runs/123'); + }); + it('should handle GitHub Enterprise Server URLs', () => { setContext({ serverUrl: 'https://github.mycompany.com', From 2d8a2b12d1ed8aacc201d38808275ecb2f3ff1fc Mon Sep 17 00:00:00 2001 From: BrandonLWhite Date: Thu, 5 Feb 2026 14:54:15 -0600 Subject: [PATCH 3/7] feat: enhance file format detection with error handling for unreadable files --- src/__tests__/file-finder.test.ts | 23 ++++++++++++++++++++++- src/file-finder.ts | 12 ++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/__tests__/file-finder.test.ts b/src/__tests__/file-finder.test.ts index 9c85607..9656de8 100644 --- a/src/__tests__/file-finder.test.ts +++ b/src/__tests__/file-finder.test.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import { findCoverageFiles } from '../file-finder'; +import { findCoverageFiles, FileFinder } from '../file-finder'; // Mock @actions/glob jest.mock('@actions/glob', () => ({ @@ -246,5 +246,26 @@ describe('file-finder', () => { expect(files).toHaveLength(1); expect(files[0].format).toBe('json'); }); + + it('should skip files that cannot be read for content detection', async () => { + // Create a file path that will trigger content detection but fail to read + const testPath = path.join(testDir, 'unreadable.xml'); + // Write the file so statSync works (file exists check) + fs.writeFileSync(testPath, 'dummy content'); + + mockGlob.create.mockResolvedValue(createMockGlobber([testPath])); + + const spySampleFileContent = jest.spyOn(FileFinder, 'sampleFileContent').mockImplementation(_ => { + throw new Error('EACCES: permission denied'); + }); + + try { + const files = await findCoverageFiles('**/*.xml'); + // The file should be skipped due to read error (returns null from detectCoverageFormat) + expect(files.some((f) => f.path === testPath)).toBe(false); + } finally { + spySampleFileContent.mockRestore(); + } + }); }); }); diff --git a/src/file-finder.ts b/src/file-finder.ts index a2f085a..45fe3dc 100644 --- a/src/file-finder.ts +++ b/src/file-finder.ts @@ -7,7 +7,15 @@ export interface CoverageFile { format: string; } -// Coverage format detection based on file patterns and content +export class FileFinder { + /** + * Reads file content for format detection. Extracted for testability. + */ + static sampleFileContent(filePath: string): string { + return fs.readFileSync(filePath, 'utf-8').slice(0, 2000); // Read first 2KB for detection + } +} + const FORMAT_PATTERNS: { pattern: RegExp; format: string; contentCheck?: (content: string) => boolean }[] = [ // JaCoCo - XML format with jacoco in root element { @@ -74,7 +82,7 @@ function detectFormat(filePath: string, content?: string): string | null { // Second pass: need to check content for files that require content checking if (!content && (filePath.endsWith('.xml') || filePath.endsWith('.json') || filePath.endsWith('.out'))) { try { - content = fs.readFileSync(filePath, 'utf-8').slice(0, 2000); // Read first 2KB for detection + content = FileFinder.sampleFileContent(filePath); } catch { return null; } From b34560ca551eea35dbf8499ced4fd3cdc67e74c8 Mon Sep 17 00:00:00 2001 From: BrandonLWhite Date: Thu, 5 Feb 2026 14:59:21 -0600 Subject: [PATCH 4/7] feat: add tests for detecting Cobertura XML files with and without line-rate --- src/__tests__/file-finder.test.ts | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/__tests__/file-finder.test.ts b/src/__tests__/file-finder.test.ts index 9656de8..710b156 100644 --- a/src/__tests__/file-finder.test.ts +++ b/src/__tests__/file-finder.test.ts @@ -104,6 +104,38 @@ describe('file-finder', () => { expect(files[0].format).toBe('cobertura'); }); + it('should detect Cobertura XML files with line-rate but without cobertura keyword', async () => { + // Test the line-rate branch of the Cobertura content check + const filePath = path.join(testDir, 'coverage-linerate.xml'); + fs.writeFileSync( + filePath, + 'some content' + ); + mockGlob.create.mockResolvedValue(createMockGlobber([filePath])); + + const files = await findCoverageFiles('**/*.xml'); + + expect(files).toHaveLength(1); + expect(files[0].path).toBe(filePath); + expect(files[0].format).toBe('cobertura'); + }); + + it('should detect Cobertura XML files with cobertura keyword but without line-rate', async () => { + // Test the cobertura keyword branch of the Cobertura content check + const filePath = path.join(testDir, 'coverage-cobertura-keyword.xml'); + fs.writeFileSync( + filePath, + 'some content' + ); + mockGlob.create.mockResolvedValue(createMockGlobber([filePath])); + + const files = await findCoverageFiles('**/*.xml'); + + expect(files).toHaveLength(1); + expect(files[0].path).toBe(filePath); + expect(files[0].format).toBe('cobertura'); + }); + it('should find and detect Clover XML files', async () => { const filePath = path.join(testDir, 'clover.xml'); mockGlob.create.mockResolvedValue(createMockGlobber([filePath])); From c277c366173f4f101dc8962e2e5cbbe34cb2bdeb Mon Sep 17 00:00:00 2001 From: BrandonLWhite Date: Thu, 5 Feb 2026 17:38:43 -0600 Subject: [PATCH 5/7] feat: add retry logic for transient axios errors in uploadCoverageFiles test --- src/__tests__/uploader.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/__tests__/uploader.test.ts b/src/__tests__/uploader.test.ts index a852e79..192991a 100644 --- a/src/__tests__/uploader.test.ts +++ b/src/__tests__/uploader.test.ts @@ -182,6 +182,31 @@ describe('uploader', () => { expect(mockCore.warning).toHaveBeenCalledTimes(2); }); + it('should retry on transient axios errors and log warning with axios message', async () => { + const options: UploadOptions = { + ...baseOptions, + files: [{ path: testFile, format: 'cobertura' }], + }; + + // Create an axios error with a 500 status (retryable) + const axiosError = { + response: { status: 500, data: 'Internal Server Error' }, + message: 'Request failed with status code 500', + isAxiosError: true, + }; + mockAxiosPost + .mockRejectedValueOnce(axiosError) + .mockResolvedValueOnce({ status: 200, data: {} } as AxiosResponse); + mockIsAxiosError.mockReturnValue(true); + + await uploadCoverageFiles(options); + + expect(mockAxiosPost).toHaveBeenCalledTimes(2); + expect(mockCore.warning).toHaveBeenCalledWith( + 'Upload attempt 1/3 failed: Request failed with status code 500' + ); + }); + it('should fail immediately on 400 error', async () => { const options: UploadOptions = { ...baseOptions, From 7fc81b6a42ae28dd1a2fddd12dcd01221d85d9db Mon Sep 17 00:00:00 2001 From: BrandonLWhite Date: Thu, 5 Feb 2026 18:01:58 -0600 Subject: [PATCH 6/7] test: enhance uploadCoverageFiles tests with error handling and context variations --- src/__tests__/uploader.test.ts | 181 ++++++++++++++++++++++++++++++++- 1 file changed, 180 insertions(+), 1 deletion(-) diff --git a/src/__tests__/uploader.test.ts b/src/__tests__/uploader.test.ts index 192991a..8dbfa06 100644 --- a/src/__tests__/uploader.test.ts +++ b/src/__tests__/uploader.test.ts @@ -175,6 +175,7 @@ describe('uploader', () => { .mockRejectedValueOnce(error) .mockRejectedValueOnce(error) .mockResolvedValueOnce({ status: 200, data: {} } as AxiosResponse); + mockIsAxiosError.mockReturnValue(false); await uploadCoverageFiles(options); @@ -255,8 +256,13 @@ describe('uploader', () => { files: [{ path: testFile, format: 'cobertura' }], }; + // Use an actual Error instance to ensure line 115's true branch is covered const error = new Error('Persistent network error'); - mockAxiosPost.mockRejectedValue(error); + mockAxiosPost + .mockRejectedValueOnce(error) + .mockRejectedValueOnce(error) + .mockRejectedValueOnce(error); + mockIsAxiosError.mockReturnValue(false); await expect(uploadCoverageFiles(options)).rejects.toThrow( 'Persistent network error' @@ -358,5 +364,178 @@ describe('uploader', () => { // The form data should contain PR tags - we verify the call was made // The actual tag values are included in the FormData }); + + it('should handle context without optional branch field', async () => { + const noBranchContext: GitHubContext = { + ...mockContext, + branch: undefined, + }; + + const options: UploadOptions = { + ...baseOptions, + context: noBranchContext, + files: [{ path: testFile, format: 'cobertura' }], + }; + + mockAxiosPost.mockResolvedValueOnce({ status: 200, data: {} } as AxiosResponse); + + await uploadCoverageFiles(options); + + expect(mockAxiosPost).toHaveBeenCalledTimes(1); + }); + + it('should handle non-Error objects thrown during upload', async () => { + const options: UploadOptions = { + ...baseOptions, + files: [{ path: testFile, format: 'cobertura' }], + }; + + // Throw a string instead of an Error object to hit the non-Error branch + mockAxiosPost + .mockRejectedValueOnce('string error') + .mockRejectedValueOnce('string error') + .mockRejectedValueOnce('string error'); + mockIsAxiosError.mockReturnValue(false); + + await expect(uploadCoverageFiles(options)).rejects.toThrow('string error'); + + expect(mockAxiosPost).toHaveBeenCalledTimes(3); + expect(mockCore.warning).toHaveBeenCalledWith( + 'Upload attempt 1/3 failed: string error' + ); + }); + + it('should handle null thrown during upload', async () => { + const options: UploadOptions = { + ...baseOptions, + files: [{ path: testFile, format: 'cobertura' }], + }; + + // Throw null to fully test the non-Error branch + mockAxiosPost + .mockRejectedValueOnce(null) + .mockRejectedValueOnce(null) + .mockRejectedValueOnce(null); + mockIsAxiosError.mockReturnValue(false); + + await expect(uploadCoverageFiles(options)).rejects.toThrow('null'); + + expect(mockAxiosPost).toHaveBeenCalledTimes(3); + }); + + it('should handle axios error without response data', async () => { + const options: UploadOptions = { + ...baseOptions, + files: [{ path: testFile, format: 'cobertura' }], + }; + + // Create an axios error with 400 status but no response data + const axiosError = { + response: { status: 400, data: undefined }, + message: 'Bad Request', + isAxiosError: true, + }; + mockAxiosPost.mockRejectedValueOnce(axiosError); + mockIsAxiosError.mockReturnValue(true); + + await expect(uploadCoverageFiles(options)).rejects.toThrow( + 'Upload failed with status 400: Bad Request' + ); + + expect(mockAxiosPost).toHaveBeenCalledTimes(1); + }); + + it('should handle axios error with 403 status and response data', async () => { + const options: UploadOptions = { + ...baseOptions, + files: [{ path: testFile, format: 'cobertura' }], + }; + + const axiosError = { + response: { status: 403, data: 'Invalid API key' }, + message: 'Forbidden', + isAxiosError: true, + }; + mockAxiosPost.mockRejectedValueOnce(axiosError); + mockIsAxiosError.mockReturnValue(true); + + await expect(uploadCoverageFiles(options)).rejects.toThrow( + 'Upload failed with status 403: Invalid API key' + ); + + expect(mockAxiosPost).toHaveBeenCalledTimes(1); + }); + + it('should use empty flags array without adding to event', async () => { + const options: UploadOptions = { + ...baseOptions, + files: [{ path: testFile, format: 'cobertura' }], + flags: [], + }; + + mockAxiosPost.mockResolvedValueOnce({ status: 200, data: {} } as AxiosResponse); + + await uploadCoverageFiles(options); + + expect(mockAxiosPost).toHaveBeenCalledTimes(1); + }); + + it('should use unknown format when file has empty format string', async () => { + const options: UploadOptions = { + ...baseOptions, + files: [{ path: testFile, format: '' }], + }; + + mockAxiosPost.mockResolvedValueOnce({ status: 200, data: {} } as AxiosResponse); + + await uploadCoverageFiles(options); + + expect(mockAxiosPost).toHaveBeenCalledTimes(1); + }); + + it('should throw default error when response has non-2xx status without exception', async () => { + const options: UploadOptions = { + ...baseOptions, + files: [{ path: testFile, format: 'cobertura' }], + }; + + // Return non-2xx status without throwing - this makes lastError undefined + mockAxiosPost + .mockResolvedValueOnce({ status: 500, data: {} } as AxiosResponse) + .mockResolvedValueOnce({ status: 502, data: {} } as AxiosResponse) + .mockResolvedValueOnce({ status: 503, data: {} } as AxiosResponse); + + await expect(uploadCoverageFiles(options)).rejects.toThrow( + 'Upload failed after all retries' + ); + + expect(mockAxiosPost).toHaveBeenCalledTimes(3); + }); + + it('should handle Error instance thrown during upload and preserve it', async () => { + const options: UploadOptions = { + ...baseOptions, + files: [{ path: testFile, format: 'cobertura' }], + }; + + // Create an actual Error instance to test the true branch of instanceof Error + const error = new Error('Actual Error instance'); + mockAxiosPost + .mockRejectedValueOnce(error) + .mockRejectedValueOnce(error) + .mockRejectedValueOnce(error); + mockIsAxiosError.mockReturnValue(false); + + // The thrown error should be the exact same Error instance + try { + await uploadCoverageFiles(options); + fail('Expected an error to be thrown'); + } catch (e) { + expect(e).toBe(error); + expect(e).toBeInstanceOf(Error); + } + + expect(mockAxiosPost).toHaveBeenCalledTimes(3); + }); }); }); From 25261b27d002cca4a61b809dcd654d5f4eed5ca8 Mon Sep 17 00:00:00 2001 From: BrandonLWhite Date: Thu, 5 Feb 2026 18:06:09 -0600 Subject: [PATCH 7/7] format and build --- dist/index.js | 11 +- package-lock.json | 17 ++ package.json | 1 + src/__tests__/file-finder.test.ts | 207 +++++++++--------- src/__tests__/github-context.test.ts | 196 +++++++++-------- src/__tests__/index.test.ts | 284 ++++++++++++------------ src/__tests__/uploader.test.ts | 308 +++++++++++++++------------ 7 files changed, 567 insertions(+), 457 deletions(-) diff --git a/dist/index.js b/dist/index.js index 63a9762..fed2db9 100644 --- a/dist/index.js +++ b/dist/index.js @@ -44341,7 +44341,14 @@ function glob_hashFiles(patterns_1) { -// Coverage format detection based on file patterns and content +class FileFinder { + /** + * Reads file content for format detection. Extracted for testability. + */ + static sampleFileContent(filePath) { + return external_fs_.readFileSync(filePath, 'utf-8').slice(0, 2000); // Read first 2KB for detection + } +} const FORMAT_PATTERNS = [ // JaCoCo - XML format with jacoco in root element { @@ -44405,7 +44412,7 @@ function detectFormat(filePath, content) { // Second pass: need to check content for files that require content checking if (!content && (filePath.endsWith('.xml') || filePath.endsWith('.json') || filePath.endsWith('.out'))) { try { - content = external_fs_.readFileSync(filePath, 'utf-8').slice(0, 2000); // Read first 2KB for detection + content = FileFinder.sampleFileContent(filePath); } catch { return null; diff --git a/package-lock.json b/package-lock.json index 7bd46c6..4cfe8f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@vercel/ncc": "^0.38.1", "eslint": "^9.39.2", "jest": "^30.2.0", + "prettier": "^3.8.1", "ts-jest": "^29.1.2", "typescript": "^5.3.3", "typescript-eslint": "^8.54.0" @@ -5185,6 +5186,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", diff --git a/package.json b/package.json index 3a690ea..d533af1 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@vercel/ncc": "^0.38.1", "eslint": "^9.39.2", "jest": "^30.2.0", + "prettier": "^3.8.1", "ts-jest": "^29.1.2", "typescript": "^5.3.3", "typescript-eslint": "^8.54.0" diff --git a/src/__tests__/file-finder.test.ts b/src/__tests__/file-finder.test.ts index 710b156..89e7714 100644 --- a/src/__tests__/file-finder.test.ts +++ b/src/__tests__/file-finder.test.ts @@ -1,18 +1,18 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { findCoverageFiles, FileFinder } from '../file-finder'; +import * as fs from "fs"; +import * as path from "path"; +import { findCoverageFiles, FileFinder } from "../file-finder"; // Mock @actions/glob -jest.mock('@actions/glob', () => ({ +jest.mock("@actions/glob", () => ({ create: jest.fn(), })); -import * as glob from '@actions/glob'; +import * as glob from "@actions/glob"; const mockGlob = glob as jest.Mocked; -describe('file-finder', () => { - const testDir = path.join(__dirname, 'fixtures'); +describe("file-finder", () => { + const testDir = path.join(__dirname, "fixtures"); beforeAll(() => { // Create test fixtures directory @@ -22,40 +22,46 @@ describe('file-finder', () => { // Create test files fs.writeFileSync( - path.join(testDir, 'jacoco.xml'), - 'jacoco content' + path.join(testDir, "jacoco.xml"), + 'jacoco content', ); fs.writeFileSync( - path.join(testDir, 'cobertura.xml'), - 'cobertura content' + path.join(testDir, "cobertura.xml"), + 'cobertura content', ); fs.writeFileSync( - path.join(testDir, 'clover.xml'), - 'clover content' + path.join(testDir, "clover.xml"), + 'clover content', ); - fs.writeFileSync(path.join(testDir, 'lcov.info'), 'SF:src/file.ts\nDA:1,1\nend_of_record'); + fs.writeFileSync( + path.join(testDir, "lcov.info"), + "SF:src/file.ts\nDA:1,1\nend_of_record", + ); - fs.writeFileSync(path.join(testDir, 'coverage.out'), 'mode: atomic\nfile.go:1.1,2.2 1 1'); + fs.writeFileSync( + path.join(testDir, "coverage.out"), + "mode: atomic\nfile.go:1.1,2.2 1 1", + ); fs.writeFileSync( - path.join(testDir, 'coverage.resultset.json'), - '{"RSpec":{"coverage":{}}}' + path.join(testDir, "coverage.resultset.json"), + '{"RSpec":{"coverage":{}}}', ); fs.writeFileSync( - path.join(testDir, 'opencover.xml'), - 'OpenCover content' + path.join(testDir, "opencover.xml"), + 'OpenCover content', ); fs.writeFileSync( - path.join(testDir, 'coverage.json'), - '{"total":{"lines":100}}' + path.join(testDir, "coverage.json"), + '{"total":{"lines":100}}', ); - fs.writeFileSync(path.join(testDir, 'random.txt'), 'not a coverage file'); + fs.writeFileSync(path.join(testDir, "random.txt"), "not a coverage file"); }); afterAll(() => { @@ -65,7 +71,7 @@ describe('file-finder', () => { } }); - describe('findCoverageFiles', () => { + describe("findCoverageFiles", () => { function createMockGlobber(files: string[]) { return { glob: jest.fn().mockResolvedValue(files), @@ -82,217 +88,222 @@ describe('file-finder', () => { jest.clearAllMocks(); }); - it('should find and detect JaCoCo XML files', async () => { - const filePath = path.join(testDir, 'jacoco.xml'); + it("should find and detect JaCoCo XML files", async () => { + const filePath = path.join(testDir, "jacoco.xml"); mockGlob.create.mockResolvedValue(createMockGlobber([filePath])); - const files = await findCoverageFiles('**/*.xml'); + const files = await findCoverageFiles("**/*.xml"); expect(files).toHaveLength(1); expect(files[0].path).toBe(filePath); - expect(files[0].format).toBe('jacoco'); + expect(files[0].format).toBe("jacoco"); }); - it('should find and detect Cobertura XML files', async () => { - const filePath = path.join(testDir, 'cobertura.xml'); + it("should find and detect Cobertura XML files", async () => { + const filePath = path.join(testDir, "cobertura.xml"); mockGlob.create.mockResolvedValue(createMockGlobber([filePath])); - const files = await findCoverageFiles('**/*.xml'); + const files = await findCoverageFiles("**/*.xml"); expect(files).toHaveLength(1); expect(files[0].path).toBe(filePath); - expect(files[0].format).toBe('cobertura'); + expect(files[0].format).toBe("cobertura"); }); - it('should detect Cobertura XML files with line-rate but without cobertura keyword', async () => { + it("should detect Cobertura XML files with line-rate but without cobertura keyword", async () => { // Test the line-rate branch of the Cobertura content check - const filePath = path.join(testDir, 'coverage-linerate.xml'); + const filePath = path.join(testDir, "coverage-linerate.xml"); fs.writeFileSync( filePath, - 'some content' + 'some content', ); mockGlob.create.mockResolvedValue(createMockGlobber([filePath])); - const files = await findCoverageFiles('**/*.xml'); + const files = await findCoverageFiles("**/*.xml"); expect(files).toHaveLength(1); expect(files[0].path).toBe(filePath); - expect(files[0].format).toBe('cobertura'); + expect(files[0].format).toBe("cobertura"); }); - it('should detect Cobertura XML files with cobertura keyword but without line-rate', async () => { + it("should detect Cobertura XML files with cobertura keyword but without line-rate", async () => { // Test the cobertura keyword branch of the Cobertura content check - const filePath = path.join(testDir, 'coverage-cobertura-keyword.xml'); + const filePath = path.join(testDir, "coverage-cobertura-keyword.xml"); fs.writeFileSync( filePath, - 'some content' + 'some content', ); mockGlob.create.mockResolvedValue(createMockGlobber([filePath])); - const files = await findCoverageFiles('**/*.xml'); + const files = await findCoverageFiles("**/*.xml"); expect(files).toHaveLength(1); expect(files[0].path).toBe(filePath); - expect(files[0].format).toBe('cobertura'); + expect(files[0].format).toBe("cobertura"); }); - it('should find and detect Clover XML files', async () => { - const filePath = path.join(testDir, 'clover.xml'); + it("should find and detect Clover XML files", async () => { + const filePath = path.join(testDir, "clover.xml"); mockGlob.create.mockResolvedValue(createMockGlobber([filePath])); - const files = await findCoverageFiles('**/clover.xml'); + const files = await findCoverageFiles("**/clover.xml"); expect(files).toHaveLength(1); expect(files[0].path).toBe(filePath); - expect(files[0].format).toBe('clover'); + expect(files[0].format).toBe("clover"); }); - it('should find and detect LCOV files', async () => { - const filePath = path.join(testDir, 'lcov.info'); + it("should find and detect LCOV files", async () => { + const filePath = path.join(testDir, "lcov.info"); mockGlob.create.mockResolvedValue(createMockGlobber([filePath])); - const files = await findCoverageFiles('**/*.info'); + const files = await findCoverageFiles("**/*.info"); expect(files).toHaveLength(1); expect(files[0].path).toBe(filePath); - expect(files[0].format).toBe('lcov'); + expect(files[0].format).toBe("lcov"); }); - it('should find and detect Go coverage files', async () => { + it("should find and detect Go coverage files", async () => { // Go files need content check - the file starts with "mode:" - const filePath = path.join(testDir, 'coverage.out'); + const filePath = path.join(testDir, "coverage.out"); mockGlob.create.mockResolvedValue(createMockGlobber([filePath])); - const files = await findCoverageFiles('**/*.out'); + const files = await findCoverageFiles("**/*.out"); expect(files).toHaveLength(1); expect(files[0].path).toBe(filePath); - expect(files[0].format).toBe('go'); + expect(files[0].format).toBe("go"); }); - it('should find and detect SimpleCov JSON files', async () => { - const filePath = path.join(testDir, 'coverage.resultset.json'); + it("should find and detect SimpleCov JSON files", async () => { + const filePath = path.join(testDir, "coverage.resultset.json"); mockGlob.create.mockResolvedValue(createMockGlobber([filePath])); - const files = await findCoverageFiles('**/*.resultset.json'); + const files = await findCoverageFiles("**/*.resultset.json"); expect(files).toHaveLength(1); expect(files[0].path).toBe(filePath); - expect(files[0].format).toBe('simplecov-internal'); + expect(files[0].format).toBe("simplecov-internal"); }); - it('should find and detect OpenCover XML files', async () => { - const filePath = path.join(testDir, 'opencover.xml'); + it("should find and detect OpenCover XML files", async () => { + const filePath = path.join(testDir, "opencover.xml"); mockGlob.create.mockResolvedValue(createMockGlobber([filePath])); - const files = await findCoverageFiles('**/*.xml'); + const files = await findCoverageFiles("**/*.xml"); expect(files).toHaveLength(1); expect(files[0].path).toBe(filePath); - expect(files[0].format).toBe('opencover'); + expect(files[0].format).toBe("opencover"); }); - it('should find and detect generic coverage JSON files', async () => { - const filePath = path.join(testDir, 'coverage.json'); + it("should find and detect generic coverage JSON files", async () => { + const filePath = path.join(testDir, "coverage.json"); mockGlob.create.mockResolvedValue(createMockGlobber([filePath])); - const files = await findCoverageFiles('**/coverage*.json'); + const files = await findCoverageFiles("**/coverage*.json"); expect(files).toHaveLength(1); expect(files[0].path).toBe(filePath); - expect(files[0].format).toBe('json'); + expect(files[0].format).toBe("json"); }); - it('should skip directories', async () => { + it("should skip directories", async () => { mockGlob.create.mockResolvedValue(createMockGlobber([testDir])); - const files = await findCoverageFiles('**/*'); + const files = await findCoverageFiles("**/*"); expect(files).toHaveLength(0); }); - it('should skip unrecognized files', async () => { - const filePath = path.join(testDir, 'random.txt'); + it("should skip unrecognized files", async () => { + const filePath = path.join(testDir, "random.txt"); mockGlob.create.mockResolvedValue(createMockGlobber([filePath])); - const files = await findCoverageFiles('**/*.txt'); + const files = await findCoverageFiles("**/*.txt"); expect(files).toHaveLength(0); }); - it('should handle multiple files', async () => { + it("should handle multiple files", async () => { const files = [ - path.join(testDir, 'jacoco.xml'), - path.join(testDir, 'lcov.info'), - path.join(testDir, 'coverage.out'), + path.join(testDir, "jacoco.xml"), + path.join(testDir, "lcov.info"), + path.join(testDir, "coverage.out"), ]; mockGlob.create.mockResolvedValue(createMockGlobber(files)); - const result = await findCoverageFiles('**/*'); + const result = await findCoverageFiles("**/*"); expect(result.length).toBeGreaterThanOrEqual(2); - expect(result.map((f) => f.format)).toContain('jacoco'); - expect(result.map((f) => f.format)).toContain('lcov'); + expect(result.map((f) => f.format)).toContain("jacoco"); + expect(result.map((f) => f.format)).toContain("lcov"); }); - it('should handle empty glob results', async () => { + it("should handle empty glob results", async () => { mockGlob.create.mockResolvedValue(createMockGlobber([])); - const files = await findCoverageFiles('**/nonexistent.*'); + const files = await findCoverageFiles("**/nonexistent.*"); expect(files).toHaveLength(0); }); - it('should use followSymbolicLinks option', async () => { + it("should use followSymbolicLinks option", async () => { mockGlob.create.mockResolvedValue(createMockGlobber([])); - await findCoverageFiles('**/*'); + await findCoverageFiles("**/*"); - expect(mockGlob.create).toHaveBeenCalledWith('**/*', { + expect(mockGlob.create).toHaveBeenCalledWith("**/*", { followSymbolicLinks: true, }); }); - it('should fallback to cobertura format for unknown XML files', async () => { + it("should fallback to cobertura format for unknown XML files", async () => { // Create an XML file that doesn't match any specific format - const unknownXmlPath = path.join(testDir, 'unknown-format.xml'); - fs.writeFileSync(unknownXmlPath, 'data'); + const unknownXmlPath = path.join(testDir, "unknown-format.xml"); + fs.writeFileSync( + unknownXmlPath, + 'data', + ); mockGlob.create.mockResolvedValue(createMockGlobber([unknownXmlPath])); - const files = await findCoverageFiles('**/*.xml'); + const files = await findCoverageFiles("**/*.xml"); expect(files).toHaveLength(1); - expect(files[0].format).toBe('cobertura'); + expect(files[0].format).toBe("cobertura"); }); - it('should fallback to json format for unknown JSON files', async () => { + it("should fallback to json format for unknown JSON files", async () => { // Create a JSON file that doesn't match any specific format pattern - const unknownJsonPath = path.join(testDir, 'unknown.json'); + const unknownJsonPath = path.join(testDir, "unknown.json"); fs.writeFileSync(unknownJsonPath, '{"data": "test"}'); mockGlob.create.mockResolvedValue(createMockGlobber([unknownJsonPath])); - const files = await findCoverageFiles('**/*.json'); + const files = await findCoverageFiles("**/*.json"); expect(files).toHaveLength(1); - expect(files[0].format).toBe('json'); + expect(files[0].format).toBe("json"); }); - it('should skip files that cannot be read for content detection', async () => { + it("should skip files that cannot be read for content detection", async () => { // Create a file path that will trigger content detection but fail to read - const testPath = path.join(testDir, 'unreadable.xml'); + const testPath = path.join(testDir, "unreadable.xml"); // Write the file so statSync works (file exists check) - fs.writeFileSync(testPath, 'dummy content'); + fs.writeFileSync(testPath, "dummy content"); mockGlob.create.mockResolvedValue(createMockGlobber([testPath])); - const spySampleFileContent = jest.spyOn(FileFinder, 'sampleFileContent').mockImplementation(_ => { - throw new Error('EACCES: permission denied'); - }); + const spySampleFileContent = jest + .spyOn(FileFinder, "sampleFileContent") + .mockImplementation((_) => { + throw new Error("EACCES: permission denied"); + }); try { - const files = await findCoverageFiles('**/*.xml'); + const files = await findCoverageFiles("**/*.xml"); // The file should be skipped due to read error (returns null from detectCoverageFormat) expect(files.some((f) => f.path === testPath)).toBe(false); } finally { diff --git a/src/__tests__/github-context.test.ts b/src/__tests__/github-context.test.ts index d40f6e5..15dd4c5 100644 --- a/src/__tests__/github-context.test.ts +++ b/src/__tests__/github-context.test.ts @@ -1,10 +1,10 @@ -import { getGitHubContext } from '../github-context'; -import { resetContext, setContext } from '../__mocks__/@actions/github'; +import { getGitHubContext } from "../github-context"; +import { resetContext, setContext } from "../__mocks__/@actions/github"; // Mock @actions/github -jest.mock('@actions/github'); +jest.mock("@actions/github"); -describe('github-context', () => { +describe("github-context", () => { const originalEnv = process.env; beforeEach(() => { @@ -26,172 +26,188 @@ describe('github-context', () => { process.env = originalEnv; }); - describe('getGitHubContext', () => { - it('should return default values when context is empty', () => { + describe("getGitHubContext", () => { + it("should return default values when context is empty", () => { const result = getGitHubContext(); - expect(result.repositoryUrl).toBe('https://github.com//.git'); - expect(result.commitSha).toBe(''); + expect(result.repositoryUrl).toBe("https://github.com//.git"); + expect(result.commitSha).toBe(""); expect(result.branch).toBeUndefined(); expect(result.tag).toBeUndefined(); expect(result.pullRequestBaseBranch).toBeUndefined(); expect(result.pullRequestHeadSha).toBeUndefined(); expect(result.pullRequestBaseBranchHeadSha).toBeUndefined(); expect(result.pullRequestNumber).toBeUndefined(); - expect(result.pipelineId).toBe('0'); - expect(result.pipelineName).toBe('/'); - expect(result.pipelineNumber).toBe('0'); - expect(result.jobName).toBe(''); + expect(result.pipelineId).toBe("0"); + expect(result.pipelineName).toBe("/"); + expect(result.pipelineNumber).toBe("0"); + expect(result.jobName).toBe(""); }); - it('should use GitHub context values', () => { + it("should use GitHub context values", () => { setContext({ - serverUrl: 'https://github.com', - repo: { owner: 'owner', repo: 'repo' }, + serverUrl: "https://github.com", + repo: { owner: "owner", repo: "repo" }, runId: 12345, runNumber: 42, - job: 'test-job', - sha: 'abc123def456', + job: "test-job", + sha: "abc123def456", }); - process.env.GITHUB_HEAD_REF = 'feature-branch'; - process.env.GITHUB_WORKSPACE = '/home/runner/work/repo'; + process.env.GITHUB_HEAD_REF = "feature-branch"; + process.env.GITHUB_WORKSPACE = "/home/runner/work/repo"; const result = getGitHubContext(); - expect(result.repositoryUrl).toBe('https://github.com/owner/repo.git'); - expect(result.commitSha).toBe('abc123def456'); - expect(result.branch).toBe('feature-branch'); - expect(result.pipelineId).toBe('12345'); - expect(result.pipelineName).toBe('owner/repo'); - expect(result.pipelineNumber).toBe('42'); - expect(result.jobName).toBe('test-job'); - expect(result.pipelineUrl).toBe('https://github.com/owner/repo/actions/runs/12345'); - expect(result.jobUrl).toBe('https://github.com/owner/repo/actions/runs/12345/job/test-job'); - expect(result.workspacePath).toBe('/home/runner/work/repo'); + expect(result.repositoryUrl).toBe("https://github.com/owner/repo.git"); + expect(result.commitSha).toBe("abc123def456"); + expect(result.branch).toBe("feature-branch"); + expect(result.pipelineId).toBe("12345"); + expect(result.pipelineName).toBe("owner/repo"); + expect(result.pipelineNumber).toBe("42"); + expect(result.jobName).toBe("test-job"); + expect(result.pipelineUrl).toBe( + "https://github.com/owner/repo/actions/runs/12345", + ); + expect(result.jobUrl).toBe( + "https://github.com/owner/repo/actions/runs/12345/job/test-job", + ); + expect(result.workspacePath).toBe("/home/runner/work/repo"); }); - it('should prefer DD_GIT_* env vars over GitHub context', () => { + it("should prefer DD_GIT_* env vars over GitHub context", () => { setContext({ - repo: { owner: 'owner', repo: 'repo' }, - sha: 'github-sha', + repo: { owner: "owner", repo: "repo" }, + sha: "github-sha", }); - process.env.GITHUB_HEAD_REF = 'github-branch'; + process.env.GITHUB_HEAD_REF = "github-branch"; - process.env.DD_GIT_REPOSITORY_URL = 'https://example.com/custom/repo.git'; - process.env.DD_GIT_COMMIT_SHA = 'dd-custom-sha'; - process.env.DD_GIT_BRANCH = 'dd-custom-branch'; - process.env.DD_GIT_TAG = 'v1.0.0'; + process.env.DD_GIT_REPOSITORY_URL = "https://example.com/custom/repo.git"; + process.env.DD_GIT_COMMIT_SHA = "dd-custom-sha"; + process.env.DD_GIT_BRANCH = "dd-custom-branch"; + process.env.DD_GIT_TAG = "v1.0.0"; const result = getGitHubContext(); - expect(result.repositoryUrl).toBe('https://example.com/custom/repo.git'); - expect(result.commitSha).toBe('dd-custom-sha'); - expect(result.branch).toBe('dd-custom-branch'); - expect(result.tag).toBe('v1.0.0'); + expect(result.repositoryUrl).toBe("https://example.com/custom/repo.git"); + expect(result.commitSha).toBe("dd-custom-sha"); + expect(result.branch).toBe("dd-custom-branch"); + expect(result.tag).toBe("v1.0.0"); }); - it('should fallback to GITHUB_REF_NAME when GITHUB_HEAD_REF is not set', () => { - process.env.GITHUB_REF_NAME = 'main'; + it("should fallback to GITHUB_REF_NAME when GITHUB_HEAD_REF is not set", () => { + process.env.GITHUB_REF_NAME = "main"; const result = getGitHubContext(); - expect(result.branch).toBe('main'); + expect(result.branch).toBe("main"); }); - it('should filter sensitive info from repository URL with credentials', () => { - process.env.DD_GIT_REPOSITORY_URL = 'https://user:password@github.com/owner/repo.git'; + it("should filter sensitive info from repository URL with credentials", () => { + process.env.DD_GIT_REPOSITORY_URL = + "https://user:password@github.com/owner/repo.git"; const result = getGitHubContext(); - expect(result.repositoryUrl).not.toContain('user'); - expect(result.repositoryUrl).not.toContain('password'); - expect(result.repositoryUrl).toBe('https://github.com/owner/repo.git'); + expect(result.repositoryUrl).not.toContain("user"); + expect(result.repositoryUrl).not.toContain("password"); + expect(result.repositoryUrl).toBe("https://github.com/owner/repo.git"); }); - it('should filter sensitive info from malformed URLs', () => { - process.env.DD_GIT_REPOSITORY_URL = 'git://user:pass@example.com/repo.git'; + it("should filter sensitive info from malformed URLs", () => { + process.env.DD_GIT_REPOSITORY_URL = + "git://user:pass@example.com/repo.git"; const result = getGitHubContext(); - expect(result.repositoryUrl).not.toContain('user'); - expect(result.repositoryUrl).not.toContain('pass'); + expect(result.repositoryUrl).not.toContain("user"); + expect(result.repositoryUrl).not.toContain("pass"); }); - it('should handle SSH-style URLs gracefully', () => { - process.env.DD_GIT_REPOSITORY_URL = 'git@github.com:owner/repo.git'; + it("should handle SSH-style URLs gracefully", () => { + process.env.DD_GIT_REPOSITORY_URL = "git@github.com:owner/repo.git"; const result = getGitHubContext(); // SSH URLs don't have credentials to filter - expect(result.repositoryUrl).toBe('git@github.com:owner/repo.git'); + expect(result.repositoryUrl).toBe("git@github.com:owner/repo.git"); }); - it('should use process.cwd() when GITHUB_WORKSPACE is not set', () => { + it("should use process.cwd() when GITHUB_WORKSPACE is not set", () => { const result = getGitHubContext(); expect(result.workspacePath).toBe(process.cwd()); }); - it('should fallback to default serverUrl when context serverUrl is empty', () => { + it("should fallback to default serverUrl when context serverUrl is empty", () => { setContext({ - serverUrl: '', - repo: { owner: 'owner', repo: 'repo' }, + serverUrl: "", + repo: { owner: "owner", repo: "repo" }, runId: 123, }); const result = getGitHubContext(); - expect(result.repositoryUrl).toBe('https://github.com/owner/repo.git'); - expect(result.pipelineUrl).toBe('https://github.com/owner/repo/actions/runs/123'); + expect(result.repositoryUrl).toBe("https://github.com/owner/repo.git"); + expect(result.pipelineUrl).toBe( + "https://github.com/owner/repo/actions/runs/123", + ); }); - it('should handle GitHub Enterprise Server URLs', () => { + it("should handle GitHub Enterprise Server URLs", () => { setContext({ - serverUrl: 'https://github.mycompany.com', - repo: { owner: 'org', repo: 'project' }, + serverUrl: "https://github.mycompany.com", + repo: { owner: "org", repo: "project" }, runId: 999, - job: 'build', + job: "build", }); const result = getGitHubContext(); - expect(result.pipelineUrl).toBe('https://github.mycompany.com/org/project/actions/runs/999'); - expect(result.jobUrl).toBe('https://github.mycompany.com/org/project/actions/runs/999/job/build'); + expect(result.pipelineUrl).toBe( + "https://github.mycompany.com/org/project/actions/runs/999", + ); + expect(result.jobUrl).toBe( + "https://github.mycompany.com/org/project/actions/runs/999/job/build", + ); }); - describe('pull request info', () => { - it('should populate PR info when GITHUB_BASE_REF is set and payload has pull_request', () => { + describe("pull request info", () => { + it("should populate PR info when GITHUB_BASE_REF is set and payload has pull_request", () => { setContext({ payload: { pull_request: { number: 42, - head: { sha: 'df289512a51123083a8e6931dd6f57bb3883d4c4' }, - base: { sha: '52e0974c74d41160a03d59ddc73bb9f5adab054b' }, + head: { sha: "df289512a51123083a8e6931dd6f57bb3883d4c4" }, + base: { sha: "52e0974c74d41160a03d59ddc73bb9f5adab054b" }, }, }, }); - process.env.GITHUB_BASE_REF = 'main'; + process.env.GITHUB_BASE_REF = "main"; const result = getGitHubContext(); - expect(result.pullRequestBaseBranch).toBe('main'); - expect(result.pullRequestHeadSha).toBe('df289512a51123083a8e6931dd6f57bb3883d4c4'); - expect(result.pullRequestBaseBranchHeadSha).toBe('52e0974c74d41160a03d59ddc73bb9f5adab054b'); - expect(result.pullRequestNumber).toBe('42'); + expect(result.pullRequestBaseBranch).toBe("main"); + expect(result.pullRequestHeadSha).toBe( + "df289512a51123083a8e6931dd6f57bb3883d4c4", + ); + expect(result.pullRequestBaseBranchHeadSha).toBe( + "52e0974c74d41160a03d59ddc73bb9f5adab054b", + ); + expect(result.pullRequestNumber).toBe("42"); }); - it('should set only pullRequestBaseBranch when GITHUB_BASE_REF is set but no payload', () => { - process.env.GITHUB_BASE_REF = 'develop'; + it("should set only pullRequestBaseBranch when GITHUB_BASE_REF is set but no payload", () => { + process.env.GITHUB_BASE_REF = "develop"; const result = getGitHubContext(); - expect(result.pullRequestBaseBranch).toBe('develop'); + expect(result.pullRequestBaseBranch).toBe("develop"); expect(result.pullRequestHeadSha).toBeUndefined(); expect(result.pullRequestBaseBranchHeadSha).toBeUndefined(); expect(result.pullRequestNumber).toBeUndefined(); }); - it('should handle partial pull_request payload gracefully', () => { + it("should handle partial pull_request payload gracefully", () => { setContext({ payload: { pull_request: { @@ -200,23 +216,23 @@ describe('github-context', () => { }, }, }); - process.env.GITHUB_BASE_REF = 'main'; + process.env.GITHUB_BASE_REF = "main"; const result = getGitHubContext(); - expect(result.pullRequestBaseBranch).toBe('main'); + expect(result.pullRequestBaseBranch).toBe("main"); expect(result.pullRequestHeadSha).toBeUndefined(); expect(result.pullRequestBaseBranchHeadSha).toBeUndefined(); - expect(result.pullRequestNumber).toBe('42'); + expect(result.pullRequestNumber).toBe("42"); }); - it('should not set PR info when GITHUB_BASE_REF is not set', () => { + it("should not set PR info when GITHUB_BASE_REF is not set", () => { setContext({ payload: { pull_request: { number: 42, - head: { sha: 'head-sha' }, - base: { sha: 'base-sha' }, + head: { sha: "head-sha" }, + base: { sha: "base-sha" }, }, }, }); @@ -230,15 +246,15 @@ describe('github-context', () => { expect(result.pullRequestNumber).toBeUndefined(); }); - it('should handle empty payload gracefully', () => { + it("should handle empty payload gracefully", () => { setContext({ payload: {}, }); - process.env.GITHUB_BASE_REF = 'main'; + process.env.GITHUB_BASE_REF = "main"; const result = getGitHubContext(); - expect(result.pullRequestBaseBranch).toBe('main'); + expect(result.pullRequestBaseBranch).toBe("main"); expect(result.pullRequestHeadSha).toBeUndefined(); expect(result.pullRequestBaseBranchHeadSha).toBeUndefined(); expect(result.pullRequestNumber).toBeUndefined(); diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 9a3e2ee..c1d5d82 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -1,21 +1,27 @@ -import * as core from '@actions/core'; +import * as core from "@actions/core"; // Mock all dependencies before importing the module -jest.mock('@actions/core'); -jest.mock('../uploader'); -jest.mock('../file-finder'); -jest.mock('../github-context'); +jest.mock("@actions/core"); +jest.mock("../uploader"); +jest.mock("../file-finder"); +jest.mock("../github-context"); -import { uploadCoverageFiles } from '../uploader'; -import { findCoverageFiles } from '../file-finder'; -import { getGitHubContext } from '../github-context'; +import { uploadCoverageFiles } from "../uploader"; +import { findCoverageFiles } from "../file-finder"; +import { getGitHubContext } from "../github-context"; const mockCore = core as jest.Mocked; -const mockUploadCoverageFiles = uploadCoverageFiles as jest.MockedFunction; -const mockFindCoverageFiles = findCoverageFiles as jest.MockedFunction; -const mockGetGitHubContext = getGitHubContext as jest.MockedFunction; - -describe('index (run)', () => { +const mockUploadCoverageFiles = uploadCoverageFiles as jest.MockedFunction< + typeof uploadCoverageFiles +>; +const mockFindCoverageFiles = findCoverageFiles as jest.MockedFunction< + typeof findCoverageFiles +>; +const mockGetGitHubContext = getGitHubContext as jest.MockedFunction< + typeof getGitHubContext +>; + +describe("index (run)", () => { const originalEnv = process.env; beforeEach(() => { @@ -25,37 +31,37 @@ describe('index (run)', () => { // Default mock implementations mockCore.getInput.mockImplementation((name: string) => { const inputs: Record = { - 'api-key': 'test-api-key', - site: 'datadoghq.com', - files: '**/coverage.xml', - service: '', - env: '', - flags: '', - 'dry-run': 'false', + "api-key": "test-api-key", + site: "datadoghq.com", + files: "**/coverage.xml", + service: "", + env: "", + flags: "", + "dry-run": "false", }; - return inputs[name] || ''; + return inputs[name] || ""; }); mockGetGitHubContext.mockReturnValue({ - repositoryUrl: 'https://github.com/owner/repo.git', - commitSha: 'abc123', - branch: 'main', + repositoryUrl: "https://github.com/owner/repo.git", + commitSha: "abc123", + branch: "main", tag: undefined, pullRequestBaseBranch: undefined, pullRequestHeadSha: undefined, pullRequestBaseBranchHeadSha: undefined, pullRequestNumber: undefined, - pipelineId: '12345', - pipelineName: 'owner/repo', - pipelineNumber: '1', - pipelineUrl: 'https://github.com/owner/repo/actions/runs/12345', - jobName: 'test', - jobUrl: 'https://github.com/owner/repo/actions/runs/12345/job/test', - workspacePath: '/workspace', + pipelineId: "12345", + pipelineName: "owner/repo", + pipelineNumber: "1", + pipelineUrl: "https://github.com/owner/repo/actions/runs/12345", + jobName: "test", + jobUrl: "https://github.com/owner/repo/actions/runs/12345/job/test", + workspacePath: "/workspace", }); mockFindCoverageFiles.mockResolvedValue([ - { path: '/path/to/coverage.xml', format: 'cobertura' }, + { path: "/path/to/coverage.xml", format: "cobertura" }, ]); mockUploadCoverageFiles.mockResolvedValue(undefined); @@ -71,70 +77,76 @@ describe('index (run)', () => { jest.resetModules(); // Re-mock all modules after reset - jest.doMock('@actions/core', () => mockCore); - jest.doMock('../uploader', () => ({ uploadCoverageFiles: mockUploadCoverageFiles })); - jest.doMock('../file-finder', () => ({ findCoverageFiles: mockFindCoverageFiles })); - jest.doMock('../github-context', () => ({ getGitHubContext: mockGetGitHubContext })); + jest.doMock("@actions/core", () => mockCore); + jest.doMock("../uploader", () => ({ + uploadCoverageFiles: mockUploadCoverageFiles, + })); + jest.doMock("../file-finder", () => ({ + findCoverageFiles: mockFindCoverageFiles, + })); + jest.doMock("../github-context", () => ({ + getGitHubContext: mockGetGitHubContext, + })); // Import and run - await import('../index'); + await import("../index"); // Wait for the async run() to complete - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 10)); } - it('should upload coverage files successfully', async () => { + it("should upload coverage files successfully", async () => { await runAction(); - expect(mockFindCoverageFiles).toHaveBeenCalledWith('**/coverage.xml'); + expect(mockFindCoverageFiles).toHaveBeenCalledWith("**/coverage.xml"); expect(mockUploadCoverageFiles).toHaveBeenCalledWith( expect.objectContaining({ - apiKey: 'test-api-key', - site: 'datadoghq.com', - }) + apiKey: "test-api-key", + site: "datadoghq.com", + }), ); - expect(mockCore.setOutput).toHaveBeenCalledWith('uploaded-files', 1); + expect(mockCore.setOutput).toHaveBeenCalledWith("uploaded-files", 1); }); - it('should use DD_API_KEY environment variable when input not provided', async () => { + it("should use DD_API_KEY environment variable when input not provided", async () => { mockCore.getInput.mockImplementation((name: string) => { - if (name === 'api-key') return ''; - if (name === 'files') return '**/coverage.xml'; - return ''; + if (name === "api-key") return ""; + if (name === "files") return "**/coverage.xml"; + return ""; }); - process.env.DD_API_KEY = 'env-api-key'; + process.env.DD_API_KEY = "env-api-key"; await runAction(); expect(mockUploadCoverageFiles).toHaveBeenCalledWith( expect.objectContaining({ - apiKey: 'env-api-key', - }) + apiKey: "env-api-key", + }), ); }); - it('should use DATADOG_API_KEY environment variable as fallback', async () => { + it("should use DATADOG_API_KEY environment variable as fallback", async () => { mockCore.getInput.mockImplementation((name: string) => { - if (name === 'api-key') return ''; - if (name === 'files') return '**/coverage.xml'; - return ''; + if (name === "api-key") return ""; + if (name === "files") return "**/coverage.xml"; + return ""; }); - process.env.DATADOG_API_KEY = 'datadog-env-api-key'; + process.env.DATADOG_API_KEY = "datadog-env-api-key"; await runAction(); expect(mockUploadCoverageFiles).toHaveBeenCalledWith( expect.objectContaining({ - apiKey: 'datadog-env-api-key', - }) + apiKey: "datadog-env-api-key", + }), ); }); - it('should fail when API key is not provided', async () => { + it("should fail when API key is not provided", async () => { mockCore.getInput.mockImplementation((name: string) => { - if (name === 'api-key') return ''; - if (name === 'files') return '**/coverage.xml'; - return ''; + if (name === "api-key") return ""; + if (name === "files") return "**/coverage.xml"; + return ""; }); delete process.env.DD_API_KEY; delete process.env.DATADOG_API_KEY; @@ -142,191 +154,195 @@ describe('index (run)', () => { await runAction(); expect(mockCore.setFailed).toHaveBeenCalledWith( - expect.stringContaining('API key is required') + expect.stringContaining("API key is required"), ); }); - it('should warn and exit when no coverage files are found', async () => { + it("should warn and exit when no coverage files are found", async () => { mockFindCoverageFiles.mockResolvedValue([]); await runAction(); expect(mockCore.warning).toHaveBeenCalledWith( - 'No coverage files found matching the pattern' + "No coverage files found matching the pattern", ); - expect(mockCore.setOutput).toHaveBeenCalledWith('uploaded-files', 0); + expect(mockCore.setOutput).toHaveBeenCalledWith("uploaded-files", 0); expect(mockUploadCoverageFiles).not.toHaveBeenCalled(); }); - it('should fail when repository URL cannot be determined', async () => { + it("should fail when repository URL cannot be determined", async () => { mockGetGitHubContext.mockReturnValue({ - repositoryUrl: '', - commitSha: 'abc123', - branch: 'main', + repositoryUrl: "", + commitSha: "abc123", + branch: "main", tag: undefined, pullRequestBaseBranch: undefined, pullRequestHeadSha: undefined, pullRequestBaseBranchHeadSha: undefined, pullRequestNumber: undefined, - pipelineId: '12345', - pipelineName: '', - pipelineNumber: '1', - pipelineUrl: '', - jobName: 'test', - jobUrl: '', - workspacePath: '/workspace', + pipelineId: "12345", + pipelineName: "", + pipelineNumber: "1", + pipelineUrl: "", + jobName: "test", + jobUrl: "", + workspacePath: "/workspace", }); await runAction(); expect(mockCore.setFailed).toHaveBeenCalledWith( - 'Could not determine git repository URL' + "Could not determine git repository URL", ); }); - it('should fail when commit SHA cannot be determined', async () => { + it("should fail when commit SHA cannot be determined", async () => { mockGetGitHubContext.mockReturnValue({ - repositoryUrl: 'https://github.com/owner/repo.git', - commitSha: '', - branch: 'main', + repositoryUrl: "https://github.com/owner/repo.git", + commitSha: "", + branch: "main", tag: undefined, pullRequestBaseBranch: undefined, pullRequestHeadSha: undefined, pullRequestBaseBranchHeadSha: undefined, pullRequestNumber: undefined, - pipelineId: '12345', - pipelineName: 'owner/repo', - pipelineNumber: '1', - pipelineUrl: '', - jobName: 'test', - jobUrl: '', - workspacePath: '/workspace', + pipelineId: "12345", + pipelineName: "owner/repo", + pipelineNumber: "1", + pipelineUrl: "", + jobName: "test", + jobUrl: "", + workspacePath: "/workspace", }); await runAction(); expect(mockCore.setFailed).toHaveBeenCalledWith( - 'Could not determine git commit SHA' + "Could not determine git commit SHA", ); }); - it('should run in dry-run mode without uploading', async () => { + it("should run in dry-run mode without uploading", async () => { mockCore.getInput.mockImplementation((name: string) => { const inputs: Record = { - 'api-key': 'test-api-key', - files: '**/coverage.xml', - 'dry-run': 'true', + "api-key": "test-api-key", + files: "**/coverage.xml", + "dry-run": "true", }; - return inputs[name] || ''; + return inputs[name] || ""; }); await runAction(); expect(mockUploadCoverageFiles).not.toHaveBeenCalled(); expect(mockCore.info).toHaveBeenCalledWith( - expect.stringContaining('[DRY-RUN]') + expect.stringContaining("[DRY-RUN]"), ); }); - it('should parse flags correctly', async () => { + it("should parse flags correctly", async () => { mockCore.getInput.mockImplementation((name: string) => { const inputs: Record = { - 'api-key': 'test-api-key', - files: '**/coverage.xml', - flags: 'unit-tests, backend, integration', + "api-key": "test-api-key", + files: "**/coverage.xml", + flags: "unit-tests, backend, integration", }; - return inputs[name] || ''; + return inputs[name] || ""; }); await runAction(); expect(mockUploadCoverageFiles).toHaveBeenCalledWith( expect.objectContaining({ - flags: ['unit-tests', 'backend', 'integration'], - }) + flags: ["unit-tests", "backend", "integration"], + }), ); }); - it('should fail when more than 32 flags are provided', async () => { - const manyFlags = Array.from({ length: 33 }, (_, i) => `flag${i}`).join(','); + it("should fail when more than 32 flags are provided", async () => { + const manyFlags = Array.from({ length: 33 }, (_, i) => `flag${i}`).join( + ",", + ); mockCore.getInput.mockImplementation((name: string) => { const inputs: Record = { - 'api-key': 'test-api-key', - files: '**/coverage.xml', + "api-key": "test-api-key", + files: "**/coverage.xml", flags: manyFlags, }; - return inputs[name] || ''; + return inputs[name] || ""; }); await runAction(); expect(mockCore.setFailed).toHaveBeenCalledWith( - expect.stringContaining('Maximum of 32 flags') + expect.stringContaining("Maximum of 32 flags"), ); }); - it('should use default site when not specified', async () => { + it("should use default site when not specified", async () => { mockCore.getInput.mockImplementation((name: string) => { const inputs: Record = { - 'api-key': 'test-api-key', - files: '**/coverage.xml', - site: '', + "api-key": "test-api-key", + files: "**/coverage.xml", + site: "", }; - return inputs[name] || ''; + return inputs[name] || ""; }); await runAction(); expect(mockUploadCoverageFiles).toHaveBeenCalledWith( expect.objectContaining({ - site: 'datadoghq.com', - }) + site: "datadoghq.com", + }), ); }); - it('should pass service and env to uploader', async () => { + it("should pass service and env to uploader", async () => { mockCore.getInput.mockImplementation((name: string) => { const inputs: Record = { - 'api-key': 'test-api-key', - files: '**/coverage.xml', - service: 'my-service', - env: 'production', + "api-key": "test-api-key", + files: "**/coverage.xml", + service: "my-service", + env: "production", }; - return inputs[name] || ''; + return inputs[name] || ""; }); await runAction(); expect(mockUploadCoverageFiles).toHaveBeenCalledWith( expect.objectContaining({ - service: 'my-service', - env: 'production', - }) + service: "my-service", + env: "production", + }), ); }); - it('should handle upload errors', async () => { - mockUploadCoverageFiles.mockRejectedValue(new Error('Upload failed')); + it("should handle upload errors", async () => { + mockUploadCoverageFiles.mockRejectedValue(new Error("Upload failed")); await runAction(); - expect(mockCore.setFailed).toHaveBeenCalledWith('Upload failed'); + expect(mockCore.setFailed).toHaveBeenCalledWith("Upload failed"); }); - it('should handle non-Error exceptions', async () => { - mockUploadCoverageFiles.mockRejectedValue('string error'); + it("should handle non-Error exceptions", async () => { + mockUploadCoverageFiles.mockRejectedValue("string error"); await runAction(); - expect(mockCore.setFailed).toHaveBeenCalledWith('An unexpected error occurred'); + expect(mockCore.setFailed).toHaveBeenCalledWith( + "An unexpected error occurred", + ); }); - it('should set upload-time output', async () => { + it("should set upload-time output", async () => { await runAction(); expect(mockCore.setOutput).toHaveBeenCalledWith( - 'upload-time', - expect.any(String) + "upload-time", + expect.any(String), ); }); }); diff --git a/src/__tests__/uploader.test.ts b/src/__tests__/uploader.test.ts index 8dbfa06..898c236 100644 --- a/src/__tests__/uploader.test.ts +++ b/src/__tests__/uploader.test.ts @@ -1,43 +1,45 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { uploadCoverageFiles, UploadOptions } from '../uploader'; -import { GitHubContext } from '../github-context'; -import { CoverageFile } from '../file-finder'; +import * as fs from "fs"; +import * as path from "path"; +import { uploadCoverageFiles, UploadOptions } from "../uploader"; +import { GitHubContext } from "../github-context"; +import { CoverageFile } from "../file-finder"; // Mock dependencies -jest.mock('@actions/core'); -jest.mock('axios', () => ({ +jest.mock("@actions/core"); +jest.mock("axios", () => ({ post: jest.fn(), isAxiosError: jest.fn(), })); -import axios, { AxiosResponse } from 'axios'; -import * as core from '@actions/core'; +import axios, { AxiosResponse } from "axios"; +import * as core from "@actions/core"; const mockAxiosPost = axios.post as jest.MockedFunction; -const mockIsAxiosError = axios.isAxiosError as jest.MockedFunction; +const mockIsAxiosError = axios.isAxiosError as jest.MockedFunction< + typeof axios.isAxiosError +>; const mockCore = core as jest.Mocked; -describe('uploader', () => { - const testDir = path.join(__dirname, 'uploader-fixtures'); +describe("uploader", () => { + const testDir = path.join(__dirname, "uploader-fixtures"); let testFile: string; const mockContext: GitHubContext = { - repositoryUrl: 'https://github.com/owner/repo.git', - commitSha: 'abc123def456', - branch: 'main', + repositoryUrl: "https://github.com/owner/repo.git", + commitSha: "abc123def456", + branch: "main", tag: undefined, pullRequestBaseBranch: undefined, pullRequestHeadSha: undefined, pullRequestBaseBranchHeadSha: undefined, pullRequestNumber: undefined, - pipelineId: '12345', - pipelineName: 'owner/repo', - pipelineNumber: '42', - pipelineUrl: 'https://github.com/owner/repo/actions/runs/12345', - jobName: 'test', - jobUrl: 'https://github.com/owner/repo/actions/runs/12345/job/test', - workspacePath: '/home/runner/work', + pipelineId: "12345", + pipelineName: "owner/repo", + pipelineNumber: "42", + pipelineUrl: "https://github.com/owner/repo/actions/runs/12345", + jobName: "test", + jobUrl: "https://github.com/owner/repo/actions/runs/12345/job/test", + workspacePath: "/home/runner/work", }; beforeAll(() => { @@ -46,10 +48,10 @@ describe('uploader', () => { fs.mkdirSync(testDir, { recursive: true }); } - testFile = path.join(testDir, 'coverage.xml'); + testFile = path.join(testDir, "coverage.xml"); fs.writeFileSync( testFile, - 'test' + 'test', ); }); @@ -65,90 +67,104 @@ describe('uploader', () => { mockIsAxiosError.mockReturnValue(false); }); - describe('uploadCoverageFiles', () => { + describe("uploadCoverageFiles", () => { const baseOptions: UploadOptions = { - apiKey: 'test-api-key', - site: 'datadoghq.com', + apiKey: "test-api-key", + site: "datadoghq.com", files: [], context: mockContext, }; - it('should upload a single file successfully', async () => { + it("should upload a single file successfully", async () => { const options: UploadOptions = { ...baseOptions, - files: [{ path: testFile, format: 'cobertura' }], + files: [{ path: testFile, format: "cobertura" }], }; - mockAxiosPost.mockResolvedValueOnce({ status: 200, data: {} } as AxiosResponse); + mockAxiosPost.mockResolvedValueOnce({ + status: 200, + data: {}, + } as AxiosResponse); await uploadCoverageFiles(options); expect(mockAxiosPost).toHaveBeenCalledTimes(1); expect(mockAxiosPost).toHaveBeenCalledWith( - 'https://ci-intake.datadoghq.com/api/v2/cicovreprt', + "https://ci-intake.datadoghq.com/api/v2/cicovreprt", expect.any(Object), expect.objectContaining({ headers: expect.objectContaining({ - 'DD-API-KEY': 'test-api-key', + "DD-API-KEY": "test-api-key", }), - }) + }), ); }); - it('should include service and env in the event payload', async () => { + it("should include service and env in the event payload", async () => { const options: UploadOptions = { ...baseOptions, - files: [{ path: testFile, format: 'cobertura' }], - service: 'my-service', - env: 'production', + files: [{ path: testFile, format: "cobertura" }], + service: "my-service", + env: "production", }; - mockAxiosPost.mockResolvedValueOnce({ status: 200, data: {} } as AxiosResponse); + mockAxiosPost.mockResolvedValueOnce({ + status: 200, + data: {}, + } as AxiosResponse); await uploadCoverageFiles(options); expect(mockAxiosPost).toHaveBeenCalledTimes(1); // The form data includes the event JSON with service and env const callArgs = mockAxiosPost.mock.calls[0]; - expect(callArgs[0]).toBe('https://ci-intake.datadoghq.com/api/v2/cicovreprt'); + expect(callArgs[0]).toBe( + "https://ci-intake.datadoghq.com/api/v2/cicovreprt", + ); }); - it('should include flags in the event payload', async () => { + it("should include flags in the event payload", async () => { const options: UploadOptions = { ...baseOptions, - files: [{ path: testFile, format: 'cobertura' }], - flags: ['unit-tests', 'backend'], + files: [{ path: testFile, format: "cobertura" }], + flags: ["unit-tests", "backend"], }; - mockAxiosPost.mockResolvedValueOnce({ status: 200, data: {} } as AxiosResponse); + mockAxiosPost.mockResolvedValueOnce({ + status: 200, + data: {}, + } as AxiosResponse); await uploadCoverageFiles(options); expect(mockAxiosPost).toHaveBeenCalledTimes(1); }); - it('should use custom site in URL', async () => { + it("should use custom site in URL", async () => { const options: UploadOptions = { ...baseOptions, - site: 'datadoghq.eu', - files: [{ path: testFile, format: 'cobertura' }], + site: "datadoghq.eu", + files: [{ path: testFile, format: "cobertura" }], }; - mockAxiosPost.mockResolvedValueOnce({ status: 200, data: {} } as AxiosResponse); + mockAxiosPost.mockResolvedValueOnce({ + status: 200, + data: {}, + } as AxiosResponse); await uploadCoverageFiles(options); expect(mockAxiosPost).toHaveBeenCalledWith( - 'https://ci-intake.datadoghq.eu/api/v2/cicovreprt', + "https://ci-intake.datadoghq.eu/api/v2/cicovreprt", + expect.any(Object), expect.any(Object), - expect.any(Object) ); }); - it('should batch files in groups of 8', async () => { + it("should batch files in groups of 8", async () => { const files: CoverageFile[] = Array.from({ length: 10 }, (_) => ({ path: testFile, - format: 'cobertura', + format: "cobertura", })); const options: UploadOptions = { @@ -156,7 +172,10 @@ describe('uploader', () => { files, }; - mockAxiosPost.mockResolvedValue({ status: 200, data: {} } as AxiosResponse); + mockAxiosPost.mockResolvedValue({ + status: 200, + data: {}, + } as AxiosResponse); await uploadCoverageFiles(options); @@ -164,13 +183,13 @@ describe('uploader', () => { expect(mockAxiosPost).toHaveBeenCalledTimes(2); }); - it('should retry on transient failures', async () => { + it("should retry on transient failures", async () => { const options: UploadOptions = { ...baseOptions, - files: [{ path: testFile, format: 'cobertura' }], + files: [{ path: testFile, format: "cobertura" }], }; - const error = new Error('Network error'); + const error = new Error("Network error"); mockAxiosPost .mockRejectedValueOnce(error) .mockRejectedValueOnce(error) @@ -183,16 +202,16 @@ describe('uploader', () => { expect(mockCore.warning).toHaveBeenCalledTimes(2); }); - it('should retry on transient axios errors and log warning with axios message', async () => { + it("should retry on transient axios errors and log warning with axios message", async () => { const options: UploadOptions = { ...baseOptions, - files: [{ path: testFile, format: 'cobertura' }], + files: [{ path: testFile, format: "cobertura" }], }; // Create an axios error with a 500 status (retryable) const axiosError = { - response: { status: 500, data: 'Internal Server Error' }, - message: 'Request failed with status code 500', + response: { status: 500, data: "Internal Server Error" }, + message: "Request failed with status code 500", isAxiosError: true, }; mockAxiosPost @@ -204,60 +223,60 @@ describe('uploader', () => { expect(mockAxiosPost).toHaveBeenCalledTimes(2); expect(mockCore.warning).toHaveBeenCalledWith( - 'Upload attempt 1/3 failed: Request failed with status code 500' + "Upload attempt 1/3 failed: Request failed with status code 500", ); }); - it('should fail immediately on 400 error', async () => { + it("should fail immediately on 400 error", async () => { const options: UploadOptions = { ...baseOptions, - files: [{ path: testFile, format: 'cobertura' }], + files: [{ path: testFile, format: "cobertura" }], }; const axiosError = { - response: { status: 400, data: 'Bad request' }, - message: 'Request failed', + response: { status: 400, data: "Bad request" }, + message: "Request failed", isAxiosError: true, }; mockAxiosPost.mockRejectedValueOnce(axiosError); mockIsAxiosError.mockReturnValue(true); await expect(uploadCoverageFiles(options)).rejects.toThrow( - 'Upload failed with status 400' + "Upload failed with status 400", ); expect(mockAxiosPost).toHaveBeenCalledTimes(1); }); - it('should fail immediately on 403 error', async () => { + it("should fail immediately on 403 error", async () => { const options: UploadOptions = { ...baseOptions, - files: [{ path: testFile, format: 'cobertura' }], + files: [{ path: testFile, format: "cobertura" }], }; const axiosError = { - response: { status: 403, data: 'Forbidden' }, - message: 'Request failed', + response: { status: 403, data: "Forbidden" }, + message: "Request failed", isAxiosError: true, }; mockAxiosPost.mockRejectedValueOnce(axiosError); mockIsAxiosError.mockReturnValue(true); await expect(uploadCoverageFiles(options)).rejects.toThrow( - 'Upload failed with status 403' + "Upload failed with status 403", ); expect(mockAxiosPost).toHaveBeenCalledTimes(1); }); - it('should fail after all retries are exhausted', async () => { + it("should fail after all retries are exhausted", async () => { const options: UploadOptions = { ...baseOptions, - files: [{ path: testFile, format: 'cobertura' }], + files: [{ path: testFile, format: "cobertura" }], }; // Use an actual Error instance to ensure line 115's true branch is covered - const error = new Error('Persistent network error'); + const error = new Error("Persistent network error"); mockAxiosPost .mockRejectedValueOnce(error) .mockRejectedValueOnce(error) @@ -265,51 +284,57 @@ describe('uploader', () => { mockIsAxiosError.mockReturnValue(false); await expect(uploadCoverageFiles(options)).rejects.toThrow( - 'Persistent network error' + "Persistent network error", ); expect(mockAxiosPost).toHaveBeenCalledTimes(3); // Default 3 retries }); - it('should handle files with leading dots in names', async () => { - const dotFile = path.join(testDir, '.coverage'); - fs.writeFileSync(dotFile, 'coverage data'); + it("should handle files with leading dots in names", async () => { + const dotFile = path.join(testDir, ".coverage"); + fs.writeFileSync(dotFile, "coverage data"); const options: UploadOptions = { ...baseOptions, - files: [{ path: dotFile, format: 'lcov' }], + files: [{ path: dotFile, format: "lcov" }], }; - mockAxiosPost.mockResolvedValueOnce({ status: 200, data: {} } as AxiosResponse); + mockAxiosPost.mockResolvedValueOnce({ + status: 200, + data: {}, + } as AxiosResponse); await uploadCoverageFiles(options); expect(mockAxiosPost).toHaveBeenCalledTimes(1); expect(mockCore.info).toHaveBeenCalledWith( - expect.stringContaining('Uploading:') + expect.stringContaining("Uploading:"), ); }); - it('should include all span tags from context', async () => { + it("should include all span tags from context", async () => { const contextWithTag: GitHubContext = { ...mockContext, - tag: 'v1.0.0', + tag: "v1.0.0", }; const options: UploadOptions = { ...baseOptions, context: contextWithTag, - files: [{ path: testFile, format: 'cobertura' }], + files: [{ path: testFile, format: "cobertura" }], }; - mockAxiosPost.mockResolvedValueOnce({ status: 200, data: {} } as AxiosResponse); + mockAxiosPost.mockResolvedValueOnce({ + status: 200, + data: {}, + } as AxiosResponse); await uploadCoverageFiles(options); expect(mockAxiosPost).toHaveBeenCalledTimes(1); }); - it('should handle empty files array', async () => { + it("should handle empty files array", async () => { const options: UploadOptions = { ...baseOptions, files: [], @@ -320,43 +345,49 @@ describe('uploader', () => { expect(mockAxiosPost).not.toHaveBeenCalled(); }); - it('should log info for each file being uploaded', async () => { + it("should log info for each file being uploaded", async () => { const options: UploadOptions = { ...baseOptions, files: [ - { path: testFile, format: 'cobertura' }, - { path: testFile, format: 'jacoco' }, + { path: testFile, format: "cobertura" }, + { path: testFile, format: "jacoco" }, ], }; - mockAxiosPost.mockResolvedValueOnce({ status: 200, data: {} } as AxiosResponse); + mockAxiosPost.mockResolvedValueOnce({ + status: 200, + data: {}, + } as AxiosResponse); await uploadCoverageFiles(options); expect(mockCore.info).toHaveBeenCalledWith( - expect.stringContaining('cobertura') + expect.stringContaining("cobertura"), ); expect(mockCore.info).toHaveBeenCalledWith( - expect.stringContaining('jacoco') + expect.stringContaining("jacoco"), ); }); - it('should include pull request span tags when PR info is present', async () => { + it("should include pull request span tags when PR info is present", async () => { const prContext: GitHubContext = { ...mockContext, - pullRequestBaseBranch: 'main', - pullRequestHeadSha: 'abc123head', - pullRequestBaseBranchHeadSha: 'def456base', - pullRequestNumber: '42', + pullRequestBaseBranch: "main", + pullRequestHeadSha: "abc123head", + pullRequestBaseBranchHeadSha: "def456base", + pullRequestNumber: "42", }; const options: UploadOptions = { ...baseOptions, context: prContext, - files: [{ path: testFile, format: 'cobertura' }], + files: [{ path: testFile, format: "cobertura" }], }; - mockAxiosPost.mockResolvedValueOnce({ status: 200, data: {} } as AxiosResponse); + mockAxiosPost.mockResolvedValueOnce({ + status: 200, + data: {}, + } as AxiosResponse); await uploadCoverageFiles(options); @@ -365,7 +396,7 @@ describe('uploader', () => { // The actual tag values are included in the FormData }); - it('should handle context without optional branch field', async () => { + it("should handle context without optional branch field", async () => { const noBranchContext: GitHubContext = { ...mockContext, branch: undefined, @@ -374,41 +405,46 @@ describe('uploader', () => { const options: UploadOptions = { ...baseOptions, context: noBranchContext, - files: [{ path: testFile, format: 'cobertura' }], + files: [{ path: testFile, format: "cobertura" }], }; - mockAxiosPost.mockResolvedValueOnce({ status: 200, data: {} } as AxiosResponse); + mockAxiosPost.mockResolvedValueOnce({ + status: 200, + data: {}, + } as AxiosResponse); await uploadCoverageFiles(options); expect(mockAxiosPost).toHaveBeenCalledTimes(1); }); - it('should handle non-Error objects thrown during upload', async () => { + it("should handle non-Error objects thrown during upload", async () => { const options: UploadOptions = { ...baseOptions, - files: [{ path: testFile, format: 'cobertura' }], + files: [{ path: testFile, format: "cobertura" }], }; // Throw a string instead of an Error object to hit the non-Error branch mockAxiosPost - .mockRejectedValueOnce('string error') - .mockRejectedValueOnce('string error') - .mockRejectedValueOnce('string error'); + .mockRejectedValueOnce("string error") + .mockRejectedValueOnce("string error") + .mockRejectedValueOnce("string error"); mockIsAxiosError.mockReturnValue(false); - await expect(uploadCoverageFiles(options)).rejects.toThrow('string error'); + await expect(uploadCoverageFiles(options)).rejects.toThrow( + "string error", + ); expect(mockAxiosPost).toHaveBeenCalledTimes(3); expect(mockCore.warning).toHaveBeenCalledWith( - 'Upload attempt 1/3 failed: string error' + "Upload attempt 1/3 failed: string error", ); }); - it('should handle null thrown during upload', async () => { + it("should handle null thrown during upload", async () => { const options: UploadOptions = { ...baseOptions, - files: [{ path: testFile, format: 'cobertura' }], + files: [{ path: testFile, format: "cobertura" }], }; // Throw null to fully test the non-Error branch @@ -418,85 +454,91 @@ describe('uploader', () => { .mockRejectedValueOnce(null); mockIsAxiosError.mockReturnValue(false); - await expect(uploadCoverageFiles(options)).rejects.toThrow('null'); + await expect(uploadCoverageFiles(options)).rejects.toThrow("null"); expect(mockAxiosPost).toHaveBeenCalledTimes(3); }); - it('should handle axios error without response data', async () => { + it("should handle axios error without response data", async () => { const options: UploadOptions = { ...baseOptions, - files: [{ path: testFile, format: 'cobertura' }], + files: [{ path: testFile, format: "cobertura" }], }; // Create an axios error with 400 status but no response data const axiosError = { response: { status: 400, data: undefined }, - message: 'Bad Request', + message: "Bad Request", isAxiosError: true, }; mockAxiosPost.mockRejectedValueOnce(axiosError); mockIsAxiosError.mockReturnValue(true); await expect(uploadCoverageFiles(options)).rejects.toThrow( - 'Upload failed with status 400: Bad Request' + "Upload failed with status 400: Bad Request", ); expect(mockAxiosPost).toHaveBeenCalledTimes(1); }); - it('should handle axios error with 403 status and response data', async () => { + it("should handle axios error with 403 status and response data", async () => { const options: UploadOptions = { ...baseOptions, - files: [{ path: testFile, format: 'cobertura' }], + files: [{ path: testFile, format: "cobertura" }], }; const axiosError = { - response: { status: 403, data: 'Invalid API key' }, - message: 'Forbidden', + response: { status: 403, data: "Invalid API key" }, + message: "Forbidden", isAxiosError: true, }; mockAxiosPost.mockRejectedValueOnce(axiosError); mockIsAxiosError.mockReturnValue(true); await expect(uploadCoverageFiles(options)).rejects.toThrow( - 'Upload failed with status 403: Invalid API key' + "Upload failed with status 403: Invalid API key", ); expect(mockAxiosPost).toHaveBeenCalledTimes(1); }); - it('should use empty flags array without adding to event', async () => { + it("should use empty flags array without adding to event", async () => { const options: UploadOptions = { ...baseOptions, - files: [{ path: testFile, format: 'cobertura' }], + files: [{ path: testFile, format: "cobertura" }], flags: [], }; - mockAxiosPost.mockResolvedValueOnce({ status: 200, data: {} } as AxiosResponse); + mockAxiosPost.mockResolvedValueOnce({ + status: 200, + data: {}, + } as AxiosResponse); await uploadCoverageFiles(options); expect(mockAxiosPost).toHaveBeenCalledTimes(1); }); - it('should use unknown format when file has empty format string', async () => { + it("should use unknown format when file has empty format string", async () => { const options: UploadOptions = { ...baseOptions, - files: [{ path: testFile, format: '' }], + files: [{ path: testFile, format: "" }], }; - mockAxiosPost.mockResolvedValueOnce({ status: 200, data: {} } as AxiosResponse); + mockAxiosPost.mockResolvedValueOnce({ + status: 200, + data: {}, + } as AxiosResponse); await uploadCoverageFiles(options); expect(mockAxiosPost).toHaveBeenCalledTimes(1); }); - it('should throw default error when response has non-2xx status without exception', async () => { + it("should throw default error when response has non-2xx status without exception", async () => { const options: UploadOptions = { ...baseOptions, - files: [{ path: testFile, format: 'cobertura' }], + files: [{ path: testFile, format: "cobertura" }], }; // Return non-2xx status without throwing - this makes lastError undefined @@ -506,20 +548,20 @@ describe('uploader', () => { .mockResolvedValueOnce({ status: 503, data: {} } as AxiosResponse); await expect(uploadCoverageFiles(options)).rejects.toThrow( - 'Upload failed after all retries' + "Upload failed after all retries", ); expect(mockAxiosPost).toHaveBeenCalledTimes(3); }); - it('should handle Error instance thrown during upload and preserve it', async () => { + it("should handle Error instance thrown during upload and preserve it", async () => { const options: UploadOptions = { ...baseOptions, - files: [{ path: testFile, format: 'cobertura' }], + files: [{ path: testFile, format: "cobertura" }], }; // Create an actual Error instance to test the true branch of instanceof Error - const error = new Error('Actual Error instance'); + const error = new Error("Actual Error instance"); mockAxiosPost .mockRejectedValueOnce(error) .mockRejectedValueOnce(error) @@ -529,7 +571,7 @@ describe('uploader', () => { // The thrown error should be the exact same Error instance try { await uploadCoverageFiles(options); - fail('Expected an error to be thrown'); + fail("Expected an error to be thrown"); } catch (e) { expect(e).toBe(error); expect(e).toBeInstanceOf(Error);