release: opensource snapshot 2026-02-27 19:25:00
This commit is contained in:
23
tests/helpers/assertions.ts
Normal file
23
tests/helpers/assertions.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { expect } from 'vitest'
|
||||
import { prisma } from './prisma'
|
||||
import { toMoneyNumber } from '@/lib/billing/money'
|
||||
|
||||
export async function expectBalance(userId: string, params: {
|
||||
balance: number
|
||||
frozenAmount: number
|
||||
totalSpent: number
|
||||
}) {
|
||||
const row = await prisma.userBalance.findUnique({ where: { userId } })
|
||||
expect(row).toBeTruthy()
|
||||
expect(toMoneyNumber(row!.balance)).toBeCloseTo(params.balance, 8)
|
||||
expect(toMoneyNumber(row!.frozenAmount)).toBeCloseTo(params.frozenAmount, 8)
|
||||
expect(toMoneyNumber(row!.totalSpent)).toBeCloseTo(params.totalSpent, 8)
|
||||
}
|
||||
|
||||
export async function expectNoNegativeLedger(userId: string) {
|
||||
const row = await prisma.userBalance.findUnique({ where: { userId } })
|
||||
expect(row).toBeTruthy()
|
||||
expect(toMoneyNumber(row!.balance)).toBeGreaterThanOrEqual(0)
|
||||
expect(toMoneyNumber(row!.frozenAmount)).toBeGreaterThanOrEqual(0)
|
||||
expect(toMoneyNumber(row!.totalSpent)).toBeGreaterThanOrEqual(0)
|
||||
}
|
||||
132
tests/helpers/auth.ts
Normal file
132
tests/helpers/auth.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
type SessionUser = {
|
||||
id: string
|
||||
name?: string | null
|
||||
email?: string | null
|
||||
}
|
||||
|
||||
type SessionPayload = {
|
||||
user: SessionUser
|
||||
}
|
||||
|
||||
type MockAuthState = {
|
||||
session: SessionPayload | null
|
||||
projectAuthMode: 'allow' | 'forbidden' | 'not_found'
|
||||
}
|
||||
|
||||
const defaultSession: SessionPayload = {
|
||||
user: {
|
||||
id: 'test-user-id',
|
||||
name: 'test-user',
|
||||
email: 'test@example.com',
|
||||
},
|
||||
}
|
||||
|
||||
let state: MockAuthState = {
|
||||
session: defaultSession,
|
||||
projectAuthMode: 'allow',
|
||||
}
|
||||
|
||||
function unauthorizedResponse() {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
{ status: 401 },
|
||||
)
|
||||
}
|
||||
|
||||
function forbiddenResponse() {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: 'FORBIDDEN',
|
||||
message: 'Forbidden',
|
||||
},
|
||||
},
|
||||
{ status: 403 },
|
||||
)
|
||||
}
|
||||
|
||||
function notFoundResponse() {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Project not found',
|
||||
},
|
||||
},
|
||||
{ status: 404 },
|
||||
)
|
||||
}
|
||||
|
||||
export function installAuthMocks() {
|
||||
vi.doMock('@/lib/api-auth', () => ({
|
||||
isErrorResponse: (value: unknown) => value instanceof NextResponse,
|
||||
requireUserAuth: async () => {
|
||||
if (!state.session) return unauthorizedResponse()
|
||||
return { session: state.session }
|
||||
},
|
||||
requireProjectAuth: async (projectId: string) => {
|
||||
if (!state.session) return unauthorizedResponse()
|
||||
if (state.projectAuthMode === 'forbidden') return forbiddenResponse()
|
||||
if (state.projectAuthMode === 'not_found') return notFoundResponse()
|
||||
return {
|
||||
session: state.session,
|
||||
project: { id: projectId, userId: state.session.user.id, name: 'project' },
|
||||
novelData: { id: 'novel-data-id' },
|
||||
}
|
||||
},
|
||||
requireProjectAuthLight: async (projectId: string) => {
|
||||
if (!state.session) return unauthorizedResponse()
|
||||
if (state.projectAuthMode === 'forbidden') return forbiddenResponse()
|
||||
if (state.projectAuthMode === 'not_found') return notFoundResponse()
|
||||
return {
|
||||
session: state.session,
|
||||
project: { id: projectId, userId: state.session.user.id, name: 'project' },
|
||||
}
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export function mockAuthenticated(userId: string) {
|
||||
state = {
|
||||
...state,
|
||||
session: {
|
||||
user: {
|
||||
...defaultSession.user,
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function mockUnauthenticated() {
|
||||
state = {
|
||||
...state,
|
||||
session: null,
|
||||
}
|
||||
}
|
||||
|
||||
export function mockProjectAuth(mode: 'allow' | 'forbidden' | 'not_found') {
|
||||
state = {
|
||||
...state,
|
||||
projectAuthMode: mode,
|
||||
}
|
||||
}
|
||||
|
||||
export function resetAuthMockState() {
|
||||
state = {
|
||||
session: defaultSession,
|
||||
projectAuthMode: 'allow',
|
||||
}
|
||||
vi.doUnmock('@/lib/api-auth')
|
||||
}
|
||||
68
tests/helpers/billing-fixtures.ts
Normal file
68
tests/helpers/billing-fixtures.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type { TaskBillingInfo, TaskType } from '@/lib/task/types'
|
||||
import { TASK_STATUS } from '@/lib/task/types'
|
||||
import { Prisma } from '@prisma/client'
|
||||
import { prisma } from './prisma'
|
||||
|
||||
export async function createTestUser() {
|
||||
const suffix = randomUUID().slice(0, 8)
|
||||
return await prisma.user.create({
|
||||
data: {
|
||||
name: `billing_user_${suffix}`,
|
||||
email: `billing_${suffix}@example.com`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createTestProject(userId: string) {
|
||||
const suffix = randomUUID().slice(0, 8)
|
||||
return await prisma.project.create({
|
||||
data: {
|
||||
name: `Billing Project ${suffix}`,
|
||||
userId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function seedBalance(userId: string, balance: number) {
|
||||
return await prisma.userBalance.upsert({
|
||||
where: { userId },
|
||||
create: {
|
||||
userId,
|
||||
balance,
|
||||
frozenAmount: 0,
|
||||
totalSpent: 0,
|
||||
},
|
||||
update: {
|
||||
balance,
|
||||
frozenAmount: 0,
|
||||
totalSpent: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createQueuedTask(params: {
|
||||
id: string
|
||||
userId: string
|
||||
projectId: string
|
||||
type: TaskType
|
||||
targetType: string
|
||||
targetId: string
|
||||
billingInfo?: TaskBillingInfo | null
|
||||
payload?: Record<string, unknown> | null
|
||||
}) {
|
||||
return await prisma.task.create({
|
||||
data: {
|
||||
id: params.id,
|
||||
userId: params.userId,
|
||||
projectId: params.projectId,
|
||||
type: params.type,
|
||||
targetType: params.targetType,
|
||||
targetId: params.targetId,
|
||||
status: TASK_STATUS.QUEUED,
|
||||
billingInfo: (params.billingInfo ?? Prisma.JsonNull) as unknown as Prisma.InputJsonValue,
|
||||
payload: (params.payload ?? Prisma.JsonNull) as unknown as Prisma.InputJsonValue,
|
||||
queuedAt: new Date(),
|
||||
},
|
||||
})
|
||||
}
|
||||
60
tests/helpers/db-reset.ts
Normal file
60
tests/helpers/db-reset.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { prisma } from './prisma'
|
||||
|
||||
export async function resetBillingState() {
|
||||
await prisma.balanceTransaction.deleteMany()
|
||||
await prisma.balanceFreeze.deleteMany()
|
||||
await prisma.usageCost.deleteMany()
|
||||
await prisma.taskEvent.deleteMany()
|
||||
await prisma.task.deleteMany()
|
||||
await prisma.userBalance.deleteMany()
|
||||
await prisma.project.deleteMany()
|
||||
await prisma.session.deleteMany()
|
||||
await prisma.account.deleteMany()
|
||||
await prisma.userPreference.deleteMany()
|
||||
await prisma.user.deleteMany()
|
||||
}
|
||||
|
||||
export async function resetTaskState() {
|
||||
await prisma.taskEvent.deleteMany()
|
||||
await prisma.task.deleteMany()
|
||||
}
|
||||
|
||||
export async function resetAssetHubState() {
|
||||
await prisma.globalCharacterAppearance.deleteMany()
|
||||
await prisma.globalCharacter.deleteMany()
|
||||
await prisma.globalLocationImage.deleteMany()
|
||||
await prisma.globalLocation.deleteMany()
|
||||
await prisma.globalVoice.deleteMany()
|
||||
await prisma.globalAssetFolder.deleteMany()
|
||||
}
|
||||
|
||||
export async function resetNovelPromotionState() {
|
||||
await prisma.novelPromotionVoiceLine.deleteMany()
|
||||
await prisma.novelPromotionPanel.deleteMany()
|
||||
await prisma.supplementaryPanel.deleteMany()
|
||||
await prisma.novelPromotionStoryboard.deleteMany()
|
||||
await prisma.novelPromotionShot.deleteMany()
|
||||
await prisma.novelPromotionClip.deleteMany()
|
||||
await prisma.characterAppearance.deleteMany()
|
||||
await prisma.locationImage.deleteMany()
|
||||
await prisma.novelPromotionCharacter.deleteMany()
|
||||
await prisma.novelPromotionLocation.deleteMany()
|
||||
await prisma.videoEditorProject.deleteMany()
|
||||
await prisma.novelPromotionEpisode.deleteMany()
|
||||
await prisma.novelPromotionProject.deleteMany()
|
||||
}
|
||||
|
||||
export async function resetSystemState() {
|
||||
await resetTaskState()
|
||||
await resetAssetHubState()
|
||||
await resetNovelPromotionState()
|
||||
await prisma.usageCost.deleteMany()
|
||||
await prisma.project.deleteMany()
|
||||
await prisma.userPreference.deleteMany()
|
||||
await prisma.account.deleteMany()
|
||||
await prisma.session.deleteMany()
|
||||
await prisma.userBalance.deleteMany()
|
||||
await prisma.balanceFreeze.deleteMany()
|
||||
await prisma.balanceTransaction.deleteMany()
|
||||
await prisma.user.deleteMany()
|
||||
}
|
||||
26
tests/helpers/fakes/llm.ts
Normal file
26
tests/helpers/fakes/llm.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
type CompletionResult = {
|
||||
text: string
|
||||
reasoning?: string
|
||||
}
|
||||
|
||||
const state: { nextText: string; nextReasoning: string } = {
|
||||
nextText: '{"ok":true}',
|
||||
nextReasoning: '',
|
||||
}
|
||||
|
||||
export function configureFakeLLM(result: CompletionResult) {
|
||||
state.nextText = result.text
|
||||
state.nextReasoning = result.reasoning || ''
|
||||
}
|
||||
|
||||
export function resetFakeLLM() {
|
||||
state.nextText = '{"ok":true}'
|
||||
state.nextReasoning = ''
|
||||
}
|
||||
|
||||
export async function fakeChatCompletion() {
|
||||
return {
|
||||
output_text: state.nextText,
|
||||
reasoning: state.nextReasoning,
|
||||
}
|
||||
}
|
||||
37
tests/helpers/fakes/media.ts
Normal file
37
tests/helpers/fakes/media.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
const state: {
|
||||
nextImageUrl: string
|
||||
nextVideoUrl: string
|
||||
nextAudioUrl: string
|
||||
} = {
|
||||
nextImageUrl: 'images/fake-image.jpg',
|
||||
nextVideoUrl: 'video/fake-video.mp4',
|
||||
nextAudioUrl: 'voice/fake-audio.mp3',
|
||||
}
|
||||
|
||||
export function configureFakeMedia(params: {
|
||||
imageUrl?: string
|
||||
videoUrl?: string
|
||||
audioUrl?: string
|
||||
}) {
|
||||
if (params.imageUrl) state.nextImageUrl = params.imageUrl
|
||||
if (params.videoUrl) state.nextVideoUrl = params.videoUrl
|
||||
if (params.audioUrl) state.nextAudioUrl = params.audioUrl
|
||||
}
|
||||
|
||||
export function resetFakeMedia() {
|
||||
state.nextImageUrl = 'images/fake-image.jpg'
|
||||
state.nextVideoUrl = 'video/fake-video.mp4'
|
||||
state.nextAudioUrl = 'voice/fake-audio.mp3'
|
||||
}
|
||||
|
||||
export async function fakeGenerateImage() {
|
||||
return { success: true, imageUrl: state.nextImageUrl }
|
||||
}
|
||||
|
||||
export async function fakeGenerateVideo() {
|
||||
return { success: true, videoUrl: state.nextVideoUrl }
|
||||
}
|
||||
|
||||
export async function fakeGenerateAudio() {
|
||||
return { success: true, audioUrl: state.nextAudioUrl }
|
||||
}
|
||||
35
tests/helpers/fakes/providers.ts
Normal file
35
tests/helpers/fakes/providers.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
const providerState: {
|
||||
falApiKey: string
|
||||
googleApiKey: string
|
||||
openrouterApiKey: string
|
||||
} = {
|
||||
falApiKey: 'fake-fal-key',
|
||||
googleApiKey: 'fake-google-key',
|
||||
openrouterApiKey: 'fake-openrouter-key',
|
||||
}
|
||||
|
||||
export function configureFakeProviders(params: {
|
||||
falApiKey?: string
|
||||
googleApiKey?: string
|
||||
openrouterApiKey?: string
|
||||
}) {
|
||||
if (params.falApiKey) providerState.falApiKey = params.falApiKey
|
||||
if (params.googleApiKey) providerState.googleApiKey = params.googleApiKey
|
||||
if (params.openrouterApiKey) providerState.openrouterApiKey = params.openrouterApiKey
|
||||
}
|
||||
|
||||
export function resetFakeProviders() {
|
||||
providerState.falApiKey = 'fake-fal-key'
|
||||
providerState.googleApiKey = 'fake-google-key'
|
||||
providerState.openrouterApiKey = 'fake-openrouter-key'
|
||||
}
|
||||
|
||||
export function getFakeProviderConfig(provider: 'fal' | 'google' | 'openrouter') {
|
||||
if (provider === 'fal') {
|
||||
return { apiKey: providerState.falApiKey }
|
||||
}
|
||||
if (provider === 'google') {
|
||||
return { apiKey: providerState.googleApiKey }
|
||||
}
|
||||
return { apiKey: providerState.openrouterApiKey }
|
||||
}
|
||||
99
tests/helpers/fixtures.ts
Normal file
99
tests/helpers/fixtures.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { prisma } from './prisma'
|
||||
|
||||
function suffix() {
|
||||
return randomUUID().slice(0, 8)
|
||||
}
|
||||
|
||||
export async function createFixtureUser() {
|
||||
const id = suffix()
|
||||
return await prisma.user.create({
|
||||
data: {
|
||||
name: `user_${id}`,
|
||||
email: `user_${id}@example.com`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createFixtureProject(userId: string, mode: 'novel-promotion' | 'general' = 'novel-promotion') {
|
||||
const id = suffix()
|
||||
return await prisma.project.create({
|
||||
data: {
|
||||
userId,
|
||||
mode,
|
||||
name: `project_${id}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createFixtureNovelProject(projectId: string) {
|
||||
return await prisma.novelPromotionProject.create({
|
||||
data: {
|
||||
projectId,
|
||||
analysisModel: 'openrouter::anthropic/claude-sonnet-4',
|
||||
characterModel: 'fal::banana/character',
|
||||
locationModel: 'fal::banana/location',
|
||||
storyboardModel: 'fal::banana/storyboard',
|
||||
editModel: 'fal::banana/edit',
|
||||
videoModel: 'fal::seedance/video',
|
||||
videoRatio: '9:16',
|
||||
imageResolution: '2K',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createFixtureGlobalCharacter(userId: string, folderId: string | null = null) {
|
||||
const id = suffix()
|
||||
return await prisma.globalCharacter.create({
|
||||
data: {
|
||||
userId,
|
||||
name: `character_${id}`,
|
||||
...(folderId ? { folderId } : {}),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createFixtureGlobalCharacterAppearance(characterId: string, appearanceIndex = 0) {
|
||||
return await prisma.globalCharacterAppearance.create({
|
||||
data: {
|
||||
characterId,
|
||||
appearanceIndex,
|
||||
changeReason: 'default',
|
||||
imageUrls: JSON.stringify(['images/test-0.jpg']),
|
||||
selectedIndex: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createFixtureGlobalLocation(userId: string, folderId: string | null = null) {
|
||||
const id = suffix()
|
||||
return await prisma.globalLocation.create({
|
||||
data: {
|
||||
userId,
|
||||
name: `location_${id}`,
|
||||
...(folderId ? { folderId } : {}),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createFixtureGlobalLocationImage(locationId: string, imageIndex = 0) {
|
||||
return await prisma.globalLocationImage.create({
|
||||
data: {
|
||||
locationId,
|
||||
imageIndex,
|
||||
imageUrl: `images/location-${suffix()}.jpg`,
|
||||
isSelected: imageIndex === 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createFixtureEpisode(novelPromotionProjectId: string, episodeNumber = 1) {
|
||||
return await prisma.novelPromotionEpisode.create({
|
||||
data: {
|
||||
novelPromotionProjectId,
|
||||
episodeNumber,
|
||||
name: `Episode ${episodeNumber}`,
|
||||
novelText: 'test novel text',
|
||||
},
|
||||
})
|
||||
}
|
||||
72
tests/helpers/mock-query-client.ts
Normal file
72
tests/helpers/mock-query-client.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { QueryKey } from '@tanstack/react-query'
|
||||
|
||||
interface QueryFilter {
|
||||
queryKey: QueryKey
|
||||
exact?: boolean
|
||||
}
|
||||
|
||||
type Updater<T> = T | ((previous: T | undefined) => T | undefined)
|
||||
|
||||
interface StoredQueryEntry {
|
||||
queryKey: QueryKey
|
||||
data: unknown
|
||||
}
|
||||
|
||||
function isPrefixQueryKey(target: QueryKey, prefix: QueryKey): boolean {
|
||||
if (prefix.length > target.length) return false
|
||||
return prefix.every((value, index) => Object.is(value, target[index]))
|
||||
}
|
||||
|
||||
function keyOf(queryKey: QueryKey): string {
|
||||
return JSON.stringify(queryKey)
|
||||
}
|
||||
|
||||
export class MockQueryClient {
|
||||
private readonly queryMap = new Map<string, StoredQueryEntry>()
|
||||
|
||||
async cancelQueries(filters: QueryFilter): Promise<void> {
|
||||
void filters
|
||||
}
|
||||
|
||||
seedQuery<T>(queryKey: QueryKey, data: T | undefined) {
|
||||
this.queryMap.set(keyOf(queryKey), {
|
||||
queryKey,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
getQueryData<T>(queryKey: QueryKey): T | undefined {
|
||||
const entry = this.queryMap.get(keyOf(queryKey))
|
||||
return entry?.data as T | undefined
|
||||
}
|
||||
|
||||
setQueryData<T>(queryKey: QueryKey, updater: Updater<T | undefined>) {
|
||||
const previous = this.getQueryData<T>(queryKey)
|
||||
const next = typeof updater === 'function'
|
||||
? (updater as (prev: T | undefined) => T | undefined)(previous)
|
||||
: updater
|
||||
this.seedQuery(queryKey, next)
|
||||
}
|
||||
|
||||
getQueriesData<T>(filters: QueryFilter): Array<[QueryKey, T | undefined]> {
|
||||
const matched: Array<[QueryKey, T | undefined]> = []
|
||||
for (const { queryKey, data } of this.queryMap.values()) {
|
||||
const isMatch = filters.exact
|
||||
? keyOf(filters.queryKey) === keyOf(queryKey)
|
||||
: isPrefixQueryKey(queryKey, filters.queryKey)
|
||||
if (!isMatch) continue
|
||||
matched.push([queryKey, data as T | undefined])
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
setQueriesData<T>(
|
||||
filters: QueryFilter,
|
||||
updater: (previous: T | undefined) => T | undefined,
|
||||
) {
|
||||
const matches = this.getQueriesData<T>(filters)
|
||||
matches.forEach(([queryKey, previous]) => {
|
||||
this.seedQuery(queryKey, updater(previous))
|
||||
})
|
||||
}
|
||||
}
|
||||
6
tests/helpers/prisma.ts
Normal file
6
tests/helpers/prisma.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { loadTestEnv } from '../setup/env'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
loadTestEnv()
|
||||
|
||||
export { prisma }
|
||||
62
tests/helpers/request.ts
Normal file
62
tests/helpers/request.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
|
||||
type HeaderMap = Record<string, string>
|
||||
type QueryMap = Record<string, string | number | boolean>
|
||||
|
||||
function toJsonBody(body: unknown): string | undefined {
|
||||
if (body === undefined) return undefined
|
||||
return JSON.stringify(body)
|
||||
}
|
||||
|
||||
function appendQuery(url: URL, query?: QueryMap) {
|
||||
if (!query) return
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
url.searchParams.set(key, String(value))
|
||||
}
|
||||
}
|
||||
|
||||
export function buildMockRequest(params: {
|
||||
path: string
|
||||
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'
|
||||
body?: unknown
|
||||
headers?: HeaderMap
|
||||
query?: QueryMap
|
||||
}) {
|
||||
const url = new URL(params.path, 'http://localhost:3000')
|
||||
appendQuery(url, params.query)
|
||||
const jsonBody = toJsonBody(params.body)
|
||||
|
||||
const headers: HeaderMap = {
|
||||
...(params.headers || {}),
|
||||
}
|
||||
if (jsonBody !== undefined && !headers['content-type']) {
|
||||
headers['content-type'] = 'application/json'
|
||||
}
|
||||
|
||||
return new NextRequest(url, {
|
||||
method: params.method,
|
||||
headers,
|
||||
...(jsonBody !== undefined ? { body: jsonBody } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
export async function callRoute<TContext>(
|
||||
handler: (req: NextRequest, ctx: TContext) => Promise<Response>,
|
||||
params: {
|
||||
path: string
|
||||
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'
|
||||
body?: unknown
|
||||
headers?: HeaderMap
|
||||
query?: QueryMap
|
||||
context: TContext
|
||||
},
|
||||
) {
|
||||
const req = buildMockRequest({
|
||||
path: params.path,
|
||||
method: params.method,
|
||||
body: params.body,
|
||||
headers: params.headers,
|
||||
query: params.query,
|
||||
})
|
||||
return await handler(req, params.context)
|
||||
}
|
||||
Reference in New Issue
Block a user