/** * 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'); }); });