Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 92 additions & 2 deletions __tests__/email/admin-user-update.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { POST, PUT } from '@/app/api/users/route'
import { GET, POST, PUT } from '@/app/api/users/route'
import type { User } from '@prisma/client'
import { beforeEach, describe, expect, it, vi } from 'vitest'

Expand All @@ -14,7 +14,7 @@ const mocks = vi.hoisted(() => ({
createUser: vi.fn(),
invalidateEmailTokens: vi.fn(),
db: {
user: { findUnique: vi.fn() },
user: { findUnique: vi.fn(), findMany: vi.fn(), count: vi.fn() },
$transaction: vi.fn(),
},
tx: {
Expand Down Expand Up @@ -250,3 +250,93 @@ describe('administrator account update serialization', () => {
expect(mocks.tx.user.update).not.toHaveBeenCalled()
})
})

describe('user directory', () => {
it('keeps directory search behind the administrator guard', async () => {
mocks.requireAdmin.mockResolvedValue({
response: new Response(null, { status: 403 }),
})
const response = await GET(
new Request('https://flare.example/api/users?search=person')
)
expect(response.status).toBe(403)
expect(mocks.db.user.findMany).not.toHaveBeenCalled()
expect(mocks.db.user.count).not.toHaveBeenCalled()
})

it('searches the complete directory with matching counts and safe public fields', async () => {
mocks.db.user.count.mockResolvedValue(26)
mocks.db.user.findMany.mockResolvedValue([])
const response = await GET(
new Request(
'https://flare.example/api/users?search=%20Jordan%20&role=USER&page=2&limit=25'
)
)
const body = await response.json()
expect(body.pagination).toEqual({
total: 26,
pageCount: 2,
page: 2,
limit: 25,
})
const query = mocks.db.user.findMany.mock.calls[0][0]
expect(query.where).toEqual({
role: 'USER',
OR: [
{ name: { contains: 'Jordan', mode: 'insensitive' } },
{ email: { contains: 'Jordan', mode: 'insensitive' } },
],
})
expect(mocks.db.user.count).toHaveBeenCalledWith({ where: query.where })
expect(query.skip).toBe(25)
expect(query.take).toBe(25)
expect(query.select.password).toBeUndefined()
expect(query.select.uploadToken).toBeUndefined()
})

it.each([25, 0])(
'returns to the first page after the last row on page 2 is removed (total %s)',
async (total) => {
mocks.db.user.count.mockResolvedValue(total)
mocks.db.user.findMany.mockResolvedValue([])
const response = await GET(
new Request('https://flare.example/api/users?page=2&limit=25')
)
expect(response.status).toBe(200)
expect((await response.json()).pagination.page).toBe(1)
expect(mocks.db.user.findMany).toHaveBeenCalledWith(
expect.objectContaining({ skip: 0, take: 25 })
)
}
)

it('bounds an oversized requested page before calculating its database offset', async () => {
mocks.db.user.count.mockResolvedValue(26)
mocks.db.user.findMany.mockResolvedValue([])
const response = await GET(
new Request(
'https://flare.example/api/users?page=9007199254740991&limit=25'
)
)
expect(response.status).toBe(200)
expect((await response.json()).pagination.page).toBe(2)
expect(mocks.db.user.findMany).toHaveBeenCalledWith(
expect.objectContaining({ skip: 25, take: 25 })
)
})

it.each(['page=NaN&limit=-4', 'page=-2&limit=Infinity'])(
'recovers malformed pagination: %s',
async (query) => {
mocks.db.user.count.mockResolvedValue(0)
mocks.db.user.findMany.mockResolvedValue([])
const response = await GET(
new Request(`https://flare.example/api/users?${query}`)
)
expect(response.status).toBe(200)
expect(mocks.db.user.findMany).toHaveBeenCalledWith(
expect.objectContaining({ skip: 0, take: 25 })
)
}
)
})
73 changes: 73 additions & 0 deletions __tests__/files/direct-video-url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { GET } from '@/app/(raw)/[userUrlId]/[filename]/direct/route'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const mocks = vi.hoisted(() => ({
session: vi.fn(),
resolve: vi.fn(),
findUnique: vi.fn(),
access: vi.fn(),
publicUrl: vi.fn(),
}))

vi.mock('@/lib/auth', () => ({ getAccessSession: mocks.session }))
vi.mock('@/lib/database/prisma', () => ({
prisma: { file: { findUnique: mocks.findUnique } },
}))
vi.mock('@/lib/files/access', () => ({ checkFileAccess: mocks.access }))
vi.mock('@/lib/files/resolve', () => ({ resolveFileUrlPath: mocks.resolve }))
vi.mock('@/lib/storage', () => ({
getStorageProvider: async () => ({ getPublicUrl: mocks.publicUrl }),
}))

beforeEach(() => {
vi.clearAllMocks()
mocks.session.mockResolvedValue(null)
mocks.resolve.mockResolvedValue('/owner/preview.webm')
mocks.findUnique.mockResolvedValue({
urlPath: '/owner/preview.webm',
path: 'preview.webm',
mimeType: 'video/webm',
})
mocks.access.mockResolvedValue({ allowed: true })
mocks.publicUrl.mockResolvedValue(null)
})

describe('direct video playback URL', () => {
it('preserves special characters in a protected local video password', async () => {
const password = 'watch+share&safe?<>#100%'
const query = new URLSearchParams({ password })
const response = await GET(
new Request(`https://flare.example/owner/preview.webm/direct?${query}`),
{
params: Promise.resolve({
userUrlId: 'owner',
filename: 'preview.webm',
}),
}
)
expect(response.status).toBe(200)
const playback = new URL(
(await response.json()).url,
'https://flare.example'
)
expect(playback.pathname).toBe('/owner/preview.webm/raw')
expect([...playback.searchParams]).toEqual([['password', password]])
expect(playback.hash).toBe('')
expect(mocks.access).toHaveBeenCalledWith(expect.anything(), null, password)
})

it('does not disclose a playback URL when file access is denied', async () => {
mocks.access.mockResolvedValue({ allowed: false, status: 403 })
const response = await GET(
new Request('https://flare.example/owner/preview.webm/direct'),
{
params: Promise.resolve({
userUrlId: 'owner',
filename: 'preview.webm',
}),
}
)
expect(response.status).toBe(403)
expect(mocks.publicUrl).not.toHaveBeenCalled()
})
})
73 changes: 73 additions & 0 deletions __tests__/files/library-date-range.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { GET } from '@/app/api/files/route'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const mocks = vi.hoisted(() => ({
requireAuth: vi.fn(),
file: { count: vi.fn(), findMany: vi.fn() },
}))

vi.mock('@/lib/auth/api-auth', () => ({ requireAuth: mocks.requireAuth }))
vi.mock('@/lib/database/prisma', () => ({ prisma: { file: mocks.file } }))
vi.mock('@/lib/logger', () => ({ loggers: { files: { error: vi.fn() } } }))
vi.mock('@/lib/config', () => ({ getConfig: vi.fn() }))
vi.mock('@/lib/events/handlers/file-expiry', () => ({
getFileExpirationInfo: vi.fn(),
}))
vi.mock('@/lib/files/streaming-upload', () => ({
parseSingleFileUpload: vi.fn(),
}))
vi.mock('@/lib/security/rate-limit', () => ({
rateLimit: vi.fn(),
uploadLimiter: {},
}))
vi.mock('@/lib/storage', () => ({ getStorageProvider: vi.fn() }))
vi.mock('@/lib/uploads/finalize', () => ({
cleanupUncommittedUpload: vi.fn(),
finalizeUpload: vi.fn(),
prepareUploadDestination: vi.fn(),
}))
vi.mock('@/lib/uploads/links', () => ({ uploadLinks: vi.fn() }))
vi.mock('@/lib/uploads/options', () => ({}))

beforeEach(() => {
vi.clearAllMocks()
mocks.requireAuth.mockResolvedValue({ user: { id: 'library-owner' } })
mocks.file.count.mockResolvedValue(0)
mocks.file.findMany.mockResolvedValue([])
})

describe('file library date boundaries', () => {
it('keeps date-only API requests inclusive of the final day', async () => {
const response = await GET(
new Request('https://flare.example/api/files?dateTo=2026-09-13')
)
const end = new Date('2026-09-13')
end.setHours(23, 59, 59, 999)
expect(response.status).toBe(200)
expect(mocks.file.count).toHaveBeenCalledWith({
where: { userId: 'library-owner', AND: [{ uploadedAt: { lte: end } }] },
})
})
it.each([
['UTC', '2026-09-13T00:00:00.000Z', '2026-09-13T23:59:59.999Z'],
['UTC+14', '2026-09-12T10:00:00.000Z', '2026-09-13T09:59:59.999Z'],
['UTC-7', '2026-09-13T07:00:00.000Z', '2026-09-14T06:59:59.999Z'],
])(
'preserves the selected local day for a user in %s',
async (_timezone, start, end) => {
const query = new URLSearchParams({ dateFrom: start, dateTo: end })
const response = await GET(
new Request(`https://flare.example/api/files?${query}`)
)
expect(response.status).toBe(200)
const expectedWhere = {
userId: 'library-owner',
AND: [{ uploadedAt: { gte: new Date(start), lte: new Date(end) } }],
}
expect(mocks.file.count).toHaveBeenCalledWith({ where: expectedWhere })
expect(mocks.file.findMany).toHaveBeenCalledWith(
expect.objectContaining({ where: expectedWhere })
)
}
)
})
Loading
Loading