diff --git a/api/src/unraid-api/rest/__test__/rest-module-dependencies.test.ts b/api/src/unraid-api/rest/__test__/rest-module-dependencies.test.ts deleted file mode 100644 index 656888bc8..000000000 --- a/api/src/unraid-api/rest/__test__/rest-module-dependencies.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Test } from '@nestjs/testing'; - -import { describe, expect, it, vi } from 'vitest'; - -import { ApiReportService } from '@app/unraid-api/cli/api-report.service.js'; -import { RestService } from '@app/unraid-api/rest/rest.service.js'; - -// Mock external dependencies -vi.mock('@app/store/index.js', () => ({ - getters: { - paths: vi.fn(() => ({ - 'log-base': '/tmp/logs', - })), - }, -})); - -vi.mock('execa', () => ({ - execa: vi.fn().mockResolvedValue({ stdout: 'mocked output' }), -})); - -describe('RestService Dependencies', () => { - it('should resolve ApiReportService dependency successfully', async () => { - const mockApiReportService = { - generateReport: vi.fn().mockResolvedValue({ timestamp: new Date().toISOString() }), - }; - - const module = await Test.createTestingModule({ - providers: [ - RestService, - { - provide: ApiReportService, - useValue: mockApiReportService, - }, - ], - }).compile(); - - const restService = module.get(RestService); - expect(restService).toBeDefined(); - expect(restService).toBeInstanceOf(RestService); - - await module.close(); - }); - - it('should fail gracefully when ApiReportService is missing', async () => { - // This test ensures we get a clear error when dependencies are missing - await expect( - Test.createTestingModule({ - providers: [RestService], - }).compile() - ).rejects.toThrow(/ApiReportService/); - }); -}); diff --git a/api/src/unraid-api/rest/__test__/rest-module.integration.test.ts b/api/src/unraid-api/rest/__test__/rest-module.integration.test.ts deleted file mode 100644 index b2a3492af..000000000 --- a/api/src/unraid-api/rest/__test__/rest-module.integration.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { CacheModule } from '@nestjs/cache-manager'; -import { Test } from '@nestjs/testing'; - -import { CANONICAL_INTERNAL_CLIENT_TOKEN } from '@unraid/shared'; -import { describe, expect, it, vi } from 'vitest'; - -import { ApiReportService } from '@app/unraid-api/cli/api-report.service.js'; -import { LogService } from '@app/unraid-api/cli/log.service.js'; -import { RestModule } from '@app/unraid-api/rest/rest.module.js'; -import { RestService } from '@app/unraid-api/rest/rest.service.js'; - -// Mock external dependencies that cause issues in tests -vi.mock('@app/store/index.js', () => ({ - store: { - getState: vi.fn(() => ({ - paths: { - 'log-base': '/tmp/logs', - 'auth-keys': '/tmp/auth-keys', - config: '/tmp/config', - }, - emhttp: {}, - dynamix: { notify: { path: '/tmp/notifications' } }, - registration: {}, - })), - subscribe: vi.fn(() => vi.fn()), // Return unsubscribe function - }, - getters: { - paths: vi.fn(() => ({ - 'log-base': '/tmp/logs', - 'auth-keys': '/tmp/auth-keys', - config: '/tmp/config', - })), - dynamix: vi.fn(() => ({ notify: { path: '/tmp/notifications' } })), - emhttp: vi.fn(() => ({})), - registration: vi.fn(() => ({})), - }, -})); - -vi.mock('@app/core/log.js', () => ({ - levels: ['trace', 'debug', 'info', 'warn', 'error', 'fatal'], - apiLogger: { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - }, - pluginLogger: { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - trace: vi.fn(), - fatal: vi.fn(), - }, -})); - -vi.mock('execa', () => ({ - execa: vi.fn().mockResolvedValue({ stdout: 'mocked output' }), -})); - -describe('RestModule Integration', () => { - it('should compile with RestService having access to ApiReportService', async () => { - const module = await Test.createTestingModule({ - imports: [CacheModule.register({ isGlobal: true }), RestModule], - }) - // Override services that have complex dependencies for testing - .overrideProvider(CANONICAL_INTERNAL_CLIENT_TOKEN) - .useValue({ getClient: vi.fn() }) - .overrideProvider(LogService) - .useValue({ error: vi.fn(), debug: vi.fn() }) - .compile(); - - const restService = module.get(RestService); - const apiReportService = module.get(ApiReportService); - - expect(restService).toBeDefined(); - expect(apiReportService).toBeDefined(); - - // Verify RestService has the injected ApiReportService - expect(restService['apiReportService']).toBeDefined(); - - await module.close(); - }, 10000); -}); diff --git a/api/src/unraid-api/rest/__test__/rest.service.test.ts b/api/src/unraid-api/rest/__test__/rest.service.test.ts deleted file mode 100644 index 86465a768..000000000 --- a/api/src/unraid-api/rest/__test__/rest.service.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Test } from '@nestjs/testing'; - -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { ApiReportService } from '@app/unraid-api/cli/api-report.service.js'; -import { RestService } from '@app/unraid-api/rest/rest.service.js'; - -const mockWriteFile = vi.fn(); - -vi.mock('node:fs/promises', () => ({ - writeFile: (...args: any[]) => mockWriteFile(...args), - stat: vi.fn(), -})); - -// Mock ApiReportService -const mockApiReportService = { - generateReport: vi.fn(), -}; - -describe('RestService', () => { - let restService: RestService; - - beforeEach(async () => { - const module = await Test.createTestingModule({ - providers: [RestService, { provide: ApiReportService, useValue: mockApiReportService }], - }).compile(); - - restService = module.get(RestService); - - // Clear mocks - vi.clearAllMocks(); - }); - - describe('saveApiReport', () => { - it('should generate report using ApiReportService and save to file', async () => { - const mockReport = { - timestamp: '2023-01-01T00:00:00.000Z', - connectionStatus: { - running: 'yes' as const, - }, - system: { - id: 'test-uuid', - name: 'Test Server', - version: '6.12.0', - machineId: 'REDACTED', - manufacturer: 'Test Manufacturer', - model: 'Test Model', - }, - connect: { - installed: true, - dynamicRemoteAccess: { - enabledType: 'STATIC', - runningType: 'STATIC', - error: null, - }, - }, - config: { - valid: true, - error: null, - }, - services: { - cloud: { name: 'cloud', online: true }, - minigraph: { name: 'minigraph', online: false }, - allServices: [], - }, - remote: { - apikey: 'REDACTED', - localApiKey: 'REDACTED', - accesstoken: 'REDACTED', - idtoken: 'REDACTED', - refreshtoken: 'REDACTED', - ssoSubIds: 'REDACTED', - allowedOrigins: 'REDACTED', - email: 'REDACTED', - }, - }; - - const reportPath = '/tmp/test-report.json'; - mockApiReportService.generateReport.mockResolvedValue(mockReport); - mockWriteFile.mockResolvedValue(undefined); - - await restService.saveApiReport(reportPath); - - // Verify ApiReportService was called (defaults to API running) - expect(mockApiReportService.generateReport).toHaveBeenCalledWith(); - - // Verify file was written with correct content - expect(mockWriteFile).toHaveBeenCalledWith( - reportPath, - JSON.stringify(mockReport, null, 2), - 'utf-8' - ); - }); - - it('should handle ApiReportService errors gracefully', async () => { - const reportPath = '/tmp/test-report.json'; - const error = new Error('Report generation failed'); - mockApiReportService.generateReport.mockRejectedValue(error); - - // Should not throw error - await restService.saveApiReport(reportPath); - - // Verify ApiReportService was called - expect(mockApiReportService.generateReport).toHaveBeenCalled(); - - // Verify file write was not called due to error - expect(mockWriteFile).not.toHaveBeenCalled(); - }); - - it('should handle file write errors gracefully', async () => { - const mockReport = { - timestamp: '2023-01-01T00:00:00.000Z', - system: { name: 'Test' }, - }; - - const reportPath = '/tmp/test-report.json'; - mockApiReportService.generateReport.mockResolvedValue(mockReport); - mockWriteFile.mockRejectedValue(new Error('File write failed')); - - // Should not throw error - await restService.saveApiReport(reportPath); - - // Verify both service and file operations were attempted - expect(mockApiReportService.generateReport).toHaveBeenCalled(); - expect(mockWriteFile).toHaveBeenCalledWith( - reportPath, - JSON.stringify(mockReport, null, 2), - 'utf-8' - ); - }); - }); -}); diff --git a/api/src/unraid-api/rest/rest.controller.ts b/api/src/unraid-api/rest/rest.controller.ts index 08d67ade0..e1402e1ba 100644 --- a/api/src/unraid-api/rest/rest.controller.ts +++ b/api/src/unraid-api/rest/rest.controller.ts @@ -1,8 +1,8 @@ import { Controller, Get, Logger, Param, Query, Req, Res, UnauthorizedException } from '@nestjs/common'; -import escapeHtml from 'escape-html'; import { AuthAction, Resource } from '@unraid/shared/graphql.model.js'; import { UsePermissions } from '@unraid/shared/use-permissions.directive.js'; +import escapeHtml from 'escape-html'; import type { FastifyReply, FastifyRequest } from '@app/unraid-api/types/fastify.js'; import { Public } from '@app/unraid-api/auth/public.decorator.js'; diff --git a/api/src/unraid-api/rest/rest.service.spec.ts b/api/src/unraid-api/rest/rest.service.spec.ts index 5b6424511..1be55250b 100644 --- a/api/src/unraid-api/rest/rest.service.spec.ts +++ b/api/src/unraid-api/rest/rest.service.spec.ts @@ -1,24 +1,88 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import type { ReadStream } from 'node:fs'; +import { createReadStream } from 'node:fs'; +import { Readable } from 'node:stream'; -import { beforeEach, describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { + getBannerPathIfPresent, + getCasePathIfPresent, +} from '@app/core/utils/images/image-file-helpers.js'; import { RestService } from '@app/unraid-api/rest/rest.service.js'; +vi.mock('node:fs'); +vi.mock('@app/core/utils/images/image-file-helpers.js', () => ({ + getBannerPathIfPresent: vi.fn(), + getCasePathIfPresent: vi.fn(), +})); + describe('RestService', () => { let service: RestService; beforeEach(async () => { + vi.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ - providers: [ - RestService, - ], + providers: [RestService], }).compile(); service = module.get(RestService); }); - it('should be defined', () => { - expect(service).toBeDefined(); + describe('getCustomizationPath', () => { + it('returns banner path when present', async () => { + const mockBannerPath = '/path/to/banner.png'; + vi.mocked(getBannerPathIfPresent).mockResolvedValue(mockBannerPath); + + await expect(service.getCustomizationPath('banner')).resolves.toBe(mockBannerPath); + }); + + it('returns case path when present', async () => { + const mockCasePath = '/path/to/case.png'; + vi.mocked(getCasePathIfPresent).mockResolvedValue(mockCasePath); + + await expect(service.getCustomizationPath('case')).resolves.toBe(mockCasePath); + }); + + it('returns null when no path is available', async () => { + vi.mocked(getBannerPathIfPresent).mockResolvedValue(null); + vi.mocked(getCasePathIfPresent).mockResolvedValue(null); + + await expect(service.getCustomizationPath('banner')).resolves.toBeNull(); + await expect(service.getCustomizationPath('case')).resolves.toBeNull(); + }); + }); + + describe('getCustomizationStream', () => { + it('returns read stream for banner', async () => { + const mockPath = '/path/to/banner.png'; + const mockStream: ReadStream = Readable.from([]) as ReadStream; + + vi.mocked(getBannerPathIfPresent).mockResolvedValue(mockPath); + vi.mocked(createReadStream).mockReturnValue(mockStream); + + await expect(service.getCustomizationStream('banner')).resolves.toBe(mockStream); + expect(createReadStream).toHaveBeenCalledWith(mockPath); + }); + + it('returns read stream for case', async () => { + const mockPath = '/path/to/case.png'; + const mockStream: ReadStream = Readable.from([]) as ReadStream; + + vi.mocked(getCasePathIfPresent).mockResolvedValue(mockPath); + vi.mocked(createReadStream).mockReturnValue(mockStream); + + await expect(service.getCustomizationStream('case')).resolves.toBe(mockStream); + expect(createReadStream).toHaveBeenCalledWith(mockPath); + }); + + it('throws when no customization is available', async () => { + vi.mocked(getBannerPathIfPresent).mockResolvedValue(null); + vi.mocked(getCasePathIfPresent).mockResolvedValue(null); + + await expect(service.getCustomizationStream('banner')).rejects.toThrow('No banner found'); + await expect(service.getCustomizationStream('case')).rejects.toThrow('No case found'); + }); }); }); diff --git a/api/src/unraid-api/rest/rest.service.test.ts b/api/src/unraid-api/rest/rest.service.test.ts deleted file mode 100644 index b406e80ea..000000000 --- a/api/src/unraid-api/rest/rest.service.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import type { ReadStream } from 'node:fs'; -import { createReadStream } from 'node:fs'; -import { Readable } from 'node:stream'; - -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { getBannerPathIfPresent, getCasePathIfPresent } from '@app/core/utils/images/image-file-helpers.js'; -import { RestService } from '@app/unraid-api/rest/rest.service.js'; - -vi.mock('node:fs'); -vi.mock('@app/core/utils/images/image-file-helpers.js', () => ({ - getBannerPathIfPresent: vi.fn(), - getCasePathIfPresent: vi.fn(), -})); - -describe('RestService', () => { - let service: RestService; - - beforeEach(async () => { - vi.clearAllMocks(); - - const module: TestingModule = await Test.createTestingModule({ - providers: [RestService], - }).compile(); - - service = module.get(RestService); - }); - - describe('getCustomizationPath', () => { - it('returns banner path when present', async () => { - const mockBannerPath = '/path/to/banner.png'; - vi.mocked(getBannerPathIfPresent).mockResolvedValue(mockBannerPath); - - await expect(service.getCustomizationPath('banner')).resolves.toBe(mockBannerPath); - }); - - it('returns case path when present', async () => { - const mockCasePath = '/path/to/case.png'; - vi.mocked(getCasePathIfPresent).mockResolvedValue(mockCasePath); - - await expect(service.getCustomizationPath('case')).resolves.toBe(mockCasePath); - }); - - it('returns null when no path is available', async () => { - vi.mocked(getBannerPathIfPresent).mockResolvedValue(null); - vi.mocked(getCasePathIfPresent).mockResolvedValue(null); - - await expect(service.getCustomizationPath('banner')).resolves.toBeNull(); - await expect(service.getCustomizationPath('case')).resolves.toBeNull(); - }); - }); - - describe('getCustomizationStream', () => { - it('returns read stream for banner', async () => { - const mockPath = '/path/to/banner.png'; - const mockStream: ReadStream = Readable.from([]) as ReadStream; - - vi.mocked(getBannerPathIfPresent).mockResolvedValue(mockPath); - vi.mocked(createReadStream).mockReturnValue(mockStream); - - await expect(service.getCustomizationStream('banner')).resolves.toBe(mockStream); - expect(createReadStream).toHaveBeenCalledWith(mockPath); - }); - - it('returns read stream for case', async () => { - const mockPath = '/path/to/case.png'; - const mockStream: ReadStream = Readable.from([]) as ReadStream; - - vi.mocked(getCasePathIfPresent).mockResolvedValue(mockPath); - vi.mocked(createReadStream).mockReturnValue(mockStream); - - await expect(service.getCustomizationStream('case')).resolves.toBe(mockStream); - expect(createReadStream).toHaveBeenCalledWith(mockPath); - }); - - it('throws when no customization is available', async () => { - vi.mocked(getBannerPathIfPresent).mockResolvedValue(null); - vi.mocked(getCasePathIfPresent).mockResolvedValue(null); - - await expect(service.getCustomizationStream('banner')).rejects.toThrow('No banner found'); - await expect(service.getCustomizationStream('case')).rejects.toThrow('No case found'); - }); - }); -}); diff --git a/api/src/unraid-api/rest/rest.service.ts b/api/src/unraid-api/rest/rest.service.ts index 575d197da..ca96ff552 100644 --- a/api/src/unraid-api/rest/rest.service.ts +++ b/api/src/unraid-api/rest/rest.service.ts @@ -2,7 +2,10 @@ import { Injectable } from '@nestjs/common'; import type { ReadStream } from 'node:fs'; import { createReadStream } from 'node:fs'; -import { getBannerPathIfPresent, getCasePathIfPresent } from '@app/core/utils/images/image-file-helpers.js'; +import { + getBannerPathIfPresent, + getCasePathIfPresent, +} from '@app/core/utils/images/image-file-helpers.js'; @Injectable() export class RestService { diff --git a/web/__test__/components/Wrapper/component-registry.test.ts b/web/__test__/components/Wrapper/component-registry.test.ts index d80aaf967..f0135c488 100644 --- a/web/__test__/components/Wrapper/component-registry.test.ts +++ b/web/__test__/components/Wrapper/component-registry.test.ts @@ -134,7 +134,6 @@ describe('component-registry', () => { 'user-profile', 'auth', 'connect-settings', - 'download-api-logs', 'modals', 'registration', 'wan-ip-check',