Skip to content

Commit d01d5d9

Browse files
committed
fix: address PR review comments
- Add Zod validation to toggleJellyfinLogin server action - Add unit tests for toggleJellyfinLogin action - Add E2E test for Jellyfin login toggle in admin settings
1 parent f5e8540 commit d01d5d9

File tree

3 files changed

+171
-1
lines changed

3 files changed

+171
-1
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/**
2+
* Tests for actions/admin/admin-servers.ts - Jellyfin login toggle
3+
*
4+
* These tests cover:
5+
* - toggleJellyfinLogin: enable/disable Jellyfin visibility on login page
6+
* - Admin authorization
7+
* - Input validation with Zod
8+
*/
9+
10+
import { toggleJellyfinLogin } from '@/actions/admin/admin-servers'
11+
import { prisma } from '@/lib/prisma'
12+
import { getServerSession } from 'next-auth'
13+
import { revalidatePath } from 'next/cache'
14+
15+
// Mock dependencies
16+
jest.mock('@/lib/prisma', () => ({
17+
prisma: {
18+
jellyfinServer: {
19+
updateMany: jest.fn(),
20+
},
21+
},
22+
}))
23+
24+
jest.mock('next-auth', () => ({
25+
getServerSession: jest.fn(),
26+
}))
27+
28+
jest.mock('next/cache', () => ({
29+
revalidatePath: jest.fn(),
30+
}))
31+
32+
jest.mock('@/lib/auth', () => ({
33+
authOptions: {},
34+
}))
35+
36+
const mockPrisma = prisma as jest.Mocked<typeof prisma>
37+
const mockGetServerSession = getServerSession as jest.MockedFunction<typeof getServerSession>
38+
const mockRevalidatePath = revalidatePath as jest.MockedFunction<typeof revalidatePath>
39+
40+
describe('admin-servers actions', () => {
41+
const mockAdminSession = {
42+
user: { id: 'admin-123', name: 'Admin', email: '[email protected]', isAdmin: true },
43+
expires: new Date(Date.now() + 86400000).toISOString(),
44+
}
45+
46+
const mockNonAdminSession = {
47+
user: { id: 'user-123', name: 'User', email: '[email protected]', isAdmin: false },
48+
expires: new Date(Date.now() + 86400000).toISOString(),
49+
}
50+
51+
beforeEach(() => {
52+
jest.clearAllMocks()
53+
})
54+
55+
describe('toggleJellyfinLogin', () => {
56+
it('should enable Jellyfin login when passed true', async () => {
57+
mockGetServerSession.mockResolvedValue(mockAdminSession)
58+
mockPrisma.jellyfinServer.updateMany.mockResolvedValue({ count: 1 })
59+
60+
const result = await toggleJellyfinLogin(true)
61+
62+
expect(result).toEqual({ success: true })
63+
expect(mockPrisma.jellyfinServer.updateMany).toHaveBeenCalledWith({
64+
where: { isActive: true },
65+
data: { enabledForLogin: true },
66+
})
67+
expect(mockRevalidatePath).toHaveBeenCalledWith('/admin/settings')
68+
expect(mockRevalidatePath).toHaveBeenCalledWith('/')
69+
})
70+
71+
it('should disable Jellyfin login when passed false', async () => {
72+
mockGetServerSession.mockResolvedValue(mockAdminSession)
73+
mockPrisma.jellyfinServer.updateMany.mockResolvedValue({ count: 1 })
74+
75+
const result = await toggleJellyfinLogin(false)
76+
77+
expect(result).toEqual({ success: true })
78+
expect(mockPrisma.jellyfinServer.updateMany).toHaveBeenCalledWith({
79+
where: { isActive: true },
80+
data: { enabledForLogin: false },
81+
})
82+
})
83+
84+
it('should reject non-admin users', async () => {
85+
mockGetServerSession.mockResolvedValue(mockNonAdminSession)
86+
87+
await expect(toggleJellyfinLogin(true)).rejects.toThrow()
88+
})
89+
90+
it('should reject unauthenticated users', async () => {
91+
mockGetServerSession.mockResolvedValue(null)
92+
93+
await expect(toggleJellyfinLogin(true)).rejects.toThrow()
94+
})
95+
96+
it('should return error for invalid input', async () => {
97+
mockGetServerSession.mockResolvedValue(mockAdminSession)
98+
99+
// TypeScript would normally catch this, but testing runtime validation
100+
const result = await toggleJellyfinLogin('invalid' as unknown as boolean)
101+
102+
expect(result).toEqual({
103+
success: false,
104+
error: 'Invalid input: enabled must be a boolean',
105+
})
106+
expect(mockPrisma.jellyfinServer.updateMany).not.toHaveBeenCalled()
107+
})
108+
109+
it('should handle database errors gracefully', async () => {
110+
mockGetServerSession.mockResolvedValue(mockAdminSession)
111+
mockPrisma.jellyfinServer.updateMany.mockRejectedValue(new Error('Database error'))
112+
113+
const result = await toggleJellyfinLogin(true)
114+
115+
expect(result).toEqual({
116+
success: false,
117+
error: 'Database error',
118+
})
119+
})
120+
121+
it('should handle unknown errors gracefully', async () => {
122+
mockGetServerSession.mockResolvedValue(mockAdminSession)
123+
mockPrisma.jellyfinServer.updateMany.mockRejectedValue('unknown error')
124+
125+
const result = await toggleJellyfinLogin(true)
126+
127+
expect(result).toEqual({
128+
success: false,
129+
error: 'Failed to toggle Jellyfin login setting',
130+
})
131+
})
132+
})
133+
})

