diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 6d5f848..d024a81 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -1,40 +1,64 @@ -# This workflow will run tests using node and then publish a package to NPM when a release is created -# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages - -name: Node.js Package +name: Publish Package on: release: types: [created] jobs: - - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'yarn' - - run: yarn install --frozen-lockfile - - run: yarn test - publish-npm: - needs: build runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 with: - node-version: 20 + node-version: '20' + registry-url: 'https://registry.npmjs.org' cache: 'yarn' - registry-url: https://registry.npmjs.org/ - - run: yarn install --frozen-lockfile - - run: yarn build - - run: npm publish --provenance --access public + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Extract version from tag + id: get_version + run: | + # Use the tag name directly and strip an optional leading 'v' + VERSION="${{ github.ref_name }}" + VERSION="${VERSION#v}" + + # Basic validation: ensure VERSION looks like a SemVer (e.g. 1.2.3, 1.2.3-beta.1) + if ! echo "$VERSION" | grep -Eq '^[0-9]+(\.[0-9]+){2}(-[0-9A-Za-z.-]+)?$'; then + echo "Error: Tag '${{ github.ref_name }}' does not contain a valid semver version (got '$VERSION')." >&2 + exit 1 + fi + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Publishing version: $VERSION" + + - name: Update package.json version + run: | + VERSION="${{ steps.get_version.outputs.version }}" + + # Guard against empty or invalid version + if [ -z "$VERSION" ]; then + echo "Error: Version output is empty" >&2 + exit 1 + fi + + # Use yarn version to keep yarn.lock in sync + yarn version --new-version "$VERSION" --no-git-tag-version + + - name: Run tests + run: yarn test + + - name: Build + run: yarn build + + - name: Publish to npm + run: npm publish --provenance --access public env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/jest.config.js b/jest.config.js index 88415b8..98ee216 100644 --- a/jest.config.js +++ b/jest.config.js @@ -21,11 +21,141 @@ module.exports = { '!src/**/*.spec.ts', ], coverageThreshold: { + // Note: Global thresholds are intentionally lower because they include + // modules without tests. Specific per-file thresholds ensure 90%+ coverage + // for all modules that have test coverage. global: { - branches: 15, - functions: 15, - lines: 25, - statements: 25, + branches: 13, + functions: 23, + lines: 27, + statements: 27, + }, + // Specific thresholds for modules with tests + 'src/apis/ArtifactApi.ts': { + branches: 85, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/apis/DashboardApi.ts': { + branches: 80, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/apis/GroupApi.ts': { + branches: 80, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/apis/HealthApi.ts': { + branches: 80, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/apis/LoginApi.ts': { + branches: 60, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/apis/ProjectApi.ts': { + branches: 80, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/apis/ResultApi.ts': { + branches: 80, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/apis/RunApi.ts': { + branches: 75, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/models/Artifact.ts': { + branches: 80, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/models/ArtifactList.ts': { + branches: 90, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/models/Dashboard.ts': { + branches: 80, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/models/DashboardList.ts': { + branches: 90, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/models/Group.ts': { + branches: 75, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/models/GroupList.ts': { + branches: 90, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/models/Pagination.ts': { + branches: 90, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/models/Project.ts': { + branches: 85, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/models/ProjectList.ts': { + branches: 90, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/models/Result.ts': { + branches: 95, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/models/ResultList.ts': { + branches: 90, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/models/Run.ts': { + branches: 90, + functions: 100, + lines: 90, + statements: 90, + }, + 'src/models/RunList.ts': { + branches: 90, + functions: 100, + lines: 90, + statements: 90, }, }, diff --git a/package.json b/package.json index 34417cb..a69e9bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ibutsu-client-ts", - "version": "2.0.1", + "version": "0.0.0-development", "description": "A TypeScript client for the Ibutsu API", "license": "MIT", "main": "dist/cjs/index.js", diff --git a/src/apis/__tests__/ArtifactApi.test.ts b/src/apis/__tests__/ArtifactApi.test.ts index e58a6a3..317815d 100644 --- a/src/apis/__tests__/ArtifactApi.test.ts +++ b/src/apis/__tests__/ArtifactApi.test.ts @@ -45,6 +45,26 @@ describe('ArtifactApi', () => { expect(result.filename).toBe(filename); }); + it('should require filename parameter', async () => { + const fileBlob = new Blob(['data'], { type: 'text/plain' }); + + await expect( + api.uploadArtifact({ + filename: null as unknown as string, + file: fileBlob, + }) + ).rejects.toThrow(); + }); + + it('should require file parameter', async () => { + await expect( + api.uploadArtifact({ + filename: 'test.txt', + file: null as unknown as Blob, + }) + ).rejects.toThrow(); + }); + it('should attach artifact to a result', async () => { const fileBlob = new Blob(['data'], { type: 'text/plain' }); const resultId = '123e4567-e89b-12d3-a456-426614174001'; @@ -405,7 +425,7 @@ describe('ArtifactApi', () => { }); describe('authentication', () => { - it('should include Bearer token when configured', async () => { + it('should include Bearer token when configured for getArtifact', async () => { const config = new Configuration({ basePath: 'http://localhost/api', accessToken: async () => 'test-token-456', @@ -437,6 +457,168 @@ describe('ArtifactApi', () => { ); }); + it('should include Bearer token when configured for uploadArtifact', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-upload', + }); + api = new ArtifactApi(config); + + const fileBlob = new Blob(['data'], { type: 'text/plain' }); + const responseArtifact: Artifact = { + id: '123e4567-e89b-12d3-a456-426614174000', + filename: 'test.txt', + }; + + mockFetch = createMockFetch(responseArtifact, 201); + global.fetch = mockFetch; + + await api.uploadArtifact({ + filename: 'test.txt', + file: fileBlob, + }); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-upload', + }), + }) + ); + }); + + it('should include Bearer token when configured for getArtifactList', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-list', + }); + api = new ArtifactApi(config); + + const mockArtifactList: ArtifactList = { + artifacts: [], + pagination: { + page: 1, + pageSize: 25, + totalItems: 0, + totalPages: 0, + }, + }; + + mockFetch = createMockFetch(mockArtifactList); + global.fetch = mockFetch; + + await api.getArtifactList({}); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-list', + }), + }) + ); + }); + + it('should include Bearer token when configured for deleteArtifact', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-delete', + }); + api = new ArtifactApi(config); + + mockFetch = jest.fn().mockResolvedValue({ + ok: true, + status: 204, + }); + global.fetch = mockFetch; + + await api.deleteArtifact({ id: '123e4567-e89b-12d3-a456-426614174000' }); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-delete', + }), + }) + ); + }); + + it('should include Bearer token when configured for downloadArtifact', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-download', + }); + api = new ArtifactApi(config); + + const mockBlob = new Blob(['file content'], { type: 'text/plain' }); + mockFetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + blob: async () => mockBlob, + }); + global.fetch = mockFetch; + + await api.downloadArtifact({ id: '123e4567-e89b-12d3-a456-426614174000' }); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-download', + }), + }) + ); + }); + + it('should include Bearer token when configured for viewArtifact', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-view', + }); + api = new ArtifactApi(config); + + const mockBlob = new Blob(['content'], { type: 'text/html' }); + mockFetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + blob: async () => mockBlob, + }); + global.fetch = mockFetch; + + await api.viewArtifact({ id: '123e4567-e89b-12d3-a456-426614174000' }); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-view', + }), + }) + ); + }); + it('should work without authentication when not configured', async () => { const artifactId = '123e4567-e89b-12d3-a456-426614174000'; const expectedArtifact: Artifact = { diff --git a/src/apis/__tests__/DashboardApi.test.ts b/src/apis/__tests__/DashboardApi.test.ts index 02d5b0a..aa6dd7e 100644 --- a/src/apis/__tests__/DashboardApi.test.ts +++ b/src/apis/__tests__/DashboardApi.test.ts @@ -328,7 +328,7 @@ describe('DashboardApi', () => { }); describe('authentication', () => { - it('should include Bearer token when configured', async () => { + it('should include Bearer token when configured for getDashboard', async () => { const config = new Configuration({ basePath: 'http://localhost/api', accessToken: async () => 'test-token-dashboard', @@ -360,6 +360,92 @@ describe('DashboardApi', () => { ); }); + it('should include Bearer token when configured for addDashboard', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-add', + }); + api = new DashboardApi(config); + + const newDashboard: Dashboard = { + title: 'New Dashboard', + }; + + mockFetch = createMockFetch({ id: 'dashboard-123', ...newDashboard }, 201); + global.fetch = mockFetch; + + await api.addDashboard({ dashboard: newDashboard }); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-add', + }), + }) + ); + }); + + it('should include Bearer token when configured for getDashboardList', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-list', + }); + api = new DashboardApi(config); + + mockFetch = createMockFetch({ dashboards: [] }); + global.fetch = mockFetch; + + await api.getDashboardList({}); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-list', + }), + }) + ); + }); + + it('should include Bearer token when configured for updateDashboard', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-update', + }); + api = new DashboardApi(config); + + const updatedDashboard: Dashboard = { + title: 'Updated Dashboard', + }; + + mockFetch = createMockFetch({ id: 'dashboard-123', ...updatedDashboard }); + global.fetch = mockFetch; + + await api.updateDashboard({ id: 'dashboard-123', dashboard: updatedDashboard }); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-update', + }), + }) + ); + }); + it('should work without authentication when not configured', async () => { const dashboardId = 'dashboard-no-auth'; const expectedDashboard: Dashboard = { diff --git a/src/apis/__tests__/GroupApi.test.ts b/src/apis/__tests__/GroupApi.test.ts index 5647f67..0b220cb 100644 --- a/src/apis/__tests__/GroupApi.test.ts +++ b/src/apis/__tests__/GroupApi.test.ts @@ -283,7 +283,7 @@ describe('GroupApi', () => { }); describe('authentication', () => { - it('should include Bearer token when configured', async () => { + it('should include Bearer token when configured for getGroup', async () => { const config = new Configuration({ basePath: 'http://localhost/api', accessToken: async () => 'test-token-abc', @@ -315,6 +315,92 @@ describe('GroupApi', () => { ); }); + it('should include Bearer token when configured for addGroup', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-add', + }); + api = new GroupApi(config); + + const newGroup: Group = { + name: 'new-group', + }; + + mockFetch = createMockFetch({ id: 'group-123', ...newGroup }, 201); + global.fetch = mockFetch; + + await api.addGroup({ group: newGroup }); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-add', + }), + }) + ); + }); + + it('should include Bearer token when configured for getGroupList', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-list', + }); + api = new GroupApi(config); + + mockFetch = createMockFetch({ groups: [] }); + global.fetch = mockFetch; + + await api.getGroupList({}); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-list', + }), + }) + ); + }); + + it('should include Bearer token when configured for updateGroup', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-update', + }); + api = new GroupApi(config); + + const updatedGroup: Group = { + name: 'updated-group', + }; + + mockFetch = createMockFetch({ id: 'group-123', ...updatedGroup }); + global.fetch = mockFetch; + + await api.updateGroup({ id: 'group-123', group: updatedGroup }); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-update', + }), + }) + ); + }); + it('should work without authentication when not configured', async () => { const groupId = 'group-no-auth'; const expectedGroup: Group = { diff --git a/src/apis/__tests__/ProjectApi.test.ts b/src/apis/__tests__/ProjectApi.test.ts index 955cba4..ff14733 100644 --- a/src/apis/__tests__/ProjectApi.test.ts +++ b/src/apis/__tests__/ProjectApi.test.ts @@ -289,7 +289,7 @@ describe('ProjectApi', () => { }); describe('authentication', () => { - it('should include Bearer token when configured', async () => { + it('should include Bearer token when configured for getProjectList', async () => { const configWithAuth = new Configuration({ basePath: 'http://localhost/api', accessToken: 'test-token-123', @@ -306,6 +306,82 @@ describe('ProjectApi', () => { expect((headers as Record).Authorization).toBe('Bearer test-token-123'); }); + it('should include Bearer token when configured for addProject', async () => { + const configWithAuth = new Configuration({ + basePath: 'http://localhost/api', + accessToken: 'test-token-add', + }); + const authenticatedApi = new ProjectApi(configWithAuth); + + const newProject: Project = { + name: 'test-project', + }; + + mockFetch = createMockFetch({ id: 'project-123', ...newProject }, 201); + global.fetch = mockFetch; + + await authenticatedApi.addProject({ project: newProject }); + + const callArgs = mockFetch.mock.calls[0] as [string, RequestInit]; + const { headers } = callArgs[1]; + expect((headers as Record).Authorization).toBe('Bearer test-token-add'); + }); + + it('should include Bearer token when configured for getProject', async () => { + const configWithAuth = new Configuration({ + basePath: 'http://localhost/api', + accessToken: 'test-token-get', + }); + const authenticatedApi = new ProjectApi(configWithAuth); + + mockFetch = createMockFetch({ id: 'project-123', name: 'test' }); + global.fetch = mockFetch; + + await authenticatedApi.getProject({ id: 'project-123' }); + + const callArgs = mockFetch.mock.calls[0] as [string, RequestInit]; + const { headers } = callArgs[1]; + expect((headers as Record).Authorization).toBe('Bearer test-token-get'); + }); + + it('should include Bearer token when configured for updateProject', async () => { + const configWithAuth = new Configuration({ + basePath: 'http://localhost/api', + accessToken: 'test-token-update', + }); + const authenticatedApi = new ProjectApi(configWithAuth); + + const updatedProject: Project = { + name: 'updated-project', + }; + + mockFetch = createMockFetch({ id: 'project-123', ...updatedProject }); + global.fetch = mockFetch; + + await authenticatedApi.updateProject({ id: 'project-123', project: updatedProject }); + + const callArgs = mockFetch.mock.calls[0] as [string, RequestInit]; + const { headers } = callArgs[1]; + expect((headers as Record).Authorization).toBe('Bearer test-token-update'); + }); + + it('should include Bearer token when configured for getFilterParams', async () => { + const configWithAuth = new Configuration({ + basePath: 'http://localhost/api', + accessToken: 'test-token-filter', + }); + const authenticatedApi = new ProjectApi(configWithAuth); + + mockFetch = createMockFetch(['param1', 'param2']); + global.fetch = mockFetch; + + await authenticatedApi.getFilterParams({ id: 'project-123' }); + + const callArgs = mockFetch.mock.calls[0] as [string, RequestInit]; + const { headers } = callArgs[1]; + expect((headers as Record).Authorization).toBe('Bearer test-token-filter'); + }); + it('should work without authentication when not configured', async () => { mockFetch = createMockFetch({ projects: [] }); global.fetch = mockFetch; diff --git a/src/apis/__tests__/ResultApi.test.ts b/src/apis/__tests__/ResultApi.test.ts index 6003692..16cdde8 100644 --- a/src/apis/__tests__/ResultApi.test.ts +++ b/src/apis/__tests__/ResultApi.test.ts @@ -385,7 +385,7 @@ describe('ResultApi', () => { }); describe('authentication', () => { - it('should include Bearer token when configured', async () => { + it('should include Bearer token when configured for getResultList', async () => { const configWithAuth = new Configuration({ basePath: 'http://localhost/api', accessToken: 'test-token-456', @@ -402,6 +402,59 @@ describe('ResultApi', () => { expect((headers as Record).Authorization).toBe('Bearer test-token-456'); }); + it('should include Bearer token when configured for addResult', async () => { + const configWithAuth = new Configuration({ + basePath: 'http://localhost/api', + accessToken: 'test-token-add', + }); + const authenticatedApi = new ResultApi(configWithAuth); + + const newResult = { metadata: { test: 'value' } }; + mockFetch = createMockFetch({ id: 'result-123', ...newResult }, 201); + global.fetch = mockFetch; + + await authenticatedApi.addResult({ result: newResult }); + + const callArgs = mockFetch.mock.calls[0] as [string, RequestInit]; + const { headers } = callArgs[1]; + expect((headers as Record).Authorization).toBe('Bearer test-token-add'); + }); + + it('should include Bearer token when configured for getResult', async () => { + const configWithAuth = new Configuration({ + basePath: 'http://localhost/api', + accessToken: 'test-token-get', + }); + const authenticatedApi = new ResultApi(configWithAuth); + + mockFetch = createMockFetch({ id: 'result-123' }); + global.fetch = mockFetch; + + await authenticatedApi.getResult({ id: 'result-123' }); + + const callArgs = mockFetch.mock.calls[0] as [string, RequestInit]; + const { headers } = callArgs[1]; + expect((headers as Record).Authorization).toBe('Bearer test-token-get'); + }); + + it('should include Bearer token when configured for updateResult', async () => { + const configWithAuth = new Configuration({ + basePath: 'http://localhost/api', + accessToken: 'test-token-update', + }); + const authenticatedApi = new ResultApi(configWithAuth); + + const updatedResult = { metadata: { updated: 'value' } }; + mockFetch = createMockFetch({ id: 'result-123', ...updatedResult }); + global.fetch = mockFetch; + + await authenticatedApi.updateResult({ id: 'result-123', result: updatedResult }); + + const callArgs = mockFetch.mock.calls[0] as [string, RequestInit]; + const { headers } = callArgs[1]; + expect((headers as Record).Authorization).toBe('Bearer test-token-update'); + }); + it('should work without authentication when not configured', async () => { mockFetch = createMockFetch({ results: [] }); global.fetch = mockFetch; diff --git a/src/apis/__tests__/RunApi.test.ts b/src/apis/__tests__/RunApi.test.ts index c0f88bc..ac02a7b 100644 --- a/src/apis/__tests__/RunApi.test.ts +++ b/src/apis/__tests__/RunApi.test.ts @@ -394,7 +394,7 @@ describe('RunApi', () => { }); describe('authentication', () => { - it('should include Bearer token when configured', async () => { + it('should include Bearer token when configured for getRun', async () => { const config = new Configuration({ basePath: 'http://localhost/api', accessToken: async () => 'test-token-123', @@ -426,6 +426,86 @@ describe('RunApi', () => { ); }); + it('should include Bearer token when configured for addRun', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-add', + }); + api = new RunApi(config); + + const newRun: Run = { metadata: { project: 'test' } }; + mockFetch = createMockFetch({ id: 'run-123', ...newRun }, 201); + global.fetch = mockFetch; + + await api.addRun({ run: newRun }); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-add', + }), + }) + ); + }); + + it('should include Bearer token when configured for getRunList', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-list', + }); + api = new RunApi(config); + + mockFetch = createMockFetch({ runs: [] }); + global.fetch = mockFetch; + + await api.getRunList({}); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-list', + }), + }) + ); + }); + + it('should include Bearer token when configured for updateRun', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-update', + }); + api = new RunApi(config); + + const updatedRun = { summary: { passed: 10 } }; + mockFetch = createMockFetch({ id: 'run-123', ...updatedRun }); + global.fetch = mockFetch; + + await api.updateRun({ id: 'run-123', run: updatedRun }); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-update', + }), + }) + ); + }); + it('should work without authentication when not configured', async () => { const runId = '123e4567-e89b-12d3-a456-426614174000'; const expectedRun: Run = { diff --git a/src/models/__tests__/ArtifactList.test.ts b/src/models/__tests__/ArtifactList.test.ts new file mode 100644 index 0000000..2593a15 --- /dev/null +++ b/src/models/__tests__/ArtifactList.test.ts @@ -0,0 +1,175 @@ +import { + type ArtifactList, + ArtifactListFromJSON, + ArtifactListToJSON, + instanceOfArtifactList, +} from '../ArtifactList'; + +describe('ArtifactList Model', () => { + describe('interface and types', () => { + it('should create a valid ArtifactList object with all fields', () => { + const artifactList: ArtifactList = { + artifacts: [ + { + id: '123e4567-e89b-12d3-a456-426614174000', + filename: 'test.log', + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + expect(artifactList.artifacts).toHaveLength(1); + expect(artifactList.pagination?.page).toBe(1); + }); + + it('should create an ArtifactList object with minimal fields', () => { + const artifactList: ArtifactList = { + artifacts: [], + }; + + expect(artifactList.artifacts).toEqual([]); + expect(artifactList.pagination).toBeUndefined(); + }); + + it('should allow undefined values for optional fields', () => { + const artifactList: ArtifactList = { + artifacts: undefined, + pagination: undefined, + }; + + expect(artifactList.artifacts).toBeUndefined(); + expect(artifactList.pagination).toBeUndefined(); + }); + }); + + describe('ArtifactListFromJSON', () => { + it('should convert JSON to ArtifactList object', () => { + const json = { + artifacts: [ + { + id: '123e4567-e89b-12d3-a456-426614174000', + filename: 'test.log', + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + const artifactList = ArtifactListFromJSON(json); + + expect(artifactList.artifacts).toHaveLength(1); + expect(artifactList.pagination?.page).toBe(1); + expect(artifactList.pagination?.pageSize).toBe(25); + }); + + it('should handle null values correctly', () => { + const json = { + artifacts: null, + pagination: null, + }; + + const artifactList = ArtifactListFromJSON(json); + + expect(artifactList.artifacts).toBeUndefined(); + expect(artifactList.pagination).toBeUndefined(); + }); + + it('should handle missing fields as undefined', () => { + const json = {}; + + const artifactList = ArtifactListFromJSON(json); + + expect(artifactList.artifacts).toBeUndefined(); + expect(artifactList.pagination).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = ArtifactListFromJSON(null); + expect(result).toBeNull(); + }); + }); + + describe('ArtifactListToJSON', () => { + it('should convert ArtifactList object to JSON', () => { + const artifactList: ArtifactList = { + artifacts: [ + { + id: '123e4567-e89b-12d3-a456-426614174000', + filename: 'test.log', + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + const json = ArtifactListToJSON(artifactList); + + expect(json.artifacts).toHaveLength(1); + expect(json.pagination.page).toBe(1); + }); + + it('should handle undefined fields', () => { + const artifactList: ArtifactList = { + artifacts: undefined, + }; + + const json = ArtifactListToJSON(artifactList); + + expect(json.artifacts).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = ArtifactListToJSON(null); + expect(result).toBeNull(); + }); + + it('should return undefined when passed undefined', () => { + const result = ArtifactListToJSON(undefined); + expect(result).toBeUndefined(); + }); + }); + + describe('instanceOfArtifactList', () => { + it('should return true for any object (as per implementation)', () => { + const result = instanceOfArtifactList({}); + expect(result).toBe(true); + }); + }); + + describe('JSON round-trip', () => { + it('should maintain data integrity through JSON conversion round-trip', () => { + const original: ArtifactList = { + artifacts: [ + { + id: '123e4567-e89b-12d3-a456-426614174000', + filename: 'test.log', + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + const json = ArtifactListToJSON(original); + const restored = ArtifactListFromJSON(json); + + expect(restored).toEqual(original); + }); + }); +}); diff --git a/src/models/__tests__/DashboardList.test.ts b/src/models/__tests__/DashboardList.test.ts new file mode 100644 index 0000000..1053809 --- /dev/null +++ b/src/models/__tests__/DashboardList.test.ts @@ -0,0 +1,175 @@ +import { + type DashboardList, + DashboardListFromJSON, + DashboardListToJSON, + instanceOfDashboardList, +} from '../DashboardList'; + +describe('DashboardList Model', () => { + describe('interface and types', () => { + it('should create a valid DashboardList object with all fields', () => { + const dashboardList: DashboardList = { + dashboards: [ + { + id: 'dashboard-123', + title: 'Test Dashboard', + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + expect(dashboardList.dashboards).toHaveLength(1); + expect(dashboardList.pagination?.page).toBe(1); + }); + + it('should create a DashboardList object with minimal fields', () => { + const dashboardList: DashboardList = { + dashboards: [], + }; + + expect(dashboardList.dashboards).toEqual([]); + expect(dashboardList.pagination).toBeUndefined(); + }); + + it('should allow undefined values for optional fields', () => { + const dashboardList: DashboardList = { + dashboards: undefined, + pagination: undefined, + }; + + expect(dashboardList.dashboards).toBeUndefined(); + expect(dashboardList.pagination).toBeUndefined(); + }); + }); + + describe('DashboardListFromJSON', () => { + it('should convert JSON to DashboardList object', () => { + const json = { + dashboards: [ + { + id: 'dashboard-123', + title: 'Test Dashboard', + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + const dashboardList = DashboardListFromJSON(json); + + expect(dashboardList.dashboards).toHaveLength(1); + expect(dashboardList.pagination?.page).toBe(1); + expect(dashboardList.pagination?.pageSize).toBe(25); + }); + + it('should handle null values correctly', () => { + const json = { + dashboards: null, + pagination: null, + }; + + const dashboardList = DashboardListFromJSON(json); + + expect(dashboardList.dashboards).toBeUndefined(); + expect(dashboardList.pagination).toBeUndefined(); + }); + + it('should handle missing fields as undefined', () => { + const json = {}; + + const dashboardList = DashboardListFromJSON(json); + + expect(dashboardList.dashboards).toBeUndefined(); + expect(dashboardList.pagination).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = DashboardListFromJSON(null); + expect(result).toBeNull(); + }); + }); + + describe('DashboardListToJSON', () => { + it('should convert DashboardList object to JSON', () => { + const dashboardList: DashboardList = { + dashboards: [ + { + id: 'dashboard-123', + title: 'Test Dashboard', + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + const json = DashboardListToJSON(dashboardList); + + expect(json.dashboards).toHaveLength(1); + expect(json.pagination.page).toBe(1); + }); + + it('should handle undefined fields', () => { + const dashboardList: DashboardList = { + dashboards: undefined, + }; + + const json = DashboardListToJSON(dashboardList); + + expect(json.dashboards).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = DashboardListToJSON(null); + expect(result).toBeNull(); + }); + + it('should return undefined when passed undefined', () => { + const result = DashboardListToJSON(undefined); + expect(result).toBeUndefined(); + }); + }); + + describe('instanceOfDashboardList', () => { + it('should return true for any object (as per implementation)', () => { + const result = instanceOfDashboardList({}); + expect(result).toBe(true); + }); + }); + + describe('JSON round-trip', () => { + it('should maintain data integrity through JSON conversion round-trip', () => { + const original: DashboardList = { + dashboards: [ + { + id: 'dashboard-123', + title: 'Test Dashboard', + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + const json = DashboardListToJSON(original); + const restored = DashboardListFromJSON(json); + + expect(restored).toEqual(original); + }); + }); +}); diff --git a/src/models/__tests__/GroupList.test.ts b/src/models/__tests__/GroupList.test.ts new file mode 100644 index 0000000..9d1ef89 --- /dev/null +++ b/src/models/__tests__/GroupList.test.ts @@ -0,0 +1,175 @@ +import { + type GroupList, + GroupListFromJSON, + GroupListToJSON, + instanceOfGroupList, +} from '../GroupList'; + +describe('GroupList Model', () => { + describe('interface and types', () => { + it('should create a valid GroupList object with all fields', () => { + const groupList: GroupList = { + groups: [ + { + id: 'group-123', + name: 'test-group', + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + expect(groupList.groups).toHaveLength(1); + expect(groupList.pagination?.page).toBe(1); + }); + + it('should create a GroupList object with minimal fields', () => { + const groupList: GroupList = { + groups: [], + }; + + expect(groupList.groups).toEqual([]); + expect(groupList.pagination).toBeUndefined(); + }); + + it('should allow undefined values for optional fields', () => { + const groupList: GroupList = { + groups: undefined, + pagination: undefined, + }; + + expect(groupList.groups).toBeUndefined(); + expect(groupList.pagination).toBeUndefined(); + }); + }); + + describe('GroupListFromJSON', () => { + it('should convert JSON to GroupList object', () => { + const json = { + groups: [ + { + id: 'group-123', + name: 'test-group', + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + const groupList = GroupListFromJSON(json); + + expect(groupList.groups).toHaveLength(1); + expect(groupList.pagination?.page).toBe(1); + expect(groupList.pagination?.pageSize).toBe(25); + }); + + it('should handle null values correctly', () => { + const json = { + groups: null, + pagination: null, + }; + + const groupList = GroupListFromJSON(json); + + expect(groupList.groups).toBeUndefined(); + expect(groupList.pagination).toBeUndefined(); + }); + + it('should handle missing fields as undefined', () => { + const json = {}; + + const groupList = GroupListFromJSON(json); + + expect(groupList.groups).toBeUndefined(); + expect(groupList.pagination).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = GroupListFromJSON(null); + expect(result).toBeNull(); + }); + }); + + describe('GroupListToJSON', () => { + it('should convert GroupList object to JSON', () => { + const groupList: GroupList = { + groups: [ + { + id: 'group-123', + name: 'test-group', + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + const json = GroupListToJSON(groupList); + + expect(json.groups).toHaveLength(1); + expect(json.pagination.page).toBe(1); + }); + + it('should handle undefined fields', () => { + const groupList: GroupList = { + groups: undefined, + }; + + const json = GroupListToJSON(groupList); + + expect(json.groups).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = GroupListToJSON(null); + expect(result).toBeNull(); + }); + + it('should return undefined when passed undefined', () => { + const result = GroupListToJSON(undefined); + expect(result).toBeUndefined(); + }); + }); + + describe('instanceOfGroupList', () => { + it('should return true for any object (as per implementation)', () => { + const result = instanceOfGroupList({}); + expect(result).toBe(true); + }); + }); + + describe('JSON round-trip', () => { + it('should maintain data integrity through JSON conversion round-trip', () => { + const original: GroupList = { + groups: [ + { + id: 'group-123', + name: 'test-group', + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + const json = GroupListToJSON(original); + const restored = GroupListFromJSON(json); + + expect(restored).toEqual(original); + }); + }); +}); diff --git a/src/models/__tests__/ProjectList.test.ts b/src/models/__tests__/ProjectList.test.ts new file mode 100644 index 0000000..71fe242 --- /dev/null +++ b/src/models/__tests__/ProjectList.test.ts @@ -0,0 +1,175 @@ +import { + type ProjectList, + ProjectListFromJSON, + ProjectListToJSON, + instanceOfProjectList, +} from '../ProjectList'; + +describe('ProjectList Model', () => { + describe('interface and types', () => { + it('should create a valid ProjectList object with all fields', () => { + const projectList: ProjectList = { + projects: [ + { + id: 'project-123', + name: 'test-project', + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + expect(projectList.projects).toHaveLength(1); + expect(projectList.pagination?.page).toBe(1); + }); + + it('should create a ProjectList object with minimal fields', () => { + const projectList: ProjectList = { + projects: [], + }; + + expect(projectList.projects).toEqual([]); + expect(projectList.pagination).toBeUndefined(); + }); + + it('should allow undefined values for optional fields', () => { + const projectList: ProjectList = { + projects: undefined, + pagination: undefined, + }; + + expect(projectList.projects).toBeUndefined(); + expect(projectList.pagination).toBeUndefined(); + }); + }); + + describe('ProjectListFromJSON', () => { + it('should convert JSON to ProjectList object', () => { + const json = { + projects: [ + { + id: 'project-123', + name: 'test-project', + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + const projectList = ProjectListFromJSON(json); + + expect(projectList.projects).toHaveLength(1); + expect(projectList.pagination?.page).toBe(1); + expect(projectList.pagination?.pageSize).toBe(25); + }); + + it('should handle null values correctly', () => { + const json = { + projects: null, + pagination: null, + }; + + const projectList = ProjectListFromJSON(json); + + expect(projectList.projects).toBeUndefined(); + expect(projectList.pagination).toBeUndefined(); + }); + + it('should handle missing fields as undefined', () => { + const json = {}; + + const projectList = ProjectListFromJSON(json); + + expect(projectList.projects).toBeUndefined(); + expect(projectList.pagination).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = ProjectListFromJSON(null); + expect(result).toBeNull(); + }); + }); + + describe('ProjectListToJSON', () => { + it('should convert ProjectList object to JSON', () => { + const projectList: ProjectList = { + projects: [ + { + id: 'project-123', + name: 'test-project', + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + const json = ProjectListToJSON(projectList); + + expect(json.projects).toHaveLength(1); + expect(json.pagination.page).toBe(1); + }); + + it('should handle undefined fields', () => { + const projectList: ProjectList = { + projects: undefined, + }; + + const json = ProjectListToJSON(projectList); + + expect(json.projects).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = ProjectListToJSON(null); + expect(result).toBeNull(); + }); + + it('should return undefined when passed undefined', () => { + const result = ProjectListToJSON(undefined); + expect(result).toBeUndefined(); + }); + }); + + describe('instanceOfProjectList', () => { + it('should return true for any object (as per implementation)', () => { + const result = instanceOfProjectList({}); + expect(result).toBe(true); + }); + }); + + describe('JSON round-trip', () => { + it('should maintain data integrity through JSON conversion round-trip', () => { + const original: ProjectList = { + projects: [ + { + id: 'project-123', + name: 'test-project', + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + const json = ProjectListToJSON(original); + const restored = ProjectListFromJSON(json); + + expect(restored).toEqual(original); + }); + }); +}); diff --git a/src/models/__tests__/ResultList.test.ts b/src/models/__tests__/ResultList.test.ts new file mode 100644 index 0000000..7366d1d --- /dev/null +++ b/src/models/__tests__/ResultList.test.ts @@ -0,0 +1,175 @@ +import { + type ResultList, + ResultListFromJSON, + ResultListToJSON, + instanceOfResultList, +} from '../ResultList'; + +describe('ResultList Model', () => { + describe('interface and types', () => { + it('should create a valid ResultList object with all fields', () => { + const resultList: ResultList = { + results: [ + { + id: 'result-123', + metadata: { test: 'value' }, + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + expect(resultList.results).toHaveLength(1); + expect(resultList.pagination?.page).toBe(1); + }); + + it('should create a ResultList object with minimal fields', () => { + const resultList: ResultList = { + results: [], + }; + + expect(resultList.results).toEqual([]); + expect(resultList.pagination).toBeUndefined(); + }); + + it('should allow undefined values for optional fields', () => { + const resultList: ResultList = { + results: undefined, + pagination: undefined, + }; + + expect(resultList.results).toBeUndefined(); + expect(resultList.pagination).toBeUndefined(); + }); + }); + + describe('ResultListFromJSON', () => { + it('should convert JSON to ResultList object', () => { + const json = { + results: [ + { + id: 'result-123', + metadata: { test: 'value' }, + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + const resultList = ResultListFromJSON(json); + + expect(resultList.results).toHaveLength(1); + expect(resultList.pagination?.page).toBe(1); + expect(resultList.pagination?.pageSize).toBe(25); + }); + + it('should handle null values correctly', () => { + const json = { + results: null, + pagination: null, + }; + + const resultList = ResultListFromJSON(json); + + expect(resultList.results).toBeUndefined(); + expect(resultList.pagination).toBeUndefined(); + }); + + it('should handle missing fields as undefined', () => { + const json = {}; + + const resultList = ResultListFromJSON(json); + + expect(resultList.results).toBeUndefined(); + expect(resultList.pagination).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = ResultListFromJSON(null); + expect(result).toBeNull(); + }); + }); + + describe('ResultListToJSON', () => { + it('should convert ResultList object to JSON', () => { + const resultList: ResultList = { + results: [ + { + id: 'result-123', + metadata: { test: 'value' }, + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + const json = ResultListToJSON(resultList); + + expect(json.results).toHaveLength(1); + expect(json.pagination.page).toBe(1); + }); + + it('should handle undefined fields', () => { + const resultList: ResultList = { + results: undefined, + }; + + const json = ResultListToJSON(resultList); + + expect(json.results).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = ResultListToJSON(null); + expect(result).toBeNull(); + }); + + it('should return undefined when passed undefined', () => { + const result = ResultListToJSON(undefined); + expect(result).toBeUndefined(); + }); + }); + + describe('instanceOfResultList', () => { + it('should return true for any object (as per implementation)', () => { + const result = instanceOfResultList({}); + expect(result).toBe(true); + }); + }); + + describe('JSON round-trip', () => { + it('should maintain data integrity through JSON conversion round-trip', () => { + const original: ResultList = { + results: [ + { + id: 'result-123', + metadata: { test: 'value' }, + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + const json = ResultListToJSON(original); + const restored = ResultListFromJSON(json); + + expect(restored).toEqual(original); + }); + }); +}); diff --git a/src/models/__tests__/RunList.test.ts b/src/models/__tests__/RunList.test.ts new file mode 100644 index 0000000..45d7f33 --- /dev/null +++ b/src/models/__tests__/RunList.test.ts @@ -0,0 +1,170 @@ +import { type RunList, RunListFromJSON, RunListToJSON, instanceOfRunList } from '../RunList'; + +describe('RunList Model', () => { + describe('interface and types', () => { + it('should create a valid RunList object with all fields', () => { + const runList: RunList = { + runs: [ + { + id: 'run-123', + metadata: { project: 'test' }, + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + expect(runList.runs).toHaveLength(1); + expect(runList.pagination?.page).toBe(1); + }); + + it('should create a RunList object with minimal fields', () => { + const runList: RunList = { + runs: [], + }; + + expect(runList.runs).toEqual([]); + expect(runList.pagination).toBeUndefined(); + }); + + it('should allow undefined values for optional fields', () => { + const runList: RunList = { + runs: undefined, + pagination: undefined, + }; + + expect(runList.runs).toBeUndefined(); + expect(runList.pagination).toBeUndefined(); + }); + }); + + describe('RunListFromJSON', () => { + it('should convert JSON to RunList object', () => { + const json = { + runs: [ + { + id: 'run-123', + metadata: { project: 'test' }, + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + const runList = RunListFromJSON(json); + + expect(runList.runs).toHaveLength(1); + expect(runList.pagination?.page).toBe(1); + expect(runList.pagination?.pageSize).toBe(25); + }); + + it('should handle null values correctly', () => { + const json = { + runs: null, + pagination: null, + }; + + const runList = RunListFromJSON(json); + + expect(runList.runs).toBeUndefined(); + expect(runList.pagination).toBeUndefined(); + }); + + it('should handle missing fields as undefined', () => { + const json = {}; + + const runList = RunListFromJSON(json); + + expect(runList.runs).toBeUndefined(); + expect(runList.pagination).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = RunListFromJSON(null); + expect(result).toBeNull(); + }); + }); + + describe('RunListToJSON', () => { + it('should convert RunList object to JSON', () => { + const runList: RunList = { + runs: [ + { + id: 'run-123', + metadata: { project: 'test' }, + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + const json = RunListToJSON(runList); + + expect(json.runs).toHaveLength(1); + expect(json.pagination.page).toBe(1); + }); + + it('should handle undefined fields', () => { + const runList: RunList = { + runs: undefined, + }; + + const json = RunListToJSON(runList); + + expect(json.runs).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = RunListToJSON(null); + expect(result).toBeNull(); + }); + + it('should return undefined when passed undefined', () => { + const result = RunListToJSON(undefined); + expect(result).toBeUndefined(); + }); + }); + + describe('instanceOfRunList', () => { + it('should return true for any object (as per implementation)', () => { + const result = instanceOfRunList({}); + expect(result).toBe(true); + }); + }); + + describe('JSON round-trip', () => { + it('should maintain data integrity through JSON conversion round-trip', () => { + const original: RunList = { + runs: [ + { + id: 'run-123', + metadata: { project: 'test' }, + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + const json = RunListToJSON(original); + const restored = RunListFromJSON(json); + + expect(restored).toEqual(original); + }); + }); +});