mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
94dbaf073b
Introduced a Vitest-based backend unit testing framework with supporting scripts, helpers, and GitHub Actions integration. Refactored the admin settings page to a modular architecture, splitting monolithic logic into feature-specific tabs and hooks for improved maintainability and testability. Updated documentation to reflect the new testing setup and settings architecture, and added new dependencies for testing utilities.
258 lines
8.5 KiB
TypeScript
258 lines
8.5 KiB
TypeScript
/**
|
|
* Component: Admin Settings Test API Route Tests
|
|
* Documentation: documentation/testing.md
|
|
*/
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { createPrismaMock } from '../helpers/prisma';
|
|
|
|
let authRequest: any;
|
|
|
|
const prismaMock = createPrismaMock();
|
|
const requireAuthMock = vi.hoisted(() => vi.fn());
|
|
const requireAdminMock = vi.hoisted(() => vi.fn());
|
|
const plexServiceMock = vi.hoisted(() => ({
|
|
testConnection: vi.fn(),
|
|
getLibraries: vi.fn(),
|
|
}));
|
|
const prowlarrMock = vi.hoisted(() => ({
|
|
getIndexers: vi.fn(),
|
|
}));
|
|
const qbtMock = vi.hoisted(() => ({
|
|
testConnectionWithCredentials: vi.fn(),
|
|
}));
|
|
const sabnzbdMock = vi.hoisted(() => ({
|
|
testConnection: vi.fn(),
|
|
}));
|
|
const testFlareSolverrMock = vi.hoisted(() => vi.fn());
|
|
const fsMock = vi.hoisted(() => ({
|
|
access: vi.fn(),
|
|
constants: { R_OK: 4 },
|
|
}));
|
|
|
|
vi.mock('@/lib/db', () => ({
|
|
prisma: prismaMock,
|
|
}));
|
|
|
|
vi.mock('@/lib/middleware/auth', () => ({
|
|
requireAuth: requireAuthMock,
|
|
requireAdmin: requireAdminMock,
|
|
}));
|
|
|
|
vi.mock('@/lib/integrations/plex.service', () => ({
|
|
getPlexService: () => plexServiceMock,
|
|
}));
|
|
|
|
vi.mock('@/lib/integrations/prowlarr.service', () => ({
|
|
ProwlarrService: class {
|
|
constructor() {}
|
|
getIndexers = prowlarrMock.getIndexers;
|
|
},
|
|
}));
|
|
|
|
vi.mock('@/lib/integrations/qbittorrent.service', () => ({
|
|
QBittorrentService: {
|
|
testConnectionWithCredentials: qbtMock.testConnectionWithCredentials,
|
|
},
|
|
}));
|
|
|
|
vi.mock('@/lib/integrations/sabnzbd.service', () => ({
|
|
SABnzbdService: class {
|
|
constructor() {}
|
|
testConnection = sabnzbdMock.testConnection;
|
|
},
|
|
}));
|
|
|
|
vi.mock('@/lib/services/ebook-scraper', () => ({
|
|
testFlareSolverrConnection: testFlareSolverrMock,
|
|
}));
|
|
|
|
vi.mock('fs/promises', () => ({
|
|
default: fsMock,
|
|
...fsMock,
|
|
}));
|
|
|
|
describe('Admin settings test routes', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
authRequest = { user: { id: 'admin-1', role: 'admin' }, json: vi.fn() };
|
|
requireAuthMock.mockImplementation((_req: any, handler: any) => handler(authRequest));
|
|
requireAdminMock.mockImplementation((_req: any, handler: any) => handler());
|
|
fsMock.access.mockResolvedValue(undefined);
|
|
});
|
|
|
|
it('tests Plex connection with stored token', async () => {
|
|
prismaMock.configuration.findUnique.mockResolvedValueOnce({ value: 'token' });
|
|
plexServiceMock.testConnection.mockResolvedValueOnce({ success: true, info: { platform: 'Plex', version: '1.0' } });
|
|
plexServiceMock.getLibraries.mockResolvedValueOnce([{ id: '1', title: 'Books', type: 'book' }]);
|
|
|
|
const request = { json: vi.fn().mockResolvedValue({ url: 'http://plex', token: '********' }) };
|
|
const { POST } = await import('@/app/api/admin/settings/test-plex/route');
|
|
const response = await POST(request as any);
|
|
const payload = await response.json();
|
|
|
|
expect(payload.success).toBe(true);
|
|
});
|
|
|
|
it('tests Prowlarr connection', async () => {
|
|
prowlarrMock.getIndexers.mockResolvedValueOnce([{ id: 1, name: 'Indexer', protocol: 'torrent', enable: true }]);
|
|
const request = { json: vi.fn().mockResolvedValue({ url: 'http://prowlarr', apiKey: 'key' }) };
|
|
|
|
const { POST } = await import('@/app/api/admin/settings/test-prowlarr/route');
|
|
const response = await POST(request as any);
|
|
const payload = await response.json();
|
|
|
|
expect(payload.success).toBe(true);
|
|
});
|
|
|
|
it('tests download client connection', async () => {
|
|
qbtMock.testConnectionWithCredentials.mockResolvedValueOnce('4.0.0');
|
|
const request = {
|
|
json: vi.fn().mockResolvedValue({ type: 'qbittorrent', url: 'http://qbt', username: 'user', password: 'pass' }),
|
|
};
|
|
|
|
const { POST } = await import('@/app/api/admin/settings/test-download-client/route');
|
|
const response = await POST(request as any);
|
|
const payload = await response.json();
|
|
|
|
expect(payload.success).toBe(true);
|
|
expect(payload.version).toBe('4.0.0');
|
|
});
|
|
|
|
it('validates required fields for download client testing', async () => {
|
|
const request = { json: vi.fn().mockResolvedValue({ url: 'http://qbt' }) };
|
|
|
|
const { POST } = await import('@/app/api/admin/settings/test-download-client/route');
|
|
const response = await POST(request as any);
|
|
const payload = await response.json();
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(payload.error).toMatch(/Type and URL are required/);
|
|
});
|
|
|
|
it('rejects invalid download client types', async () => {
|
|
const request = { json: vi.fn().mockResolvedValue({ type: 'invalid', url: 'http://qbt' }) };
|
|
|
|
const { POST } = await import('@/app/api/admin/settings/test-download-client/route');
|
|
const response = await POST(request as any);
|
|
const payload = await response.json();
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(payload.error).toMatch(/Invalid client type/);
|
|
});
|
|
|
|
it('uses stored password when masked password is provided', async () => {
|
|
prismaMock.configuration.findUnique.mockResolvedValueOnce({ value: 'stored-pass' });
|
|
qbtMock.testConnectionWithCredentials.mockResolvedValueOnce('4.1.0');
|
|
const request = {
|
|
json: vi.fn().mockResolvedValue({
|
|
type: 'qbittorrent',
|
|
url: 'http://qbt',
|
|
username: 'user',
|
|
password: '\u2022\u2022\u2022\u2022',
|
|
}),
|
|
};
|
|
|
|
const { POST } = await import('@/app/api/admin/settings/test-download-client/route');
|
|
const response = await POST(request as any);
|
|
const payload = await response.json();
|
|
|
|
expect(payload.success).toBe(true);
|
|
expect(qbtMock.testConnectionWithCredentials).toHaveBeenCalledWith(
|
|
'http://qbt',
|
|
'user',
|
|
'stored-pass',
|
|
false
|
|
);
|
|
});
|
|
|
|
it('returns error when masked password is missing in storage', async () => {
|
|
prismaMock.configuration.findUnique.mockResolvedValueOnce(null);
|
|
const request = {
|
|
json: vi.fn().mockResolvedValue({
|
|
type: 'qbittorrent',
|
|
url: 'http://qbt',
|
|
username: 'user',
|
|
password: '\u2022\u2022\u2022\u2022',
|
|
}),
|
|
};
|
|
|
|
const { POST } = await import('@/app/api/admin/settings/test-download-client/route');
|
|
const response = await POST(request as any);
|
|
const payload = await response.json();
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(payload.error).toMatch(/No stored password/);
|
|
});
|
|
|
|
it('returns error when SABnzbd connection fails', async () => {
|
|
sabnzbdMock.testConnection.mockResolvedValueOnce({ success: false, error: 'bad key' });
|
|
const request = {
|
|
json: vi.fn().mockResolvedValue({ type: 'sabnzbd', url: 'http://sab', password: 'key' }),
|
|
};
|
|
|
|
const { POST } = await import('@/app/api/admin/settings/test-download-client/route');
|
|
const response = await POST(request as any);
|
|
const payload = await response.json();
|
|
|
|
expect(response.status).toBe(500);
|
|
expect(payload.error).toMatch(/bad key/);
|
|
});
|
|
|
|
it('requires path mapping values when enabled', async () => {
|
|
qbtMock.testConnectionWithCredentials.mockResolvedValueOnce('4.0.0');
|
|
const request = {
|
|
json: vi.fn().mockResolvedValue({
|
|
type: 'qbittorrent',
|
|
url: 'http://qbt',
|
|
username: 'user',
|
|
password: 'pass',
|
|
remotePathMappingEnabled: true,
|
|
}),
|
|
};
|
|
|
|
const { POST } = await import('@/app/api/admin/settings/test-download-client/route');
|
|
const response = await POST(request as any);
|
|
const payload = await response.json();
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(payload.error).toMatch(/Remote path and local path are required/);
|
|
});
|
|
|
|
it('rejects inaccessible local path when mapping is enabled', async () => {
|
|
qbtMock.testConnectionWithCredentials.mockResolvedValueOnce('4.0.0');
|
|
fsMock.access.mockRejectedValueOnce(new Error('missing'));
|
|
const request = {
|
|
json: vi.fn().mockResolvedValue({
|
|
type: 'qbittorrent',
|
|
url: 'http://qbt',
|
|
username: 'user',
|
|
password: 'pass',
|
|
remotePathMappingEnabled: true,
|
|
remotePath: '/remote',
|
|
localPath: '/local',
|
|
}),
|
|
};
|
|
|
|
const { POST } = await import('@/app/api/admin/settings/test-download-client/route');
|
|
const response = await POST(request as any);
|
|
const payload = await response.json();
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(payload.error).toMatch(/not accessible/);
|
|
});
|
|
|
|
it('tests FlareSolverr connection', async () => {
|
|
testFlareSolverrMock.mockResolvedValueOnce({ success: true });
|
|
const request = { json: vi.fn().mockResolvedValue({ url: 'http://flare' }) };
|
|
|
|
const { POST } = await import('@/app/api/admin/settings/ebook/test-flaresolverr/route');
|
|
const response = await POST(request as any);
|
|
const payload = await response.json();
|
|
|
|
expect(payload.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
|