diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fa1eb71..67e4d4c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '20' cache: 'yarn' - name: Install dependencies @@ -40,7 +40,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '20' cache: 'yarn' - name: Install dependencies @@ -57,7 +57,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '20' cache: 'yarn' - name: Install dependencies diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index bda6d5e..6d5f848 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 20 cache: 'yarn' - run: yarn install --frozen-lockfile - run: yarn test @@ -30,7 +30,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 20 cache: 'yarn' registry-url: https://registry.npmjs.org/ - run: yarn install --frozen-lockfile diff --git a/.gitignore b/.gitignore index 34f9f14..98ee422 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ generate.log # Testing coverage/ +coverage-output.txt .nyc_output/ # Temporary files diff --git a/.nvmrc b/.nvmrc index 2bd5a0a..209e3ef 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22 +20 diff --git a/eslint.config.mjs b/eslint.config.mjs index 6c49eb0..156495e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -233,9 +233,18 @@ export default tseslint.config( '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', + // Relax type safety rules for test mocking and dynamic test data + '@typescript-eslint/no-unsafe-assignment': 'off', // Mock responses often use any + '@typescript-eslint/no-unsafe-member-access': 'off', // Accessing mock object properties + '@typescript-eslint/no-unsafe-call': 'off', // Calling mocked functions + '@typescript-eslint/no-unsafe-argument': 'off', // Passing mock data to functions + '@typescript-eslint/no-explicit-any': 'warn', // Warn but allow any in tests + // Allow common test patterns '@typescript-eslint/unbound-method': 'off', // Jest mocks often trigger this '@typescript-eslint/no-non-null-assertion': 'warn', // Sometimes needed in tests, but warn + '@typescript-eslint/require-await': 'off', // Mock async functions often don't need await + '@typescript-eslint/no-unnecessary-condition': 'off', // Test assertions may check conditions 'no-console': 'off', // Allow console in tests for debugging }, }, diff --git a/package.json b/package.json index 868ae9f..34417cb 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,6 @@ "LICENSE" ], "engines": { - "node": ">=22.0.0" + "node": ">=20.0.0" } } diff --git a/src/__tests__/test-utils.test.ts b/src/__tests__/test-utils.test.ts index 4cfc9ef..6674098 100644 --- a/src/__tests__/test-utils.test.ts +++ b/src/__tests__/test-utils.test.ts @@ -2,9 +2,6 @@ * Tests for test utilities */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/strict-boolean-expressions */ import { diff --git a/src/apis/__tests__/ArtifactApi.test.ts b/src/apis/__tests__/ArtifactApi.test.ts new file mode 100644 index 0000000..e58a6a3 --- /dev/null +++ b/src/apis/__tests__/ArtifactApi.test.ts @@ -0,0 +1,512 @@ +import { ArtifactApi } from '../ArtifactApi'; +import type { Artifact, ArtifactList } from '../../models'; +import { Configuration } from '../../runtime'; +import { createMockFetch, createMockResponse } from '../../__tests__/test-utils'; + +describe('ArtifactApi', () => { + let api: ArtifactApi; + let mockFetch: jest.Mock; + + beforeEach(() => { + const config = new Configuration({ + basePath: 'http://localhost/api', + }); + api = new ArtifactApi(config); + }); + + describe('uploadArtifact', () => { + it('should upload a file artifact', async () => { + const fileContent = 'test file content'; + const fileBlob = new Blob([fileContent], { type: 'text/plain' }); + const filename = 'test.txt'; + + const responseArtifact: Artifact = { + id: '123e4567-e89b-12d3-a456-426614174000', + filename, + uploadDate: '2024-01-01T00:00:00.000Z', + }; + + mockFetch = createMockFetch(responseArtifact, 201); + global.fetch = mockFetch; + + const result = await api.uploadArtifact({ + filename, + file: fileBlob, + }); + + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost/api/artifact', + expect.objectContaining({ + method: 'POST', + }) + ); + expect(result.id).toBe('123e4567-e89b-12d3-a456-426614174000'); + expect(result.filename).toBe(filename); + }); + + it('should attach artifact to a result', async () => { + const fileBlob = new Blob(['data'], { type: 'text/plain' }); + const resultId = '123e4567-e89b-12d3-a456-426614174001'; + + const responseArtifact: Artifact = { + id: '123e4567-e89b-12d3-a456-426614174000', + filename: 'test.txt', + resultId, + }; + + mockFetch = createMockFetch(responseArtifact, 201); + global.fetch = mockFetch; + + const result = await api.uploadArtifact({ + filename: 'test.txt', + file: fileBlob, + resultId, + }); + + expect(result.id).toBe('123e4567-e89b-12d3-a456-426614174000'); + }); + + it('should attach artifact to a run', async () => { + const fileBlob = new Blob(['data'], { type: 'text/plain' }); + const runId = '123e4567-e89b-12d3-a456-426614174002'; + + const responseArtifact: Artifact = { + id: '123e4567-e89b-12d3-a456-426614174000', + filename: 'test.txt', + runId, + }; + + mockFetch = createMockFetch(responseArtifact, 201); + global.fetch = mockFetch; + + const result = await api.uploadArtifact({ + filename: 'test.txt', + file: fileBlob, + runId, + }); + + expect(result.id).toBe('123e4567-e89b-12d3-a456-426614174000'); + }); + + it('should include additional metadata', async () => { + const fileBlob = new Blob(['data'], { type: 'application/json' }); + const additionalMetadata = { + category: 'logs', + source: 'jenkins', + }; + + const responseArtifact: Artifact = { + id: '123e4567-e89b-12d3-a456-426614174000', + filename: 'log.json', + }; + + mockFetch = createMockFetch(responseArtifact, 201); + global.fetch = mockFetch; + + await api.uploadArtifact({ + filename: 'log.json', + file: fileBlob, + additionalMetadata, + }); + + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it('should handle upload errors', async () => { + const fileBlob = new Blob(['data'], { type: 'text/plain' }); + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'File too large' }, 413)); + global.fetch = mockFetch; + + await expect( + api.uploadArtifact({ + filename: 'test.txt', + file: fileBlob, + }) + ).rejects.toThrow(); + }); + }); + + describe('getArtifact', () => { + it('should fetch an artifact by ID', async () => { + const artifactId = '123e4567-e89b-12d3-a456-426614174000'; + const expectedArtifact: Artifact = { + id: artifactId, + filename: 'screenshot.png', + uploadDate: '2024-01-01T00:00:00.000Z', + }; + + mockFetch = createMockFetch(expectedArtifact); + global.fetch = mockFetch; + + const result = await api.getArtifact({ id: artifactId }); + + expect(mockFetch).toHaveBeenCalledWith( + `http://localhost/api/artifact/${artifactId}`, + expect.objectContaining({ + method: 'GET', + }) + ); + expect(result.id).toBe(artifactId); + expect(result.filename).toBe('screenshot.png'); + }); + + it('should handle 404 when artifact not found', async () => { + const artifactId = '123e4567-e89b-12d3-a456-426614174000'; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Not Found' }, 404)); + global.fetch = mockFetch; + + await expect(api.getArtifact({ id: artifactId })).rejects.toThrow(); + }); + + it('should require id parameter', async () => { + await expect(api.getArtifact({ id: null as unknown as string })).rejects.toThrow(); + }); + }); + + describe('getArtifactList', () => { + it('should fetch a list of artifacts', async () => { + const mockArtifactList: ArtifactList = { + artifacts: [ + { + id: '123e4567-e89b-12d3-a456-426614174000', + filename: 'log.txt', + }, + { + id: '123e4567-e89b-12d3-a456-426614174001', + filename: 'screenshot.png', + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 2, + totalPages: 1, + }, + }; + + mockFetch = createMockFetch(mockArtifactList); + global.fetch = mockFetch; + + const result = await api.getArtifactList({}); + + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost/api/artifact', + expect.objectContaining({ + method: 'GET', + }) + ); + expect(result.artifacts).toHaveLength(2); + }); + + it('should handle pagination parameters', async () => { + const mockArtifactList: ArtifactList = { + artifacts: [], + pagination: { + page: 2, + pageSize: 10, + totalItems: 0, + totalPages: 0, + }, + }; + + mockFetch = createMockFetch(mockArtifactList); + global.fetch = mockFetch; + + await api.getArtifactList({ page: 2, pageSize: 10 }); + + const url = mockFetch.mock.calls[0][0] as string; + expect(url).toContain('page=2'); + expect(url).toContain('pageSize=10'); + }); + + it('should filter by resultId', async () => { + const resultId = '123e4567-e89b-12d3-a456-426614174000'; + const mockArtifactList: ArtifactList = { + artifacts: [ + { + id: '123e4567-e89b-12d3-a456-426614174001', + filename: 'test.log', + resultId, + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + mockFetch = createMockFetch(mockArtifactList); + global.fetch = mockFetch; + + await api.getArtifactList({ resultId }); + + const url = mockFetch.mock.calls[0][0] as string; + expect(url).toContain(`resultId=${resultId}`); + }); + + it('should filter by runId', async () => { + const runId = '123e4567-e89b-12d3-a456-426614174000'; + const mockArtifactList: ArtifactList = { + artifacts: [ + { + id: '123e4567-e89b-12d3-a456-426614174001', + filename: 'test.log', + runId, + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1, + totalPages: 1, + }, + }; + + mockFetch = createMockFetch(mockArtifactList); + global.fetch = mockFetch; + + await api.getArtifactList({ runId }); + + const url = mockFetch.mock.calls[0][0] as string; + expect(url).toContain(`runId=${runId}`); + }); + + it('should return empty list when no artifacts exist', async () => { + const mockArtifactList: ArtifactList = { + artifacts: [], + pagination: { + page: 1, + pageSize: 25, + totalItems: 0, + totalPages: 0, + }, + }; + + mockFetch = createMockFetch(mockArtifactList); + global.fetch = mockFetch; + + const result = await api.getArtifactList({}); + + expect(result.artifacts).toHaveLength(0); + }); + }); + + describe('downloadArtifact', () => { + it('should download an artifact as Blob', async () => { + const artifactId = '123e4567-e89b-12d3-a456-426614174000'; + const mockBlob = new Blob(['file content'], { type: 'text/plain' }); + + mockFetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + blob: async () => mockBlob, + }); + global.fetch = mockFetch; + + const result = await api.downloadArtifact({ id: artifactId }); + + expect(mockFetch).toHaveBeenCalledWith( + `http://localhost/api/artifact/${artifactId}/download`, + expect.objectContaining({ + method: 'GET', + }) + ); + expect(result).toBeInstanceOf(Blob); + }); + + it('should handle download errors', async () => { + const artifactId = '123e4567-e89b-12d3-a456-426614174000'; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Not Found' }, 404)); + global.fetch = mockFetch; + + await expect(api.downloadArtifact({ id: artifactId })).rejects.toThrow(); + }); + + it('should require id parameter', async () => { + await expect(api.downloadArtifact({ id: null as unknown as string })).rejects.toThrow(); + }); + }); + + describe('viewArtifact', () => { + it('should stream an artifact as Blob', async () => { + const artifactId = '123e4567-e89b-12d3-a456-426614174000'; + const mockBlob = new Blob(['content'], { type: 'text/html' }); + + mockFetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + blob: async () => mockBlob, + }); + global.fetch = mockFetch; + + const result = await api.viewArtifact({ id: artifactId }); + + expect(mockFetch).toHaveBeenCalledWith( + `http://localhost/api/artifact/${artifactId}/view`, + expect.objectContaining({ + method: 'GET', + }) + ); + expect(result).toBeInstanceOf(Blob); + }); + + it('should handle viewing errors', async () => { + const artifactId = '123e4567-e89b-12d3-a456-426614174000'; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Forbidden' }, 403)); + global.fetch = mockFetch; + + await expect(api.viewArtifact({ id: artifactId })).rejects.toThrow(); + }); + + it('should require id parameter', async () => { + await expect(api.viewArtifact({ id: null as unknown as string })).rejects.toThrow(); + }); + }); + + describe('deleteArtifact', () => { + it('should delete an artifact', async () => { + const artifactId = '123e4567-e89b-12d3-a456-426614174000'; + + mockFetch = jest.fn().mockResolvedValue({ + ok: true, + status: 204, + }); + global.fetch = mockFetch; + + await api.deleteArtifact({ id: artifactId }); + + expect(mockFetch).toHaveBeenCalledWith( + `http://localhost/api/artifact/${artifactId}`, + expect.objectContaining({ + method: 'DELETE', + }) + ); + }); + + it('should handle delete errors', async () => { + const artifactId = '123e4567-e89b-12d3-a456-426614174000'; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Forbidden' }, 403)); + global.fetch = mockFetch; + + await expect(api.deleteArtifact({ id: artifactId })).rejects.toThrow(); + }); + + it('should require id parameter', async () => { + await expect(api.deleteArtifact({ id: null as unknown as string })).rejects.toThrow(); + }); + }); + + describe('authentication', () => { + it('should include Bearer token when configured', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-456', + }); + api = new ArtifactApi(config); + + const artifactId = '123e4567-e89b-12d3-a456-426614174000'; + const expectedArtifact: Artifact = { + id: artifactId, + filename: 'test.txt', + }; + + mockFetch = createMockFetch(expectedArtifact); + global.fetch = mockFetch; + + await api.getArtifact({ id: artifactId }); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-456', + }), + }) + ); + }); + + it('should work without authentication when not configured', async () => { + const artifactId = '123e4567-e89b-12d3-a456-426614174000'; + const expectedArtifact: Artifact = { + id: artifactId, + filename: 'test.txt', + }; + + mockFetch = createMockFetch(expectedArtifact); + global.fetch = mockFetch; + + await api.getArtifact({ id: artifactId }); + + const callArgs = mockFetch.mock.calls[0][1] as RequestInit; + const headers = callArgs.headers as Record; + expect(headers.Authorization).toBeUndefined(); + }); + }); + + describe('error handling', () => { + it('should handle 400 Bad Request errors', async () => { + const fileBlob = new Blob(['data'], { type: 'text/plain' }); + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Bad Request' }, 400)); + global.fetch = mockFetch; + + await expect( + api.uploadArtifact({ + filename: 'test.txt', + file: fileBlob, + }) + ).rejects.toThrow(); + }); + + it('should handle 401 Unauthorized errors', async () => { + const artifactId = '123e4567-e89b-12d3-a456-426614174000'; + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Unauthorized' }, 401)); + global.fetch = mockFetch; + + await expect(api.getArtifact({ id: artifactId })).rejects.toThrow(); + }); + + it('should handle 403 Forbidden errors', async () => { + const artifactId = '123e4567-e89b-12d3-a456-426614174000'; + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Forbidden' }, 403)); + global.fetch = mockFetch; + + await expect(api.deleteArtifact({ id: artifactId })).rejects.toThrow(); + }); + + it('should handle 404 Not Found errors', async () => { + const artifactId = '123e4567-e89b-12d3-a456-426614174000'; + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Not Found' }, 404)); + global.fetch = mockFetch; + + await expect(api.getArtifact({ id: artifactId })).rejects.toThrow(); + }); + + it('should handle 500 Internal Server Error', async () => { + mockFetch = jest + .fn() + .mockResolvedValue(createMockResponse({ error: 'Internal Server Error' }, 500)); + global.fetch = mockFetch; + + await expect(api.getArtifactList({})).rejects.toThrow(); + }); + + it('should handle network errors', async () => { + mockFetch = jest.fn().mockRejectedValue(new Error('Network error')); + global.fetch = mockFetch; + + await expect(api.getArtifactList({})).rejects.toThrow(); + }); + }); +}); diff --git a/src/apis/__tests__/DashboardApi.test.ts b/src/apis/__tests__/DashboardApi.test.ts new file mode 100644 index 0000000..02d5b0a --- /dev/null +++ b/src/apis/__tests__/DashboardApi.test.ts @@ -0,0 +1,429 @@ +import { DashboardApi } from '../DashboardApi'; +import type { Dashboard, DashboardList } from '../../models'; +import { Configuration } from '../../runtime'; +import { createMockFetch, createMockResponse } from '../../__tests__/test-utils'; + +describe('DashboardApi', () => { + let api: DashboardApi; + let mockFetch: jest.Mock; + + beforeEach(() => { + const config = new Configuration({ + basePath: 'http://localhost/api', + }); + api = new DashboardApi(config); + }); + + describe('addDashboard', () => { + it('should create a new dashboard', async () => { + const newDashboard: Dashboard = { + title: 'Test Dashboard', + description: 'A test dashboard', + }; + + const responseDashboard: Dashboard = { + id: 'dashboard-123', + ...newDashboard, + }; + + mockFetch = createMockFetch(responseDashboard, 201); + global.fetch = mockFetch; + + const result = await api.addDashboard({ dashboard: newDashboard }); + + expect(mockFetch).toHaveBeenCalledTimes(1); + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost/api/dashboard', + expect.objectContaining>({ + method: 'POST', + headers: expect.objectContaining>({ + 'Content-Type': 'application/json', + }), + }) + ); + expect(result.id).toBe('dashboard-123'); + expect(result.title).toBe('Test Dashboard'); + }); + + it('should handle errors when creating a dashboard', async () => { + const newDashboard: Dashboard = { + title: 'Test', + }; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Bad Request' }, 400)); + global.fetch = mockFetch; + + await expect(api.addDashboard({ dashboard: newDashboard })).rejects.toThrow(); + }); + }); + + describe('getDashboard', () => { + it('should fetch a dashboard by ID', async () => { + const dashboardId = 'dashboard-456'; + const expectedDashboard: Dashboard = { + id: dashboardId, + title: 'My Dashboard', + description: 'Dashboard description', + filters: 'project_id=123', + }; + + mockFetch = createMockFetch(expectedDashboard); + global.fetch = mockFetch; + + const result = await api.getDashboard({ id: dashboardId }); + + expect(mockFetch).toHaveBeenCalledWith( + `http://localhost/api/dashboard/${dashboardId}`, + expect.objectContaining({ + method: 'GET', + }) + ); + expect(result.id).toBe(dashboardId); + expect(result.title).toBe('My Dashboard'); + }); + + it('should handle 404 when dashboard not found', async () => { + const dashboardId = 'non-existent'; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Not Found' }, 404)); + global.fetch = mockFetch; + + await expect(api.getDashboard({ id: dashboardId })).rejects.toThrow(); + }); + + it('should require id parameter', async () => { + await expect(api.getDashboard({ id: null as unknown as string })).rejects.toThrow(); + }); + }); + + describe('getDashboardList', () => { + it('should fetch a list of dashboards', async () => { + const mockDashboardList: DashboardList = { + dashboards: [ + { + id: 'dashboard-1', + title: 'Dashboard One', + }, + { + id: 'dashboard-2', + title: 'Dashboard Two', + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 2, + totalPages: 1, + }, + }; + + mockFetch = createMockFetch(mockDashboardList); + global.fetch = mockFetch; + + const result = await api.getDashboardList({}); + + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost/api/dashboard', + expect.objectContaining({ + method: 'GET', + }) + ); + expect(result.dashboards).toHaveLength(2); + }); + + it('should handle pagination parameters', async () => { + const mockDashboardList: DashboardList = { + dashboards: [], + pagination: { + page: 2, + pageSize: 10, + totalItems: 0, + totalPages: 0, + }, + }; + + mockFetch = createMockFetch(mockDashboardList); + global.fetch = mockFetch; + + await api.getDashboardList({ page: 2, pageSize: 10 }); + + const url = mockFetch.mock.calls[0][0] as string; + expect(url).toContain('page=2'); + expect(url).toContain('pageSize=10'); + }); + + it('should filter by projectId', async () => { + const projectId = 'project-123'; + const mockDashboardList: DashboardList = { + dashboards: [], + pagination: { + page: 1, + pageSize: 25, + totalItems: 0, + totalPages: 0, + }, + }; + + mockFetch = createMockFetch(mockDashboardList); + global.fetch = mockFetch; + + await api.getDashboardList({ projectId }); + + const url = mockFetch.mock.calls[0][0] as string; + expect(url).toContain(`project_id=${projectId}`); + }); + + it('should filter by userId', async () => { + const userId = 'user-456'; + const mockDashboardList: DashboardList = { + dashboards: [], + pagination: { + page: 1, + pageSize: 25, + totalItems: 0, + totalPages: 0, + }, + }; + + mockFetch = createMockFetch(mockDashboardList); + global.fetch = mockFetch; + + await api.getDashboardList({ userId }); + + const url = mockFetch.mock.calls[0][0] as string; + expect(url).toContain(`user_id=${userId}`); + }); + + it('should handle filter parameters', async () => { + const mockDashboardList: DashboardList = { + dashboards: [], + pagination: { + page: 1, + pageSize: 25, + totalItems: 0, + totalPages: 0, + }, + }; + + mockFetch = createMockFetch(mockDashboardList); + global.fetch = mockFetch; + + await api.getDashboardList({ filter: ['title~test'] }); + + const url = mockFetch.mock.calls[0][0] as string; + expect(url).toContain('filter='); + }); + + it('should return empty list when no dashboards exist', async () => { + const mockDashboardList: DashboardList = { + dashboards: [], + pagination: { + page: 1, + pageSize: 25, + totalItems: 0, + totalPages: 0, + }, + }; + + mockFetch = createMockFetch(mockDashboardList); + global.fetch = mockFetch; + + const result = await api.getDashboardList({}); + + expect(result.dashboards).toHaveLength(0); + }); + }); + + describe('updateDashboard', () => { + it('should update an existing dashboard', async () => { + const dashboardId = 'dashboard-789'; + const updatedDashboard: Dashboard = { + id: dashboardId, + title: 'Updated Dashboard', + description: 'Updated description', + filters: 'status=passed', + }; + + mockFetch = createMockFetch(updatedDashboard); + global.fetch = mockFetch; + + const result = await api.updateDashboard({ id: dashboardId, dashboard: updatedDashboard }); + + expect(mockFetch).toHaveBeenCalledWith( + `http://localhost/api/dashboard/${dashboardId}`, + expect.objectContaining({ + method: 'PUT', + }) + ); + expect(result.title).toBe('Updated Dashboard'); + }); + + it('should handle partial updates', async () => { + const dashboardId = 'dashboard-999'; + const partialUpdate: Dashboard = { + title: 'New Title', + }; + + const responseDashboard: Dashboard = { + id: dashboardId, + ...partialUpdate, + }; + + mockFetch = createMockFetch(responseDashboard); + global.fetch = mockFetch; + + const result = await api.updateDashboard({ id: dashboardId, dashboard: partialUpdate }); + + expect(result.id).toBe(dashboardId); + expect(result.title).toBe('New Title'); + }); + + it('should require id parameter', async () => { + const dashboard: Dashboard = { + title: 'Test', + }; + + await expect( + api.updateDashboard({ id: null as unknown as string, dashboard }) + ).rejects.toThrow(); + }); + }); + + describe('deleteDashboard', () => { + it('should delete a dashboard', async () => { + const dashboardId = 'dashboard-delete'; + + mockFetch = jest.fn().mockResolvedValue({ + ok: true, + status: 204, + }); + global.fetch = mockFetch; + + await api.deleteDashboard({ id: dashboardId }); + + expect(mockFetch).toHaveBeenCalledWith( + `http://localhost/api/dashboard/${dashboardId}`, + expect.objectContaining({ + method: 'DELETE', + }) + ); + }); + + it('should handle delete errors', async () => { + const dashboardId = 'dashboard-error'; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Forbidden' }, 403)); + global.fetch = mockFetch; + + await expect(api.deleteDashboard({ id: dashboardId })).rejects.toThrow(); + }); + + it('should require id parameter', async () => { + await expect(api.deleteDashboard({ id: null as unknown as string })).rejects.toThrow(); + }); + }); + + describe('authentication', () => { + it('should include Bearer token when configured', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-dashboard', + }); + api = new DashboardApi(config); + + const dashboardId = 'dashboard-auth'; + const expectedDashboard: Dashboard = { + id: dashboardId, + title: 'Test', + }; + + mockFetch = createMockFetch(expectedDashboard); + global.fetch = mockFetch; + + await api.getDashboard({ id: dashboardId }); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-dashboard', + }), + }) + ); + }); + + it('should work without authentication when not configured', async () => { + const dashboardId = 'dashboard-no-auth'; + const expectedDashboard: Dashboard = { + id: dashboardId, + title: 'Test', + }; + + mockFetch = createMockFetch(expectedDashboard); + global.fetch = mockFetch; + + await api.getDashboard({ id: dashboardId }); + + const callArgs = mockFetch.mock.calls[0][1] as RequestInit; + const headers = callArgs.headers as Record; + expect(headers.Authorization).toBeUndefined(); + }); + }); + + describe('error handling', () => { + it('should handle 400 Bad Request errors', async () => { + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Bad Request' }, 400)); + global.fetch = mockFetch; + + await expect(api.addDashboard({ dashboard: {} })).rejects.toThrow(); + }); + + it('should handle 401 Unauthorized errors', async () => { + const dashboardId = 'dashboard-123'; + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Unauthorized' }, 401)); + global.fetch = mockFetch; + + await expect(api.getDashboard({ id: dashboardId })).rejects.toThrow(); + }); + + it('should handle 403 Forbidden errors', async () => { + const dashboardId = 'dashboard-123'; + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Forbidden' }, 403)); + global.fetch = mockFetch; + + await expect(api.deleteDashboard({ id: dashboardId })).rejects.toThrow(); + }); + + it('should handle 404 Not Found errors', async () => { + const dashboardId = 'non-existent'; + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Not Found' }, 404)); + global.fetch = mockFetch; + + await expect(api.getDashboard({ id: dashboardId })).rejects.toThrow(); + }); + + it('should handle 500 Internal Server Error', async () => { + mockFetch = jest + .fn() + .mockResolvedValue(createMockResponse({ error: 'Internal Server Error' }, 500)); + global.fetch = mockFetch; + + await expect(api.getDashboardList({})).rejects.toThrow(); + }); + + it('should handle network errors', async () => { + mockFetch = jest.fn().mockRejectedValue(new Error('Network error')); + global.fetch = mockFetch; + + await expect(api.getDashboardList({})).rejects.toThrow(); + }); + }); +}); diff --git a/src/apis/__tests__/GroupApi.test.ts b/src/apis/__tests__/GroupApi.test.ts new file mode 100644 index 0000000..5647f67 --- /dev/null +++ b/src/apis/__tests__/GroupApi.test.ts @@ -0,0 +1,397 @@ +import { GroupApi } from '../GroupApi'; +import type { Group, GroupList } from '../../models'; +import { Configuration } from '../../runtime'; +import { createMockFetch, createMockResponse } from '../../__tests__/test-utils'; + +describe('GroupApi', () => { + let api: GroupApi; + let mockFetch: jest.Mock; + + beforeEach(() => { + const config = new Configuration({ + basePath: 'http://localhost/api', + }); + api = new GroupApi(config); + }); + + describe('addGroup', () => { + it('should create a new group', async () => { + const newGroup: Group = { + name: 'engineering-team', + }; + + const responseGroup: Group = { + id: 'group-123', + ...newGroup, + }; + + mockFetch = createMockFetch(responseGroup, 201); + global.fetch = mockFetch; + + const result = await api.addGroup({ group: newGroup }); + + expect(mockFetch).toHaveBeenCalledTimes(1); + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost/api/group', + expect.objectContaining>({ + method: 'POST', + headers: expect.objectContaining>({ + 'Content-Type': 'application/json', + }), + }) + ); + expect(result.id).toBe('group-123'); + expect(result.name).toBe('engineering-team'); + }); + + it('should send group data with correct JSON format', async () => { + const newGroup: Group = { + name: 'test-team', + }; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse(newGroup, 201)); + global.fetch = mockFetch; + + await api.addGroup({ group: newGroup }); + + expect(mockFetch).toHaveBeenCalledTimes(1); + const callArgs = mockFetch.mock.calls[0]; + expect(callArgs[0]).toBe('http://localhost/api/group'); + }); + + it('should handle errors when creating a group', async () => { + const newGroup: Group = { + name: 'test-team', + }; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Bad Request' }, 400)); + global.fetch = mockFetch; + + await expect(api.addGroup({ group: newGroup })).rejects.toThrow(); + }); + + it('should handle duplicate group name errors', async () => { + const newGroup: Group = { + name: 'existing-group', + }; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Conflict' }, 409)); + global.fetch = mockFetch; + + await expect(api.addGroup({ group: newGroup })).rejects.toThrow(); + }); + }); + + describe('getGroup', () => { + it('should fetch a group by ID', async () => { + const groupId = 'group-456'; + const expectedGroup: Group = { + id: groupId, + name: 'qa-team', + }; + + mockFetch = createMockFetch(expectedGroup); + global.fetch = mockFetch; + + const result = await api.getGroup({ id: groupId }); + + expect(mockFetch).toHaveBeenCalledWith( + `http://localhost/api/group/${groupId}`, + expect.objectContaining({ + method: 'GET', + }) + ); + expect(result.id).toBe(groupId); + expect(result.name).toBe('qa-team'); + }); + + it('should handle 404 when group not found', async () => { + const groupId = 'non-existent-group'; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Not Found' }, 404)); + global.fetch = mockFetch; + + await expect(api.getGroup({ id: groupId })).rejects.toThrow(); + }); + + it('should require id parameter', async () => { + await expect(api.getGroup({ id: null as unknown as string })).rejects.toThrow(); + }); + }); + + describe('getGroupList', () => { + it('should fetch a list of groups', async () => { + const mockGroupList: GroupList = { + groups: [ + { + id: 'group-1', + name: 'team-alpha', + }, + { + id: 'group-2', + name: 'team-beta', + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 2, + totalPages: 1, + }, + }; + + mockFetch = createMockFetch(mockGroupList); + global.fetch = mockFetch; + + const result = await api.getGroupList({}); + + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost/api/group', + expect.objectContaining({ + method: 'GET', + }) + ); + expect(result.groups).toHaveLength(2); + }); + + it('should handle pagination parameters', async () => { + const mockGroupList: GroupList = { + groups: [], + pagination: { + page: 2, + pageSize: 10, + totalItems: 0, + totalPages: 0, + }, + }; + + mockFetch = createMockFetch(mockGroupList); + global.fetch = mockFetch; + + await api.getGroupList({ page: 2, pageSize: 10 }); + + const url = mockFetch.mock.calls[0][0] as string; + expect(url).toContain('page=2'); + expect(url).toContain('pageSize=10'); + }); + + it('should return empty list when no groups exist', async () => { + const mockGroupList: GroupList = { + groups: [], + pagination: { + page: 1, + pageSize: 25, + totalItems: 0, + totalPages: 0, + }, + }; + + mockFetch = createMockFetch(mockGroupList); + global.fetch = mockFetch; + + const result = await api.getGroupList({}); + + expect(result.groups).toHaveLength(0); + }); + + it('should handle large page sizes', async () => { + const mockGroupList: GroupList = { + groups: [], + pagination: { + page: 1, + pageSize: 100, + totalItems: 0, + totalPages: 0, + }, + }; + + mockFetch = createMockFetch(mockGroupList); + global.fetch = mockFetch; + + await api.getGroupList({ pageSize: 100 }); + + const url = mockFetch.mock.calls[0][0] as string; + expect(url).toContain('pageSize=100'); + }); + }); + + describe('updateGroup', () => { + it('should update an existing group', async () => { + const groupId = 'group-789'; + const updatedGroup: Group = { + id: groupId, + name: 'updated-team-name', + }; + + mockFetch = createMockFetch(updatedGroup); + global.fetch = mockFetch; + + const result = await api.updateGroup({ id: groupId, group: updatedGroup }); + + expect(mockFetch).toHaveBeenCalledWith( + `http://localhost/api/group/${groupId}`, + expect.objectContaining({ + method: 'PUT', + }) + ); + expect(result.name).toBe('updated-team-name'); + }); + + it('should handle partial updates', async () => { + const groupId = 'group-999'; + const partialUpdate: Group = { + name: 'new-name', + }; + + const responseGroup: Group = { + id: groupId, + ...partialUpdate, + }; + + mockFetch = createMockFetch(responseGroup); + global.fetch = mockFetch; + + const result = await api.updateGroup({ id: groupId, group: partialUpdate }); + + expect(result.id).toBe(groupId); + expect(result.name).toBe('new-name'); + }); + + it('should require id parameter', async () => { + const group: Group = { + name: 'test', + }; + + await expect(api.updateGroup({ id: null as unknown as string, group })).rejects.toThrow(); + }); + + it('should handle update errors', async () => { + const groupId = 'group-error'; + const group: Group = { + name: 'test', + }; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Bad Request' }, 400)); + global.fetch = mockFetch; + + await expect(api.updateGroup({ id: groupId, group })).rejects.toThrow(); + }); + }); + + describe('authentication', () => { + it('should include Bearer token when configured', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-abc', + }); + api = new GroupApi(config); + + const groupId = 'group-auth'; + const expectedGroup: Group = { + id: groupId, + name: 'test-group', + }; + + mockFetch = createMockFetch(expectedGroup); + global.fetch = mockFetch; + + await api.getGroup({ id: groupId }); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-abc', + }), + }) + ); + }); + + it('should work without authentication when not configured', async () => { + const groupId = 'group-no-auth'; + const expectedGroup: Group = { + id: groupId, + name: 'test-group', + }; + + mockFetch = createMockFetch(expectedGroup); + global.fetch = mockFetch; + + await api.getGroup({ id: groupId }); + + const callArgs = mockFetch.mock.calls[0][1] as RequestInit; + const headers = callArgs.headers as Record; + expect(headers.Authorization).toBeUndefined(); + }); + }); + + describe('error handling', () => { + it('should handle 400 Bad Request errors', async () => { + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Bad Request' }, 400)); + global.fetch = mockFetch; + + await expect(api.addGroup({ group: { name: '' } })).rejects.toThrow(); + }); + + it('should handle 401 Unauthorized errors', async () => { + const groupId = 'group-123'; + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Unauthorized' }, 401)); + global.fetch = mockFetch; + + await expect(api.getGroup({ id: groupId })).rejects.toThrow(); + }); + + it('should handle 403 Forbidden errors', async () => { + const groupId = 'group-123'; + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Forbidden' }, 403)); + global.fetch = mockFetch; + + await expect(api.updateGroup({ id: groupId, group: {} })).rejects.toThrow(); + }); + + it('should handle 404 Not Found errors', async () => { + const groupId = 'non-existent'; + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Not Found' }, 404)); + global.fetch = mockFetch; + + await expect(api.getGroup({ id: groupId })).rejects.toThrow(); + }); + + it('should handle 500 Internal Server Error', async () => { + mockFetch = jest + .fn() + .mockResolvedValue(createMockResponse({ error: 'Internal Server Error' }, 500)); + global.fetch = mockFetch; + + await expect(api.getGroupList({})).rejects.toThrow(); + }); + + it('should handle network errors', async () => { + mockFetch = jest.fn().mockRejectedValue(new Error('Network error')); + global.fetch = mockFetch; + + await expect(api.getGroupList({})).rejects.toThrow(); + }); + + it('should handle malformed JSON responses', async () => { + mockFetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => { + throw new Error('Invalid JSON'); + }, + }); + global.fetch = mockFetch; + + await expect(api.getGroupList({})).rejects.toThrow(); + }); + }); +}); diff --git a/src/apis/__tests__/HealthApi.test.ts b/src/apis/__tests__/HealthApi.test.ts new file mode 100644 index 0000000..347375f --- /dev/null +++ b/src/apis/__tests__/HealthApi.test.ts @@ -0,0 +1,263 @@ +import { HealthApi } from '../HealthApi'; +import type { Health, HealthInfo } from '../../models'; +import { Configuration } from '../../runtime'; +import { createMockFetch, createMockResponse } from '../../__tests__/test-utils'; + +describe('HealthApi', () => { + let api: HealthApi; + let mockFetch: jest.Mock; + + beforeEach(() => { + const config = new Configuration({ + basePath: 'http://localhost/api', + }); + api = new HealthApi(config); + }); + + describe('getHealth', () => { + it('should fetch general health report', async () => { + const healthResponse: Health = { + status: 'healthy', + message: 'All systems operational', + }; + + mockFetch = createMockFetch(healthResponse); + global.fetch = mockFetch; + + const result = await api.getHealth(); + + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost/api/health', + expect.objectContaining({ + method: 'GET', + }) + ); + expect(result.status).toBe('healthy'); + expect(result.message).toBe('All systems operational'); + }); + + it('should handle unhealthy status', async () => { + const healthResponse: Health = { + status: 'unhealthy', + message: 'Service degraded', + }; + + mockFetch = createMockFetch(healthResponse); + global.fetch = mockFetch; + + const result = await api.getHealth(); + + expect(result.status).toBe('unhealthy'); + }); + + it('should handle health check errors', async () => { + mockFetch = jest + .fn() + .mockResolvedValue(createMockResponse({ error: 'Service Unavailable' }, 503)); + global.fetch = mockFetch; + + await expect(api.getHealth()).rejects.toThrow(); + }); + + it('should work without authentication', async () => { + const healthResponse: Health = { + status: 'healthy', + }; + + mockFetch = createMockFetch(healthResponse); + global.fetch = mockFetch; + + await api.getHealth(); + + const callArgs = mockFetch.mock.calls[0][1] as RequestInit; + const headers = callArgs.headers as Record; + expect(headers.Authorization).toBeUndefined(); + }); + }); + + describe('getDatabaseHealth', () => { + it('should fetch database health report', async () => { + const healthResponse: Health = { + status: 'healthy', + message: 'Database connection OK', + }; + + mockFetch = createMockFetch(healthResponse); + global.fetch = mockFetch; + + const result = await api.getDatabaseHealth(); + + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost/api/health/database', + expect.objectContaining({ + method: 'GET', + }) + ); + expect(result.status).toBe('healthy'); + }); + + it('should handle database connection issues', async () => { + const healthResponse: Health = { + status: 'unhealthy', + message: 'Database connection failed', + }; + + mockFetch = createMockFetch(healthResponse, 500); + global.fetch = mockFetch; + + await expect(api.getDatabaseHealth()).rejects.toThrow(); + }); + + it('should include authentication when configured', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-789', + }); + api = new HealthApi(config); + + const healthResponse: Health = { + status: 'healthy', + }; + + mockFetch = createMockFetch(healthResponse); + global.fetch = mockFetch; + + await api.getDatabaseHealth(); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-789', + }), + }) + ); + }); + + it('should handle timeout errors', async () => { + mockFetch = jest.fn().mockRejectedValue(new Error('Request timeout')); + global.fetch = mockFetch; + + await expect(api.getDatabaseHealth()).rejects.toThrow(); + }); + }); + + describe('getHealthInfo', () => { + it('should fetch server information', async () => { + const healthInfoResponse = { + frontend: 'http://localhost:3000', + backend: 'http://localhost:8080/api', + api_ui: 'http://localhost:8080/api/ui', + }; + + mockFetch = createMockFetch(healthInfoResponse); + global.fetch = mockFetch; + + const result = await api.getHealthInfo(); + + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost/api/health/info', + expect.objectContaining({ + method: 'GET', + }) + ); + expect(result.frontend).toBe('http://localhost:3000'); + expect(result.backend).toBe('http://localhost:8080/api'); + expect(result.apiUi).toBe('http://localhost:8080/api/ui'); + }); + + it('should handle minimal health info', async () => { + const healthInfoResponse: HealthInfo = { + frontend: 'http://localhost:3000', + }; + + mockFetch = createMockFetch(healthInfoResponse); + global.fetch = mockFetch; + + const result = await api.getHealthInfo(); + + expect(result.frontend).toBe('http://localhost:3000'); + }); + + it('should include authentication when configured', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-info', + }); + api = new HealthApi(config); + + const healthInfoResponse = { + backend: 'http://localhost:8080/api', + }; + + mockFetch = createMockFetch(healthInfoResponse); + global.fetch = mockFetch; + + await api.getHealthInfo(); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-info', + }), + }) + ); + }); + + it('should handle health info errors', async () => { + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Forbidden' }, 403)); + global.fetch = mockFetch; + + await expect(api.getHealthInfo()).rejects.toThrow(); + }); + }); + + describe('error handling', () => { + it('should handle 500 Internal Server Error', async () => { + mockFetch = jest + .fn() + .mockResolvedValue(createMockResponse({ error: 'Internal Server Error' }, 500)); + global.fetch = mockFetch; + + await expect(api.getHealth()).rejects.toThrow(); + }); + + it('should handle 503 Service Unavailable', async () => { + mockFetch = jest + .fn() + .mockResolvedValue(createMockResponse({ error: 'Service Unavailable' }, 503)); + global.fetch = mockFetch; + + await expect(api.getHealth()).rejects.toThrow(); + }); + + it('should handle network errors', async () => { + mockFetch = jest.fn().mockRejectedValue(new Error('Network connection failed')); + global.fetch = mockFetch; + + await expect(api.getHealth()).rejects.toThrow(); + }); + + it('should handle malformed JSON responses', async () => { + mockFetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => { + throw new Error('Invalid JSON'); + }, + }); + global.fetch = mockFetch; + + await expect(api.getHealth()).rejects.toThrow(); + }); + }); +}); diff --git a/src/apis/__tests__/LoginApi.test.ts b/src/apis/__tests__/LoginApi.test.ts new file mode 100644 index 0000000..4faa382 --- /dev/null +++ b/src/apis/__tests__/LoginApi.test.ts @@ -0,0 +1,429 @@ +import { LoginApi } from '../LoginApi'; +import type { + LoginToken, + Credentials, + AccountRecovery, + AccountRegistration, + AccountReset, + LoginSupport, +} from '../../models'; +import { Configuration } from '../../runtime'; +import { createMockFetch, createMockResponse } from '../../__tests__/test-utils'; + +describe('LoginApi', () => { + let api: LoginApi; + let mockFetch: jest.Mock; + + beforeEach(() => { + const config = new Configuration({ + basePath: 'http://localhost/api', + }); + api = new LoginApi(config); + }); + + describe('login', () => { + it('should login with credentials and return token', async () => { + const credentials: Credentials = { + email: 'testuser@example.com', + password: 'testpass123', + }; + + const loginResponse: LoginToken = { + token: 'jwt-token-abc123', + }; + + mockFetch = createMockFetch(loginResponse); + global.fetch = mockFetch; + + const result = await api.login({ credentials }); + + expect(mockFetch).toHaveBeenCalledTimes(1); + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost/api/login', + expect.objectContaining>({ + method: 'POST', + headers: expect.objectContaining>({ + 'Content-Type': 'application/json', + }), + }) + ); + expect(result.token).toBe('jwt-token-abc123'); + }); + + it('should handle invalid credentials', async () => { + const credentials: Credentials = { + email: 'baduser@example.com', + password: 'wrongpass', + }; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Unauthorized' }, 401)); + global.fetch = mockFetch; + + await expect(api.login({ credentials })).rejects.toThrow(); + }); + + it('should handle invalid credentials', async () => { + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Bad Request' }, 400)); + global.fetch = mockFetch; + + await expect( + api.login({ credentials: { email: 'invalid@example.com', password: 'wrong' } }) + ).rejects.toThrow(); + }); + }); + + describe('register', () => { + it('should register a new account', async () => { + const registration: AccountRegistration = { + email: 'newuser@example.com', + password: 'newpass123', + }; + + mockFetch = jest.fn().mockResolvedValue({ + ok: true, + status: 201, + }); + global.fetch = mockFetch; + + await api.register({ accountRegistration: registration }); + + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost/api/login/register', + expect.objectContaining({ + method: 'POST', + }) + ); + }); + + it('should handle duplicate email errors', async () => { + const registration: AccountRegistration = { + email: 'existing@example.com', + password: 'pass123', + }; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Conflict' }, 409)); + global.fetch = mockFetch; + + await expect(api.register({ accountRegistration: registration })).rejects.toThrow(); + }); + + it('should handle validation errors', async () => { + const registration: AccountRegistration = { + email: 'invalid-email', + password: 'short', + }; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Bad Request' }, 400)); + global.fetch = mockFetch; + + await expect(api.register({ accountRegistration: registration })).rejects.toThrow(); + }); + }); + + describe('activate', () => { + it('should activate an account with valid code', async () => { + const activationCode = 'ABC123XYZ789'; + + mockFetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + }); + global.fetch = mockFetch; + + await api.activate({ activationCode }); + + expect(mockFetch).toHaveBeenCalledWith( + `http://localhost/api/login/activate/${activationCode}`, + expect.objectContaining({ + method: 'GET', + }) + ); + }); + + it('should handle invalid activation code', async () => { + const activationCode = 'INVALID'; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Bad Request' }, 400)); + global.fetch = mockFetch; + + await expect(api.activate({ activationCode })).rejects.toThrow(); + }); + + it('should require activationCode parameter', async () => { + await expect(api.activate({ activationCode: null as unknown as string })).rejects.toThrow(); + }); + }); + + describe('recover', () => { + it('should initiate password recovery', async () => { + const recovery: AccountRecovery = { + email: 'user@example.com', + }; + + mockFetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + }); + global.fetch = mockFetch; + + await api.recover({ accountRecovery: recovery }); + + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost/api/login/recover', + expect.objectContaining({ + method: 'POST', + }) + ); + }); + + it('should handle non-existent email', async () => { + const recovery: AccountRecovery = { + email: 'nonexistent@example.com', + }; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Not Found' }, 404)); + global.fetch = mockFetch; + + await expect(api.recover({ accountRecovery: recovery })).rejects.toThrow(); + }); + + it('should handle invalid email format', async () => { + const recovery: AccountRecovery = { + email: 'invalid-email', + }; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Bad Request' }, 400)); + global.fetch = mockFetch; + + await expect(api.recover({ accountRecovery: recovery })).rejects.toThrow(); + }); + }); + + describe('resetPassword', () => { + it('should reset password with valid activation code', async () => { + const reset: AccountReset = { + activationCode: 'reset-code-123', + password: 'newpassword123', + }; + + mockFetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + }); + global.fetch = mockFetch; + + await api.resetPassword({ accountReset: reset }); + + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost/api/login/reset-password', + expect.objectContaining({ + method: 'POST', + }) + ); + }); + + it('should handle invalid reset activation code', async () => { + const reset: AccountReset = { + activationCode: 'invalid-code', + password: 'newpass123', + }; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Forbidden' }, 403)); + global.fetch = mockFetch; + + await expect(api.resetPassword({ accountReset: reset })).rejects.toThrow(); + }); + + it('should handle weak password', async () => { + const reset: AccountReset = { + activationCode: 'valid-code', + password: '123', + }; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Bad Request' }, 400)); + global.fetch = mockFetch; + + await expect(api.resetPassword({ accountReset: reset })).rejects.toThrow(); + }); + }); + + describe('auth', () => { + it('should initiate OAuth authentication', async () => { + const provider = 'github'; + const authResponse = { + url: 'https://github.com/login/oauth/authorize', + }; + + mockFetch = createMockFetch(authResponse); + global.fetch = mockFetch; + + await api.auth({ provider }); + + expect(mockFetch).toHaveBeenCalledWith( + `http://localhost/api/login/auth/${provider}`, + expect.objectContaining({ + method: 'GET', + }) + ); + }); + + it('should handle unsupported provider', async () => { + const provider = 'unsupported'; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Not Found' }, 404)); + global.fetch = mockFetch; + + await expect(api.auth({ provider })).rejects.toThrow(); + }); + + it('should require provider parameter', async () => { + await expect(api.auth({ provider: null as unknown as string })).rejects.toThrow(); + }); + }); + + describe('config', () => { + it('should get login configuration for provider', async () => { + const provider = 'oidc'; + const configResponse = { + client_id: 'client-123', + redirect_uri: 'http://localhost:3000/callback', + scope: 'openid profile email', + }; + + mockFetch = createMockFetch(configResponse); + global.fetch = mockFetch; + + const result = await api.config({ provider }); + + expect(mockFetch).toHaveBeenCalledWith( + `http://localhost/api/login/config/${provider}`, + expect.objectContaining({ + method: 'GET', + }) + ); + expect(result.clientId).toBe('client-123'); + expect(result.redirectUri).toBe('http://localhost:3000/callback'); + }); + + it('should handle unsupported provider config', async () => { + const provider = 'unsupported'; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Not Found' }, 404)); + global.fetch = mockFetch; + + await expect(api.config({ provider })).rejects.toThrow(); + }); + + it('should require provider parameter', async () => { + await expect(api.config({ provider: null as unknown as string })).rejects.toThrow(); + }); + }); + + describe('support', () => { + it('should get login support information', async () => { + const supportResponse: LoginSupport = { + user: true, + github: true, + google: true, + keycloak: false, + }; + + mockFetch = createMockFetch(supportResponse); + global.fetch = mockFetch; + + const result = await api.support(); + + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost/api/login/support', + expect.objectContaining({ + method: 'GET', + }) + ); + expect(result.user).toBe(true); + expect(result.github).toBe(true); + }); + + it('should handle minimal support info', async () => { + const supportResponse: LoginSupport = { + user: false, + keycloak: false, + }; + + mockFetch = createMockFetch(supportResponse); + global.fetch = mockFetch; + + const result = await api.support(); + + expect(result.user).toBe(false); + }); + + it('should handle support endpoint errors', async () => { + mockFetch = jest + .fn() + .mockResolvedValue(createMockResponse({ error: 'Service Unavailable' }, 503)); + global.fetch = mockFetch; + + await expect(api.support()).rejects.toThrow(); + }); + }); + + describe('error handling', () => { + it('should handle 400 Bad Request errors', async () => { + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Bad Request' }, 400)); + global.fetch = mockFetch; + + await expect( + api.login({ credentials: { email: 'invalid@example.com', password: 'bad' } }) + ).rejects.toThrow(); + }); + + it('should handle 401 Unauthorized errors', async () => { + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Unauthorized' }, 401)); + global.fetch = mockFetch; + + await expect( + api.login({ credentials: { email: 'user@example.com', password: 'pass' } }) + ).rejects.toThrow(); + }); + + it('should handle 403 Forbidden errors', async () => { + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Forbidden' }, 403)); + global.fetch = mockFetch; + + await expect( + api.resetPassword({ accountReset: { activationCode: 'invalid', password: 'pass' } }) + ).rejects.toThrow(); + }); + + it('should handle 500 Internal Server Error', async () => { + mockFetch = jest + .fn() + .mockResolvedValue(createMockResponse({ error: 'Internal Server Error' }, 500)); + global.fetch = mockFetch; + + await expect(api.support()).rejects.toThrow(); + }); + + it('should handle network errors', async () => { + mockFetch = jest.fn().mockRejectedValue(new Error('Network error')); + global.fetch = mockFetch; + + await expect(api.support()).rejects.toThrow(); + }); + + it('should handle rate limiting', async () => { + mockFetch = jest + .fn() + .mockResolvedValue(createMockResponse({ error: 'Too Many Requests' }, 429)); + global.fetch = mockFetch; + + await expect( + api.login({ credentials: { email: 'user@example.com', password: 'pass' } }) + ).rejects.toThrow(); + }); + }); +}); diff --git a/src/apis/__tests__/ProjectApi.test.ts b/src/apis/__tests__/ProjectApi.test.ts index c3df418..955cba4 100644 --- a/src/apis/__tests__/ProjectApi.test.ts +++ b/src/apis/__tests__/ProjectApi.test.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ - import { ProjectApi } from '../ProjectApi'; import type { Project, ProjectList } from '../../models'; import { Configuration } from '../../runtime'; diff --git a/src/apis/__tests__/ResultApi.test.ts b/src/apis/__tests__/ResultApi.test.ts index 07e6ba8..6003692 100644 --- a/src/apis/__tests__/ResultApi.test.ts +++ b/src/apis/__tests__/ResultApi.test.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ - import { ResultApi } from '../ResultApi'; import { type Result, type ResultList, ResultResultEnum } from '../../models'; import { Configuration } from '../../runtime'; diff --git a/src/apis/__tests__/RunApi.test.ts b/src/apis/__tests__/RunApi.test.ts new file mode 100644 index 0000000..c0f88bc --- /dev/null +++ b/src/apis/__tests__/RunApi.test.ts @@ -0,0 +1,523 @@ +import { RunApi } from '../RunApi'; +import type { Run, RunList, UpdateRun } from '../../models'; +import { Configuration } from '../../runtime'; +import { createMockFetch, createMockResponse } from '../../__tests__/test-utils'; + +describe('RunApi', () => { + let api: RunApi; + let mockFetch: jest.Mock; + + beforeEach(() => { + const config = new Configuration({ + basePath: 'http://localhost/api', + }); + api = new RunApi(config); + }); + + describe('addRun', () => { + it('should create a new run', async () => { + const newRun: Run = { + metadata: { + project: 'test-project', + component: 'test-component', + }, + summary: { + passes: 0, + failures: 0, + skips: 0, + errors: 0, + xfailures: 0, + xpasses: 0, + tests: 0, + }, + }; + + const responseRun: Run = { + id: '123e4567-e89b-12d3-a456-426614174000', + ...newRun, + created: '2024-01-01T00:00:00.000Z', + }; + + mockFetch = createMockFetch(responseRun, 201); + global.fetch = mockFetch; + + const result = await api.addRun({ run: newRun }); + + expect(mockFetch).toHaveBeenCalledTimes(1); + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost/api/run', + expect.objectContaining>({ + method: 'POST', + headers: expect.objectContaining>({ + 'Content-Type': 'application/json', + }), + }) + ); + expect(result.id).toBe('123e4567-e89b-12d3-a456-426614174000'); + expect((result.metadata as Record).project).toBe('test-project'); + }); + + it('should send run data with correct JSON format (snake_case)', async () => { + const newRun: Run = { + metadata: { project: 'test' }, + startTime: '2024-01-01T00:00:00.000Z', + }; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse(newRun, 201)); + global.fetch = mockFetch; + + await api.addRun({ run: newRun }); + + const callArgs = mockFetch.mock.calls[0]; + const requestBody = JSON.parse((callArgs[1] as RequestInit).body as string); + + expect(requestBody).toHaveProperty('start_time'); + expect(requestBody.start_time).toBe('2024-01-01T00:00:00.000Z'); + }); + + it('should handle errors when creating a run', async () => { + const newRun: Run = { metadata: { project: 'test' } }; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Bad Request' }, 400)); + global.fetch = mockFetch; + + await expect(api.addRun({ run: newRun })).rejects.toThrow(); + }); + }); + + describe('getRun', () => { + it('should fetch a run by ID', async () => { + const runId = '123e4567-e89b-12d3-a456-426614174000'; + const expectedRun: Run = { + id: runId, + metadata: { project: 'test-project' }, + summary: { + passes: 10, + failures: 2, + skips: 1, + errors: 0, + xfailures: 0, + xpasses: 0, + tests: 13, + }, + }; + + mockFetch = createMockFetch(expectedRun); + global.fetch = mockFetch; + + const result = await api.getRun({ id: runId }); + + expect(mockFetch).toHaveBeenCalledWith( + `http://localhost/api/run/${runId}`, + expect.objectContaining({ + method: 'GET', + }) + ); + expect(result.id).toBe(runId); + expect((result.summary as Record).tests).toBe(13); + }); + + it('should handle 404 when run not found', async () => { + const runId = '123e4567-e89b-12d3-a456-426614174000'; + + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Not Found' }, 404)); + global.fetch = mockFetch; + + await expect(api.getRun({ id: runId })).rejects.toThrow(); + }); + + it('should require id parameter', async () => { + await expect(api.getRun({ id: null as unknown as string })).rejects.toThrow(); + }); + }); + + describe('getRunList', () => { + it('should fetch a list of runs', async () => { + const mockRunList: RunList = { + runs: [ + { + id: '123e4567-e89b-12d3-a456-426614174000', + metadata: { project: 'project1' }, + }, + { + id: '123e4567-e89b-12d3-a456-426614174001', + metadata: { project: 'project2' }, + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 2, + totalPages: 1, + }, + }; + + mockFetch = createMockFetch(mockRunList); + global.fetch = mockFetch; + + const result = await api.getRunList({}); + + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost/api/run', + expect.objectContaining({ + method: 'GET', + }) + ); + expect(result.runs).toHaveLength(2); + }); + + it('should handle pagination parameters', async () => { + const mockRunList: RunList = { + runs: [], + pagination: { + page: 2, + pageSize: 10, + totalItems: 0, + totalPages: 0, + }, + }; + + mockFetch = createMockFetch(mockRunList); + global.fetch = mockFetch; + + await api.getRunList({ page: 2, pageSize: 10 }); + + const url = mockFetch.mock.calls[0][0] as string; + expect(url).toContain('page=2'); + expect(url).toContain('pageSize=10'); + }); + + it('should handle filter parameters', async () => { + const mockRunList: RunList = { + runs: [], + pagination: { + page: 1, + pageSize: 25, + totalItems: 0, + totalPages: 0, + }, + }; + + mockFetch = createMockFetch(mockRunList); + global.fetch = mockFetch; + + await api.getRunList({ filter: ['metadata.project=test-project', 'summary.failures>0'] }); + + const url = mockFetch.mock.calls[0][0] as string; + expect(url).toContain('filter='); + }); + + it('should handle estimate parameter', async () => { + const mockRunList: RunList = { + runs: [], + pagination: { + page: 1, + pageSize: 25, + totalItems: 1000, + totalPages: 40, + }, + }; + + mockFetch = createMockFetch(mockRunList); + global.fetch = mockFetch; + + await api.getRunList({ estimate: true }); + + const url = mockFetch.mock.calls[0][0] as string; + expect(url).toContain('estimate=true'); + }); + + it('should return empty list when no runs exist', async () => { + const mockRunList: RunList = { + runs: [], + pagination: { + page: 1, + pageSize: 25, + totalItems: 0, + totalPages: 0, + }, + }; + + mockFetch = createMockFetch(mockRunList); + global.fetch = mockFetch; + + const result = await api.getRunList({}); + + expect(result.runs).toHaveLength(0); + }); + }); + + describe('updateRun', () => { + it('should update an existing run', async () => { + const runId = '123e4567-e89b-12d3-a456-426614174000'; + const updatedRun: Run = { + id: runId, + metadata: { + project: 'updated-project', + env: 'production', + }, + summary: { + passes: 15, + failures: 1, + skips: 0, + errors: 0, + xfailures: 0, + xpasses: 0, + tests: 16, + }, + }; + + mockFetch = createMockFetch(updatedRun); + global.fetch = mockFetch; + + const result = await api.updateRun({ id: runId, run: updatedRun }); + + expect(mockFetch).toHaveBeenCalledWith( + `http://localhost/api/run/${runId}`, + expect.objectContaining({ + method: 'PUT', + }) + ); + expect((result.metadata as Record).project).toBe('updated-project'); + }); + + it('should handle partial updates', async () => { + const runId = '123e4567-e89b-12d3-a456-426614174000'; + const partialUpdate: Run = { + id: runId, + metadata: { env: 'staging' }, + }; + + mockFetch = createMockFetch(partialUpdate); + global.fetch = mockFetch; + + const result = await api.updateRun({ id: runId, run: partialUpdate }); + + expect((result.metadata as Record).env).toBe('staging'); + }); + + it('should require id parameter', async () => { + const run: Run = { metadata: { project: 'test' } }; + + await expect(api.updateRun({ id: null as unknown as string, run })).rejects.toThrow(); + }); + }); + + describe('bulkUpdate', () => { + it('should update multiple runs', async () => { + const updateData: UpdateRun = { + metadata: { + tagged: 'bulk-update', + }, + }; + + const mockRunList: RunList = { + runs: [ + { + id: '123e4567-e89b-12d3-a456-426614174000', + metadata: { project: 'test', tagged: 'bulk-update' }, + }, + { + id: '123e4567-e89b-12d3-a456-426614174001', + metadata: { project: 'test', tagged: 'bulk-update' }, + }, + ], + pagination: { + page: 1, + pageSize: 25, + totalItems: 2, + totalPages: 1, + }, + }; + + mockFetch = createMockFetch(mockRunList); + global.fetch = mockFetch; + + const result = await api.bulkUpdate({ + filter: ['metadata.project=test'], + updateRun: updateData, + }); + + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining('/runs/bulk-update'), + expect.objectContaining({ + method: 'POST', + }) + ); + expect(result.runs).toHaveLength(2); + }); + + it('should handle pageSize parameter', async () => { + const updateData: UpdateRun = { + metadata: { status: 'archived' }, + }; + + const mockRunList: RunList = { + runs: [], + pagination: { page: 1, pageSize: 100, totalItems: 0, totalPages: 0 }, + }; + + mockFetch = createMockFetch(mockRunList); + global.fetch = mockFetch; + + await api.bulkUpdate({ + filter: ['metadata.project=test'], + pageSize: 100, + updateRun: updateData, + }); + + const url = mockFetch.mock.calls[0][0] as string; + expect(url).toContain('pageSize=100'); + }); + + it('should handle empty filter', async () => { + const updateData: UpdateRun = { + metadata: { global: 'true' }, + }; + + const mockRunList: RunList = { + runs: [], + pagination: { page: 1, pageSize: 25, totalItems: 0, totalPages: 0 }, + }; + + mockFetch = createMockFetch(mockRunList); + global.fetch = mockFetch; + + const result = await api.bulkUpdate({ updateRun: updateData }); + + expect(result.runs).toHaveLength(0); + }); + }); + + describe('authentication', () => { + it('should include Bearer token when configured', async () => { + const config = new Configuration({ + basePath: 'http://localhost/api', + accessToken: async () => 'test-token-123', + }); + api = new RunApi(config); + + const runId = '123e4567-e89b-12d3-a456-426614174000'; + const expectedRun: Run = { + id: runId, + metadata: { project: 'test' }, + }; + + mockFetch = createMockFetch(expectedRun); + global.fetch = mockFetch; + + await api.getRun({ id: runId }); + + interface FetchOptions { + method: string; + headers: Record; + } + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining>({ + headers: expect.objectContaining>({ + Authorization: 'Bearer test-token-123', + }), + }) + ); + }); + + it('should work without authentication when not configured', async () => { + const runId = '123e4567-e89b-12d3-a456-426614174000'; + const expectedRun: Run = { + id: runId, + metadata: { project: 'test' }, + }; + + mockFetch = createMockFetch(expectedRun); + global.fetch = mockFetch; + + await api.getRun({ id: runId }); + + const callArgs = mockFetch.mock.calls[0][1] as RequestInit; + const headers = callArgs.headers as Record; + expect(headers.Authorization).toBeUndefined(); + }); + }); + + describe('error handling', () => { + it('should handle 400 Bad Request errors', async () => { + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Bad Request' }, 400)); + global.fetch = mockFetch; + + await expect(api.addRun({ run: {} })).rejects.toThrow(); + }); + + it('should handle 401 Unauthorized errors', async () => { + const runId = '123e4567-e89b-12d3-a456-426614174000'; + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Unauthorized' }, 401)); + global.fetch = mockFetch; + + await expect(api.getRun({ id: runId })).rejects.toThrow(); + }); + + it('should handle 403 Forbidden errors', async () => { + const runId = '123e4567-e89b-12d3-a456-426614174000'; + mockFetch = jest.fn().mockResolvedValue(createMockResponse({ error: 'Forbidden' }, 403)); + global.fetch = mockFetch; + + await expect(api.updateRun({ id: runId, run: {} })).rejects.toThrow(); + }); + + it('should handle 500 Internal Server Error', async () => { + mockFetch = jest + .fn() + .mockResolvedValue(createMockResponse({ error: 'Internal Server Error' }, 500)); + global.fetch = mockFetch; + + await expect(api.getRunList({})).rejects.toThrow(); + }); + + it('should handle 503 Service Unavailable', async () => { + mockFetch = jest + .fn() + .mockResolvedValue(createMockResponse({ error: 'Service Unavailable' }, 503)); + global.fetch = mockFetch; + + await expect(api.getRunList({})).rejects.toThrow(); + }); + + it('should handle network errors', async () => { + mockFetch = jest.fn().mockRejectedValue(new Error('Network error')); + global.fetch = mockFetch; + + await expect(api.getRunList({})).rejects.toThrow(); + }); + + it('should handle malformed JSON responses', async () => { + mockFetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => { + throw new Error('Invalid JSON'); + }, + }); + global.fetch = mockFetch; + + await expect(api.getRunList({})).rejects.toThrow(); + }); + + it('should handle empty response body for non-2xx status', async () => { + mockFetch = jest.fn().mockResolvedValue({ + ok: false, + status: 404, + statusText: 'Not Found', + json: async () => ({}), + text: async () => '', + }); + global.fetch = mockFetch; + + const runId = '123e4567-e89b-12d3-a456-426614174000'; + await expect(api.getRun({ id: runId })).rejects.toThrow(); + }); + }); +}); diff --git a/src/models/__tests__/Artifact.test.ts b/src/models/__tests__/Artifact.test.ts new file mode 100644 index 0000000..211c7b7 --- /dev/null +++ b/src/models/__tests__/Artifact.test.ts @@ -0,0 +1,151 @@ +import { type Artifact, ArtifactFromJSON, ArtifactToJSON, instanceOfArtifact } from '../Artifact'; + +describe('Artifact Model', () => { + describe('interface and types', () => { + it('should create a valid Artifact object with all fields', () => { + const artifact: Artifact = { + id: '123e4567-e89b-12d3-a456-426614174000', + filename: 'test-artifact.log', + uploadDate: '2024-01-01T00:00:00.000Z', + resultId: '123e4567-e89b-12d3-a456-426614174001', + runId: '123e4567-e89b-12d3-a456-426614174002', + }; + + expect(artifact.id).toBe('123e4567-e89b-12d3-a456-426614174000'); + expect(artifact.filename).toBe('test-artifact.log'); + expect(artifact.uploadDate).toBe('2024-01-01T00:00:00.000Z'); + }); + + it('should create an Artifact object with minimal fields', () => { + const artifact: Artifact = { + filename: 'test.txt', + }; + + expect(artifact.filename).toBe('test.txt'); + expect(artifact.id).toBeUndefined(); + }); + + it('should allow undefined values for optional fields', () => { + const artifact: Artifact = { + id: undefined, + filename: 'test.log', + uploadDate: undefined, + }; + + expect(artifact.id).toBeUndefined(); + expect(artifact.uploadDate).toBeUndefined(); + }); + }); + + describe('ArtifactFromJSON', () => { + it('should convert JSON with snake_case to Artifact object with camelCase', () => { + const json = { + id: '123e4567-e89b-12d3-a456-426614174000', + filename: 'artifact.png', + upload_date: '2024-01-01T00:00:00.000Z', + result_id: '123e4567-e89b-12d3-a456-426614174001', + run_id: '123e4567-e89b-12d3-a456-426614174002', + }; + + const artifact = ArtifactFromJSON(json); + + expect(artifact.id).toBe('123e4567-e89b-12d3-a456-426614174000'); + expect(artifact.filename).toBe('artifact.png'); + expect(artifact.uploadDate).toBe('2024-01-01T00:00:00.000Z'); + expect(artifact.resultId).toBe('123e4567-e89b-12d3-a456-426614174001'); + expect(artifact.runId).toBe('123e4567-e89b-12d3-a456-426614174002'); + }); + + it('should handle null values correctly', () => { + const json = { + filename: 'test.txt', + upload_date: null, + }; + + const artifact = ArtifactFromJSON(json); + + expect(artifact.uploadDate).toBeUndefined(); + }); + + it('should handle missing fields as undefined', () => { + const json = { + filename: 'test.log', + }; + + const artifact = ArtifactFromJSON(json); + + expect(artifact.filename).toBe('test.log'); + expect(artifact.id).toBeUndefined(); + expect(artifact.uploadDate).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = ArtifactFromJSON(null); + expect(result).toBeNull(); + }); + }); + + describe('ArtifactToJSON', () => { + it('should convert Artifact object with camelCase to JSON with snake_case', () => { + const artifact: Artifact = { + id: '123e4567-e89b-12d3-a456-426614174000', + filename: 'test.log', + uploadDate: '2024-01-01T00:00:00.000Z', + resultId: '123e4567-e89b-12d3-a456-426614174001', + runId: '123e4567-e89b-12d3-a456-426614174002', + }; + + const json = ArtifactToJSON(artifact); + + expect(json.id).toBe('123e4567-e89b-12d3-a456-426614174000'); + expect(json.filename).toBe('test.log'); + expect(json.upload_date).toBe('2024-01-01T00:00:00.000Z'); + expect(json.result_id).toBe('123e4567-e89b-12d3-a456-426614174001'); + expect(json.run_id).toBe('123e4567-e89b-12d3-a456-426614174002'); + }); + + it('should handle undefined fields', () => { + const artifact: Artifact = { + filename: 'test.txt', + }; + + const json = ArtifactToJSON(artifact); + + expect(json.filename).toBe('test.txt'); + expect(json.id).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = ArtifactToJSON(null); + expect(result).toBeNull(); + }); + + it('should return undefined when passed undefined', () => { + const result = ArtifactToJSON(undefined); + expect(result).toBeUndefined(); + }); + }); + + describe('instanceOfArtifact', () => { + it('should return true for any object (as per implementation)', () => { + const result = instanceOfArtifact({}); + expect(result).toBe(true); + }); + }); + + describe('JSON round-trip', () => { + it('should maintain data integrity through JSON conversion round-trip', () => { + const original: Artifact = { + id: '123e4567-e89b-12d3-a456-426614174000', + filename: 'screenshot.png', + uploadDate: '2024-01-01T00:00:00.000Z', + resultId: '123e4567-e89b-12d3-a456-426614174001', + }; + + const json = ArtifactToJSON(original); + const restored = ArtifactFromJSON(json); + + expect(restored).toEqual(original); + }); + }); +}); diff --git a/src/models/__tests__/Dashboard.test.ts b/src/models/__tests__/Dashboard.test.ts new file mode 100644 index 0000000..73c93d6 --- /dev/null +++ b/src/models/__tests__/Dashboard.test.ts @@ -0,0 +1,152 @@ +import { + type Dashboard, + DashboardFromJSON, + DashboardToJSON, + instanceOfDashboard, +} from '../Dashboard'; + +describe('Dashboard Model', () => { + describe('interface and types', () => { + it('should create a valid Dashboard object with all fields', () => { + const dashboard: Dashboard = { + id: 'dashboard-123', + title: 'Test Dashboard', + description: 'A test dashboard for testing', + filters: 'status=active', + projectId: 'project-456', + userId: 'user-789', + }; + + expect(dashboard.id).toBe('dashboard-123'); + expect(dashboard.title).toBe('Test Dashboard'); + expect(dashboard.description).toBe('A test dashboard for testing'); + }); + + it('should create a Dashboard object with minimal fields', () => { + const dashboard: Dashboard = { + title: 'Simple Dashboard', + }; + + expect(dashboard.title).toBe('Simple Dashboard'); + expect(dashboard.id).toBeUndefined(); + }); + + it('should allow undefined values for optional fields', () => { + const dashboard: Dashboard = { + id: undefined, + title: 'Test', + description: undefined, + }; + + expect(dashboard.id).toBeUndefined(); + expect(dashboard.description).toBeUndefined(); + }); + }); + + describe('DashboardFromJSON', () => { + it('should convert JSON to Dashboard object', () => { + const json = { + id: 'dashboard-456', + title: 'My Dashboard', + description: 'Description', + filters: 'env=prod', + }; + + const dashboard = DashboardFromJSON(json); + + expect(dashboard.id).toBe('dashboard-456'); + expect(dashboard.title).toBe('My Dashboard'); + expect(dashboard.description).toBe('Description'); + }); + + it('should handle null values correctly', () => { + const json = { + title: 'Test', + description: null, + }; + + const dashboard = DashboardFromJSON(json); + + expect(dashboard.description).toBeUndefined(); + }); + + it('should handle missing fields as undefined', () => { + const json = { + title: 'Test Dashboard', + }; + + const dashboard = DashboardFromJSON(json); + + expect(dashboard.title).toBe('Test Dashboard'); + expect(dashboard.id).toBeUndefined(); + expect(dashboard.description).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = DashboardFromJSON(null); + expect(result).toBeNull(); + }); + }); + + describe('DashboardToJSON', () => { + it('should convert Dashboard object to JSON', () => { + const dashboard: Dashboard = { + id: 'dashboard-789', + title: 'Export Dashboard', + description: 'For export', + filters: 'type=test', + }; + + const json = DashboardToJSON(dashboard); + + expect(json.id).toBe('dashboard-789'); + expect(json.title).toBe('Export Dashboard'); + expect(json.description).toBe('For export'); + }); + + it('should handle undefined fields', () => { + const dashboard: Dashboard = { + title: 'Test', + }; + + const json = DashboardToJSON(dashboard); + + expect(json.title).toBe('Test'); + expect(json.id).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = DashboardToJSON(null); + expect(result).toBeNull(); + }); + + it('should return undefined when passed undefined', () => { + const result = DashboardToJSON(undefined); + expect(result).toBeUndefined(); + }); + }); + + describe('instanceOfDashboard', () => { + it('should return true for any object (as per implementation)', () => { + const result = instanceOfDashboard({}); + expect(result).toBe(true); + }); + }); + + describe('JSON round-trip', () => { + it('should maintain data integrity through JSON conversion round-trip', () => { + const original: Dashboard = { + id: 'dashboard-999', + title: 'Round Trip Dashboard', + description: 'Test round trip', + filters: 'env=staging', + projectId: 'project-123', + }; + + const json = DashboardToJSON(original); + const restored = DashboardFromJSON(json); + + expect(restored).toEqual(original); + }); + }); +}); diff --git a/src/models/__tests__/Group.test.ts b/src/models/__tests__/Group.test.ts new file mode 100644 index 0000000..06a0efb --- /dev/null +++ b/src/models/__tests__/Group.test.ts @@ -0,0 +1,130 @@ +import { type Group, GroupFromJSON, GroupToJSON, instanceOfGroup } from '../Group'; + +describe('Group Model', () => { + describe('interface and types', () => { + it('should create a valid Group object with all fields', () => { + const group: Group = { + id: 'group-123', + name: 'engineering-team', + }; + + expect(group.id).toBe('group-123'); + expect(group.name).toBe('engineering-team'); + }); + + it('should create a Group object with minimal fields', () => { + const group: Group = { + name: 'qa-team', + }; + + expect(group.name).toBe('qa-team'); + expect(group.id).toBeUndefined(); + }); + + it('should allow undefined values for optional fields', () => { + const group: Group = { + id: undefined, + name: 'test-team', + }; + + expect(group.id).toBeUndefined(); + }); + }); + + describe('GroupFromJSON', () => { + it('should convert JSON to Group object', () => { + const json = { + id: 'group-456', + name: 'dev-team', + }; + + const group = GroupFromJSON(json); + + expect(group.id).toBe('group-456'); + expect(group.name).toBe('dev-team'); + }); + + it('should handle null values correctly', () => { + const json = { + id: null, + name: 'test', + }; + + const group = GroupFromJSON(json); + + expect(group.id).toBeUndefined(); + }); + + it('should handle missing fields as undefined', () => { + const json = { + name: 'team', + }; + + const group = GroupFromJSON(json); + + expect(group.name).toBe('team'); + expect(group.id).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = GroupFromJSON(null); + expect(result).toBeNull(); + }); + }); + + describe('GroupToJSON', () => { + it('should convert Group object to JSON', () => { + const group: Group = { + id: 'group-789', + name: 'ops-team', + }; + + const json = GroupToJSON(group); + + expect(json.id).toBe('group-789'); + expect(json.name).toBe('ops-team'); + }); + + it('should handle undefined fields', () => { + const group: Group = { + name: 'test', + }; + + const json = GroupToJSON(group); + + expect(json.name).toBe('test'); + expect(json.id).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = GroupToJSON(null); + expect(result).toBeNull(); + }); + + it('should return undefined when passed undefined', () => { + const result = GroupToJSON(undefined); + expect(result).toBeUndefined(); + }); + }); + + describe('instanceOfGroup', () => { + it('should return true for any object (as per implementation)', () => { + const result = instanceOfGroup({}); + expect(result).toBe(true); + }); + }); + + describe('JSON round-trip', () => { + it('should maintain data integrity through JSON conversion round-trip', () => { + const original: Group = { + id: 'group-999', + name: 'test-group', + }; + + const json = GroupToJSON(original); + const restored = GroupFromJSON(json); + + expect(restored).toEqual(original); + }); + }); +}); diff --git a/src/models/__tests__/Pagination.test.ts b/src/models/__tests__/Pagination.test.ts new file mode 100644 index 0000000..65d021f --- /dev/null +++ b/src/models/__tests__/Pagination.test.ts @@ -0,0 +1,174 @@ +import { + type Pagination, + PaginationFromJSON, + PaginationToJSON, + instanceOfPagination, +} from '../Pagination'; + +describe('Pagination Model', () => { + describe('interface and types', () => { + it('should create a valid Pagination object with all fields', () => { + const pagination: Pagination = { + page: 2, + pageSize: 50, + totalItems: 150, + totalPages: 3, + }; + + expect(pagination.page).toBe(2); + expect(pagination.pageSize).toBe(50); + expect(pagination.totalItems).toBe(150); + expect(pagination.totalPages).toBe(3); + }); + + it('should create a Pagination object with minimal fields', () => { + const pagination: Pagination = { + page: 1, + pageSize: 25, + }; + + expect(pagination.page).toBe(1); + expect(pagination.pageSize).toBe(25); + expect(pagination.totalItems).toBeUndefined(); + }); + + it('should allow undefined values for optional fields', () => { + const pagination: Pagination = { + page: undefined, + pageSize: undefined, + totalItems: undefined, + totalPages: undefined, + }; + + expect(pagination.page).toBeUndefined(); + expect(pagination.totalItems).toBeUndefined(); + }); + }); + + describe('PaginationFromJSON', () => { + it('should convert JSON to Pagination object', () => { + const json = { + page: 3, + pageSize: 100, + totalItems: 500, + totalPages: 5, + }; + + const pagination = PaginationFromJSON(json); + + expect(pagination.page).toBe(3); + expect(pagination.pageSize).toBe(100); + expect(pagination.totalItems).toBe(500); + expect(pagination.totalPages).toBe(5); + }); + + it('should handle null values correctly', () => { + const json = { + page: null, + page_size: null, + total_items: null, + total_pages: null, + }; + + const pagination = PaginationFromJSON(json); + + expect(pagination.page).toBeUndefined(); + expect(pagination.pageSize).toBeUndefined(); + }); + + it('should handle missing fields as undefined', () => { + const json = { + page: 1, + }; + + const pagination = PaginationFromJSON(json); + + expect(pagination.page).toBe(1); + expect(pagination.pageSize).toBeUndefined(); + expect(pagination.totalItems).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = PaginationFromJSON(null); + expect(result).toBeNull(); + }); + }); + + describe('PaginationToJSON', () => { + it('should convert Pagination object to JSON', () => { + const pagination: Pagination = { + page: 4, + pageSize: 25, + totalItems: 200, + totalPages: 8, + }; + + const json = PaginationToJSON(pagination); + + expect(json.page).toBe(4); + expect(json.pageSize).toBe(25); + expect(json.totalItems).toBe(200); + expect(json.totalPages).toBe(8); + }); + + it('should handle undefined fields', () => { + const pagination: Pagination = { + page: 1, + pageSize: 10, + }; + + const json = PaginationToJSON(pagination); + + expect(json.page).toBe(1); + expect(json.pageSize).toBe(10); + expect(json.totalItems).toBeUndefined(); + }); + + it('should return null when passed null', () => { + const result = PaginationToJSON(null); + expect(result).toBeNull(); + }); + + it('should return undefined when passed undefined', () => { + const result = PaginationToJSON(undefined); + expect(result).toBeUndefined(); + }); + }); + + describe('instanceOfPagination', () => { + it('should return true for any object (as per implementation)', () => { + const result = instanceOfPagination({}); + expect(result).toBe(true); + }); + }); + + describe('JSON round-trip', () => { + it('should maintain data integrity through JSON conversion round-trip', () => { + const original: Pagination = { + page: 5, + pageSize: 75, + totalItems: 300, + totalPages: 4, + }; + + const json = PaginationToJSON(original); + const restored = PaginationFromJSON(json); + + expect(restored).toEqual(original); + }); + + it('should handle pagination with zero items', () => { + const original: Pagination = { + page: 1, + pageSize: 25, + totalItems: 0, + totalPages: 0, + }; + + const json = PaginationToJSON(original); + const restored = PaginationFromJSON(json); + + expect(restored).toEqual(original); + }); + }); +});