Files
ReadMeABook/tests/api/setup.routes.test.ts
T
kikootwo 94dbaf073b Add backend unit test framework and modularize settings UI
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.
2026-01-28 11:41:59 -05:00

197 lines
6.6 KiB
TypeScript

/**
* Component: Setup API Route Tests
* Documentation: documentation/testing.md
*/
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { createPrismaMock } from '../helpers/prisma';
const prismaMock = createPrismaMock();
const bcryptMock = vi.hoisted(() => ({
hash: vi.fn(),
}));
const encryptionMock = vi.hoisted(() => ({
encrypt: vi.fn((value: string) => `enc-${value}`),
}));
const generateAccessTokenMock = vi.hoisted(() => vi.fn(() => 'access-token'));
const generateRefreshTokenMock = vi.hoisted(() => vi.fn(() => 'refresh-token'));
vi.mock('@/lib/db', () => ({
prisma: prismaMock,
}));
vi.mock('bcrypt', () => ({
default: bcryptMock,
...bcryptMock,
}));
vi.mock('@/lib/services/encryption.service', () => ({
getEncryptionService: () => encryptionMock,
}));
vi.mock('@/lib/utils/jwt', () => ({
generateAccessToken: generateAccessTokenMock,
generateRefreshToken: generateRefreshTokenMock,
}));
describe('Setup routes', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('returns setup status from configuration', async () => {
prismaMock.configuration.findUnique.mockResolvedValueOnce({ value: 'true' });
const { GET } = await import('@/app/api/setup/status/route');
const response = await GET({} as any);
const payload = await response.json();
expect(payload.setupComplete).toBe(true);
});
it('rejects invalid backend mode on setup completion', async () => {
const { POST } = await import('@/app/api/setup/complete/route');
const response = await POST({ json: vi.fn().mockResolvedValue({ backendMode: 'invalid' }) } as any);
const payload = await response.json();
expect(response.status).toBe(400);
expect(payload.error).toMatch(/Invalid or missing backend mode/);
});
it('completes setup for Plex mode and returns tokens', async () => {
bcryptMock.hash.mockResolvedValue('hashed');
prismaMock.user.create.mockResolvedValue({
id: 'admin-1',
plexId: 'local-admin',
plexUsername: 'admin',
plexEmail: null,
role: 'admin',
avatarUrl: null,
});
prismaMock.configuration.upsert.mockResolvedValue({});
prismaMock.scheduledJob.updateMany.mockResolvedValue({ count: 1 });
prismaMock.bookDateConfig.findFirst.mockResolvedValue(null);
const { POST } = await import('@/app/api/setup/complete/route');
const response = await POST({
json: vi.fn().mockResolvedValue({
backendMode: 'plex',
admin: { username: 'admin', password: 'pass' },
plex: { url: 'http://plex', token: 'token', audiobook_library_id: 'lib', machine_identifier: 'machine' },
prowlarr: { url: 'http://prowlarr', api_key: 'key', indexers: [{ id: 1 }] },
downloadClient: { type: 'qbittorrent', url: 'http://qbt', username: 'u', password: 'p' },
paths: { download_dir: '/downloads', media_dir: '/media' },
}),
} as any);
const payload = await response.json();
expect(payload.success).toBe(true);
expect(payload.accessToken).toBe('access-token');
expect(prismaMock.configuration.upsert).toHaveBeenCalled();
});
it('completes setup for Audiobookshelf with both auth methods', async () => {
bcryptMock.hash.mockResolvedValue('hashed');
prismaMock.user.create.mockResolvedValue({
id: 'admin-2',
plexId: 'local-admin',
plexUsername: 'admin',
plexEmail: null,
role: 'admin',
avatarUrl: null,
});
prismaMock.configuration.upsert.mockResolvedValue({});
prismaMock.scheduledJob.updateMany.mockResolvedValue({ count: 1 });
prismaMock.bookDateConfig.findFirst.mockResolvedValue({ id: 'bookdate-1' });
prismaMock.bookDateConfig.update.mockResolvedValue({});
const { POST } = await import('@/app/api/setup/complete/route');
const response = await POST({
json: vi.fn().mockResolvedValue({
backendMode: 'audiobookshelf',
admin: { username: 'admin', password: 'pass' },
authMethod: 'both',
audiobookshelf: {
server_url: 'http://abs',
api_token: 'abs-token',
library_id: 'lib',
trigger_scan_after_import: true,
},
oidc: {
provider_name: 'OIDC',
issuer_url: 'https://issuer',
client_id: 'client-id',
client_secret: 'client-secret',
access_control_method: 'open',
access_group_claim: 'groups',
access_group_value: '',
allowed_emails: '[]',
allowed_usernames: '[]',
admin_claim_enabled: 'true',
admin_claim_name: 'groups',
admin_claim_value: 'admins',
},
registration: { require_admin_approval: true },
prowlarr: { url: 'http://prowlarr', api_key: 'key', indexers: [{ id: 1 }] },
downloadClient: {
type: 'qbittorrent',
url: 'http://qbt',
username: 'u',
password: 'p',
disableSSLVerify: true,
remotePathMappingEnabled: true,
remotePath: '/remote',
localPath: '/local',
},
paths: {
download_dir: '/downloads',
media_dir: '/media',
metadata_tagging_enabled: false,
chapter_merging_enabled: true,
},
bookdate: { provider: 'openai', apiKey: 'bd-key', model: 'gpt-4' },
}),
} as any);
const payload = await response.json();
expect(payload.success).toBe(true);
expect(payload.accessToken).toBe('access-token');
expect(prismaMock.bookDateConfig.update).toHaveBeenCalled();
});
it('completes setup for Audiobookshelf without admin user', async () => {
prismaMock.configuration.upsert.mockResolvedValue({});
prismaMock.scheduledJob.updateMany.mockResolvedValue({ count: 1 });
prismaMock.bookDateConfig.findFirst.mockResolvedValue(null);
const { POST } = await import('@/app/api/setup/complete/route');
const response = await POST({
json: vi.fn().mockResolvedValue({
backendMode: 'audiobookshelf',
authMethod: 'oidc',
audiobookshelf: {
server_url: 'http://abs',
api_token: 'abs-token',
library_id: 'lib',
},
oidc: {
provider_name: 'OIDC',
issuer_url: 'https://issuer',
client_id: 'client-id',
client_secret: 'client-secret',
},
prowlarr: { url: 'http://prowlarr', api_key: 'key', indexers: [{ id: 1 }] },
downloadClient: { type: 'qbittorrent', url: 'http://qbt', username: 'u', password: 'p' },
paths: { download_dir: '/downloads', media_dir: '/media' },
}),
} as any);
const payload = await response.json();
expect(payload.success).toBe(true);
expect(payload.accessToken).toBeUndefined();
});
});