Files
ReadMeABook/tests/services/auth/plex-auth-provider.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

233 lines
7.8 KiB
TypeScript

/**
* Component: Plex Auth Provider Tests
* Documentation: documentation/backend/services/auth.md
*/
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { createPrismaMock } from '../../helpers/prisma';
const prismaMock = createPrismaMock();
const configMock = vi.hoisted(() => ({
getPlexConfig: vi.fn(),
}));
const encryptionMock = vi.hoisted(() => ({
encrypt: vi.fn((value: string) => `enc:${value}`),
decrypt: vi.fn((value: string) => value.replace('enc:', '')),
}));
const plexServiceMock = vi.hoisted(() => ({
requestPin: vi.fn(),
getOAuthUrl: vi.fn(),
checkPin: vi.fn(),
getUserInfo: vi.fn(),
verifyServerAccess: vi.fn(),
getHomeUsers: vi.fn(),
}));
vi.mock('@/lib/db', () => ({
prisma: prismaMock,
}));
vi.mock('@/lib/services/config.service', () => ({
getConfigService: () => configMock,
}));
vi.mock('@/lib/services/encryption.service', () => ({
getEncryptionService: () => encryptionMock,
}));
vi.mock('@/lib/integrations/plex.service', () => ({
getPlexService: () => plexServiceMock,
}));
describe('PlexAuthProvider', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('initiates login and returns OAuth URL', async () => {
process.env.PLEX_OAUTH_CALLBACK_URL = 'http://app/callback';
plexServiceMock.requestPin.mockResolvedValue({ id: 42, code: 'CODE' });
plexServiceMock.getOAuthUrl.mockReturnValue('http://plex/oauth');
const { PlexAuthProvider } = await import('@/lib/services/auth/PlexAuthProvider');
const provider = new PlexAuthProvider();
const result = await provider.initiateLogin();
expect(result.redirectUrl).toBe('http://plex/oauth');
expect(result.pinId).toBe('42');
});
it('returns error when PIN authorization is still pending', async () => {
plexServiceMock.checkPin.mockResolvedValue(null);
const { PlexAuthProvider } = await import('@/lib/services/auth/PlexAuthProvider');
const provider = new PlexAuthProvider();
const result = await provider.handleCallback({ pinId: '123' });
expect(result.success).toBe(false);
expect(result.error).toMatch(/waiting for user authorization/i);
});
it('returns error when pinId is missing', async () => {
const { PlexAuthProvider } = await import('@/lib/services/auth/PlexAuthProvider');
const provider = new PlexAuthProvider();
const result = await provider.handleCallback({});
expect(result.success).toBe(false);
expect(result.error).toMatch(/missing pin id/i);
});
it('returns error when Plex server is not configured', async () => {
plexServiceMock.checkPin.mockResolvedValue('token');
plexServiceMock.getUserInfo.mockResolvedValue({
id: 1,
username: 'user',
authToken: 'token',
});
configMock.getPlexConfig.mockResolvedValue({
serverUrl: null,
authToken: 'token',
libraryId: null,
machineIdentifier: null,
});
const { PlexAuthProvider } = await import('@/lib/services/auth/PlexAuthProvider');
const provider = new PlexAuthProvider();
const result = await provider.handleCallback({ pinId: '123' });
expect(result.success).toBe(false);
expect(result.error).toMatch(/plex server is not configured/i);
});
it('returns profile selection when multiple home users are present', async () => {
plexServiceMock.checkPin.mockResolvedValue('token');
plexServiceMock.getUserInfo.mockResolvedValue({
id: 1,
username: 'user',
authToken: 'token',
});
configMock.getPlexConfig.mockResolvedValue({
serverUrl: 'http://plex',
authToken: 'token',
libraryId: 'lib',
machineIdentifier: 'machine',
});
plexServiceMock.verifyServerAccess.mockResolvedValue(true);
plexServiceMock.getHomeUsers.mockResolvedValue([
{ id: '1', title: 'User 1' },
{ id: '2', title: 'User 2' },
]);
const { PlexAuthProvider } = await import('@/lib/services/auth/PlexAuthProvider');
const provider = new PlexAuthProvider();
const result = await provider.handleCallback({ pinId: '123' });
expect(result.success).toBe(true);
expect(result.requiresProfileSelection).toBe(true);
expect(result.profiles?.length).toBe(2);
});
it('denies login when server access check fails', async () => {
plexServiceMock.checkPin.mockResolvedValue('token');
plexServiceMock.getUserInfo.mockResolvedValue({
id: 1,
username: 'user',
authToken: 'token',
});
configMock.getPlexConfig.mockResolvedValue({
serverUrl: 'http://plex',
authToken: 'token',
libraryId: 'lib',
machineIdentifier: 'machine',
});
plexServiceMock.verifyServerAccess.mockResolvedValue(false);
const { PlexAuthProvider } = await import('@/lib/services/auth/PlexAuthProvider');
const provider = new PlexAuthProvider();
const result = await provider.handleCallback({ pinId: '123' });
expect(result.success).toBe(false);
expect(result.error).toMatch(/do not have access/i);
});
it('creates user and returns tokens when auth succeeds', async () => {
plexServiceMock.checkPin.mockResolvedValue('token');
plexServiceMock.getUserInfo.mockResolvedValue({
id: 1,
username: 'user',
email: 'user@example.com',
thumb: 'avatar',
authToken: 'token',
});
configMock.getPlexConfig.mockResolvedValue({
serverUrl: 'http://plex',
authToken: 'token',
libraryId: 'lib',
machineIdentifier: 'machine',
});
plexServiceMock.verifyServerAccess.mockResolvedValue(true);
plexServiceMock.getHomeUsers.mockResolvedValue([]);
prismaMock.user.count.mockResolvedValue(1);
prismaMock.user.upsert.mockResolvedValue({
id: 'user-1',
plexUsername: 'user',
plexEmail: 'user@example.com',
avatarUrl: 'avatar',
role: 'user',
});
const { PlexAuthProvider } = await import('@/lib/services/auth/PlexAuthProvider');
const provider = new PlexAuthProvider();
const result = await provider.handleCallback({ pinId: '123' });
expect(result.success).toBe(true);
expect(result.tokens?.accessToken).toBeTruthy();
expect(result.user?.authProvider).toBe('plex');
});
it('returns false when access validation has no server config', async () => {
configMock.getPlexConfig.mockResolvedValue({
serverUrl: null,
machineIdentifier: null,
});
const { PlexAuthProvider } = await import('@/lib/services/auth/PlexAuthProvider');
const provider = new PlexAuthProvider();
const ok = await provider.validateAccess({ id: 'user-1', username: 'user', isAdmin: false, authProvider: 'plex' });
expect(ok).toBe(false);
});
it('returns false when Plex auth token is missing in the database', async () => {
configMock.getPlexConfig.mockResolvedValue({
serverUrl: 'http://plex',
machineIdentifier: 'machine',
});
prismaMock.user.findUnique.mockResolvedValue({ id: 'user-1', authToken: null });
const { PlexAuthProvider } = await import('@/lib/services/auth/PlexAuthProvider');
const provider = new PlexAuthProvider();
const ok = await provider.validateAccess({ id: 'user-1', username: 'user', isAdmin: false, authProvider: 'plex' });
expect(ok).toBe(false);
});
it('decrypts tokens and verifies server access', async () => {
configMock.getPlexConfig.mockResolvedValue({
serverUrl: 'http://plex',
machineIdentifier: 'machine',
});
prismaMock.user.findUnique.mockResolvedValue({ id: 'user-1', authToken: 'enc:token' });
plexServiceMock.verifyServerAccess.mockResolvedValue(true);
const { PlexAuthProvider } = await import('@/lib/services/auth/PlexAuthProvider');
const provider = new PlexAuthProvider();
const ok = await provider.validateAccess({ id: 'user-1', username: 'user', isAdmin: false, authProvider: 'plex' });
expect(ok).toBe(true);
expect(encryptionMock.decrypt).toHaveBeenCalledWith('enc:token');
});
});