actions/admin/admin-servers.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { requireAdmin } from "@/lib/admin"
44
import { prisma } from "@/lib/prisma"
5+
import { z } from "zod"
56

67
/**
78
* Get Jellyfin libraries for invite creation
@@ -569,19 +570,26 @@ export async function deleteJellyfinServer() {
569570
}
570571
}
571572

573+
const toggleJellyfinLoginSchema = z.boolean()
574+
572575
/**
573576
* Toggle Jellyfin login visibility (admin only)
574577
* When disabled, Jellyfin won't appear as a login option on the home page
575578
*/
576579
export async function toggleJellyfinLogin(enabled: boolean) {
577580
await requireAdmin()
578581

582+
const validated = toggleJellyfinLoginSchema.safeParse(enabled)
583+
if (!validated.success) {
584+
return { success: false, error: "Invalid input: enabled must be a boolean" }
585+
}
586+
579587
try {
580588
const { revalidatePath } = await import("next/cache")
581589

582590
await prisma.jellyfinServer.updateMany({
583591
where: { isActive: true },
584-
data: { enabledForLogin: enabled },
592+
data: { enabledForLogin: validated.data },
585593
})
586594

587595
revalidatePath("/admin/settings")

e2e/admin-functionality.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,33 @@ test.describe('Admin Functionality', () => {
8080
// Wait for page content to be visible (not just accessible)
8181
await expect(adminPage.locator('main')).toBeVisible({ timeout: WAIT_TIMEOUTS.ADMIN_CONTENT });
8282
});
83+
84+
test('should toggle Jellyfin login visibility in settings', async ({ adminPage }) => {
85+
// Navigate to settings
86+
await adminPage.locator('aside').getByTestId('admin-nav-settings').first().click();
87+
await waitForAdminContent(adminPage, [
88+
{ type: 'heading', value: 'Settings' }
89+
], { timeout: WAIT_TIMEOUTS.EXTENDED_OPERATION });
90+
91+
// Look for the Jellyfin login toggle - it should only be visible if Jellyfin is configured
92+
const jellyfinToggle = adminPage.getByTestId('jellyfin-login-toggle');
93+
94+
// If Jellyfin is configured, test the toggle functionality
95+
if (await jellyfinToggle.isVisible({ timeout: 5000 }).catch(() => false)) {
96+
// Get current button text
97+
const buttonText = await jellyfinToggle.textContent();
98+
99+
// Click the toggle
100+
await jellyfinToggle.click();
101+
102+
// Wait for the toggle to update (button text should change)
103+
await expect(jellyfinToggle).not.toHaveText(buttonText!, { timeout: WAIT_TIMEOUTS.EXTENDED_OPERATION });
104+
105+
// Click again to restore original state
106+
await jellyfinToggle.click();
107+
108+
// Verify it changed back
109+
await expect(jellyfinToggle).toHaveText(buttonText!, { timeout: WAIT_TIMEOUTS.EXTENDED_OPERATION });
110+
}
111+
});
83112
});

0 commit comments

Comments
 (0)