/** * User Service Integration Tests * * Tests userService functions against a real database. * Uses global integration test setup for database management. * * Run with: pnpm test:integration */ import { describe, it, expect, beforeEach } from 'vitest'; import { randomUUID } from 'node:crypto'; import { db } from '../../src/db/client.js'; import { servers } from '../../src/db/schema.js'; import { batchSyncUsersFromMediaServer, syncUserFromMediaServer, getServerUsersByServer, getServerUserByExternalId, } from '../../src/services/userService.js'; import type { MediaUser } from '../../src/services/userService.js'; describe('userService integration tests', () => { let testServerId: string; // Create a fresh server before each test (after global reset) beforeEach(async () => { const [server] = await db .insert(servers) .values({ name: 'Integration Test Server', type: 'plex', url: 'http://localhost:32400', token: 'encrypted-test-token', }) .returning(); testServerId = server.id; }); describe('syncUserFromMediaServer', () => { it('should create a new user and server user when none exists', async () => { const mediaUser: MediaUser = { id: `ext-${randomUUID().slice(0, 8)}`, username: 'newuser', email: 'newuser@example.com', thumb: 'https://example.com/thumb.jpg', isAdmin: false, }; const result = await syncUserFromMediaServer(testServerId, mediaUser); expect(result.created).toBe(true); expect(result.serverUser.externalId).toBe(mediaUser.id); expect(result.serverUser.username).toBe(mediaUser.username); expect(result.user.username).toBe(mediaUser.username); // Verify in database const dbServerUser = await getServerUserByExternalId(testServerId, mediaUser.id); expect(dbServerUser).not.toBeNull(); expect(dbServerUser?.username).toBe(mediaUser.username); }); it('should update existing server user when already exists', async () => { const externalId = `ext-${randomUUID().slice(0, 8)}`; const mediaUser: MediaUser = { id: externalId, username: 'originalname', isAdmin: false, }; // First create const createResult = await syncUserFromMediaServer(testServerId, mediaUser); expect(createResult.created).toBe(true); // Then update with new username const updatedMediaUser: MediaUser = { id: externalId, username: 'updatedname', email: 'updated@example.com', isAdmin: true, }; const updateResult = await syncUserFromMediaServer(testServerId, updatedMediaUser); expect(updateResult.created).toBe(false); expect(updateResult.serverUser.username).toBe('updatedname'); }); }); describe('batchSyncUsersFromMediaServer', () => { it('should return zeros for empty input', async () => { const result = await batchSyncUsersFromMediaServer(testServerId, []); expect(result.added).toBe(0); expect(result.updated).toBe(0); }); it('should create multiple new users', async () => { const mediaUsers: MediaUser[] = [ { id: `batch-1-${randomUUID().slice(0, 8)}`, username: 'batchuser1', isAdmin: false }, { id: `batch-2-${randomUUID().slice(0, 8)}`, username: 'batchuser2', isAdmin: false }, { id: `batch-3-${randomUUID().slice(0, 8)}`, username: 'batchuser3', isAdmin: true }, ]; const result = await batchSyncUsersFromMediaServer(testServerId, mediaUsers); expect(result.added).toBe(3); expect(result.updated).toBe(0); // Verify all users exist in database const serverUsersMap = await getServerUsersByServer(testServerId); expect(serverUsersMap.size).toBe(3); expect(serverUsersMap.get(mediaUsers[0].id)?.username).toBe('batchuser1'); expect(serverUsersMap.get(mediaUsers[1].id)?.username).toBe('batchuser2'); expect(serverUsersMap.get(mediaUsers[2].id)?.username).toBe('batchuser3'); }); it('should handle mix of new and existing users', async () => { // Create one user first const existingExternalId = `existing-${randomUUID().slice(0, 8)}`; await syncUserFromMediaServer(testServerId, { id: existingExternalId, username: 'existinguser', isAdmin: false, }); // Now batch sync with mix of existing and new const mediaUsers: MediaUser[] = [ { id: existingExternalId, username: 'existinguser-updated', isAdmin: false }, { id: `new-1-${randomUUID().slice(0, 8)}`, username: 'newuser1', isAdmin: false }, { id: `new-2-${randomUUID().slice(0, 8)}`, username: 'newuser2', isAdmin: false }, ]; const result = await batchSyncUsersFromMediaServer(testServerId, mediaUsers); expect(result.added).toBe(2); expect(result.updated).toBe(1); // Verify the existing user was updated const updatedUser = await getServerUserByExternalId(testServerId, existingExternalId); expect(updatedUser?.username).toBe('existinguser-updated'); }); it('should handle all existing users (update only)', async () => { // Create users first const externalIds = [ `preexist-1-${randomUUID().slice(0, 8)}`, `preexist-2-${randomUUID().slice(0, 8)}`, ]; for (const extId of externalIds) { await syncUserFromMediaServer(testServerId, { id: extId, username: `user-${extId.slice(0, 8)}`, isAdmin: false, }); } // Batch sync with updates only const mediaUsers: MediaUser[] = externalIds.map((extId) => ({ id: extId, username: `updated-${extId.slice(0, 8)}`, isAdmin: true, })); const result = await batchSyncUsersFromMediaServer(testServerId, mediaUsers); expect(result.added).toBe(0); expect(result.updated).toBe(2); }); }); describe('getServerUsersByServer', () => { it('should return empty map for server with no users', async () => { const result = await getServerUsersByServer(testServerId); expect(result.size).toBe(0); }); it('should return map of all server users keyed by externalId', async () => { // Create some users const mediaUsers: MediaUser[] = [ { id: `map-1-${randomUUID().slice(0, 8)}`, username: 'mapuser1', isAdmin: false }, { id: `map-2-${randomUUID().slice(0, 8)}`, username: 'mapuser2', isAdmin: true }, ]; await batchSyncUsersFromMediaServer(testServerId, mediaUsers); const result = await getServerUsersByServer(testServerId); expect(result.size).toBe(2); expect(result.has(mediaUsers[0].id)).toBe(true); expect(result.has(mediaUsers[1].id)).toBe(true); expect(result.get(mediaUsers[0].id)?.username).toBe('mapuser1'); expect(result.get(mediaUsers[1].id)?.isServerAdmin).toBe(true); }); }); });