mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
a97979358f
Adds file hash-based matching for Audiobookshelf library items to ensure 100% accurate ASIN assignment for RMAB-organized content. Removes fuzzy matching from library availability checks, making all matching ASIN-only to eliminate false positives and race conditions. Updates database schema, processors, and matcher utilities; adds new tests and documentation for the new matching strategy. Removes obsolete scripts, Dockerfile, and related tests; updates docker-compose for test environments.
233 lines
7.8 KiB
TypeScript
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', role: 'user', 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', role: 'user', 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', role: 'user', authProvider: 'plex' });
|
|
|
|
expect(ok).toBe(true);
|
|
expect(encryptionMock.decrypt).toHaveBeenCalledWith('enc:token');
|
|
});
|
|
});
|
|
|
|
|