Refactor RestService tests and remove deprecated files

- Updated `rest.service.spec.ts` to include new tests for `getCustomizationPath` and `getCustomizationStream` methods, ensuring proper handling of banner and case paths.
- Removed obsolete test files: `rest.service.test.ts`, `rest-module-dependencies.test.ts`, and `rest-module.integration.test.ts` to streamline the test suite.
- Added necessary mocks for file system operations and image file helper functions to enhance test reliability.
This commit is contained in:
Eli Bosley
2025-11-19 06:29:42 -05:00
parent c78197052e
commit 6527494ce6
8 changed files with 77 additions and 364 deletions

View File

@@ -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>(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/);
});
});

View File

@@ -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>(RestService);
const apiReportService = module.get<ApiReportService>(ApiReportService);
expect(restService).toBeDefined();
expect(apiReportService).toBeDefined();
// Verify RestService has the injected ApiReportService
expect(restService['apiReportService']).toBeDefined();
await module.close();
}, 10000);
});

View File

@@ -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>(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'
);
});
});
});

View File

@@ -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';

View File

@@ -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>(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');
});
});
});

View File

@@ -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>(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');
});
});
});

View File

@@ -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 {

View File

@@ -134,7 +134,6 @@ describe('component-registry', () => {
'user-profile',
'auth',
'connect-settings',
'download-api-logs',
'modals',
'registration',
'wan-ip-check',