feat: add props system and refactor asset library architecture
This commit is contained in:
@@ -40,6 +40,16 @@ const prismaMock = vi.hoisted(() => ({
|
||||
novelPromotionClip: {
|
||||
update: vi.fn(),
|
||||
},
|
||||
novelPromotionStoryboard: {
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
novelPromotionPanel: {
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
create: vi.fn(),
|
||||
count: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/api-auth', () => {
|
||||
@@ -158,14 +168,18 @@ async function invokeRouteMethod(
|
||||
const req = buildMockRequest({
|
||||
path,
|
||||
method,
|
||||
...(method === 'GET' || method === 'DELETE' ? {} : { body: buildGenericBody() }),
|
||||
...(method === 'GET' ? {} : { body: buildGenericBody() }),
|
||||
})
|
||||
return await handler(req, { params: Promise.resolve(params) })
|
||||
}
|
||||
|
||||
describe('api contract - crud routes (behavior)', () => {
|
||||
const routes = ROUTE_CATALOG.filter(
|
||||
(entry) => entry.contractGroup === 'crud-asset-hub-routes' || entry.contractGroup === 'crud-novel-promotion-routes',
|
||||
(entry) => (
|
||||
entry.contractGroup === 'crud-assets-routes'
|
||||
|| entry.contractGroup === 'crud-asset-hub-routes'
|
||||
|| entry.contractGroup === 'crud-novel-promotion-routes'
|
||||
),
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -223,9 +237,36 @@ describe('api contract - crud routes (behavior)', () => {
|
||||
id: 'clip-1',
|
||||
characters: JSON.stringify(['Alice']),
|
||||
location: 'Old Town',
|
||||
props: JSON.stringify(['Bronze Dagger']),
|
||||
content: 'clip content',
|
||||
screenplay: JSON.stringify({ scenes: [{ id: 1 }] }),
|
||||
})
|
||||
prismaMock.novelPromotionStoryboard.findUnique.mockResolvedValue({
|
||||
id: 'storyboard-1',
|
||||
projectId: 'project-1',
|
||||
})
|
||||
prismaMock.novelPromotionStoryboard.update.mockResolvedValue({
|
||||
id: 'storyboard-1',
|
||||
panelCount: 1,
|
||||
})
|
||||
prismaMock.novelPromotionPanel.findUnique.mockResolvedValue({
|
||||
id: 'panel-1',
|
||||
storyboardId: 'storyboard-1',
|
||||
panelIndex: 0,
|
||||
})
|
||||
prismaMock.novelPromotionPanel.update.mockResolvedValue({
|
||||
id: 'panel-1',
|
||||
storyboardId: 'storyboard-1',
|
||||
panelIndex: 0,
|
||||
props: JSON.stringify(['Bronze Dagger']),
|
||||
})
|
||||
prismaMock.novelPromotionPanel.create.mockResolvedValue({
|
||||
id: 'panel-2',
|
||||
storyboardId: 'storyboard-1',
|
||||
panelIndex: 1,
|
||||
props: JSON.stringify(['Bronze Dagger']),
|
||||
})
|
||||
prismaMock.novelPromotionPanel.count.mockResolvedValue(1)
|
||||
})
|
||||
|
||||
it('crud route group exists', () => {
|
||||
@@ -329,11 +370,9 @@ describe('api contract - crud routes (behavior)', () => {
|
||||
},
|
||||
})
|
||||
|
||||
const payload = await res.json() as { success: boolean; selectedIndex: number; imageUrl: string }
|
||||
const payload = await res.json() as { success: boolean }
|
||||
expect(payload).toEqual({
|
||||
success: true,
|
||||
selectedIndex: 1,
|
||||
imageUrl: 'https://signed.example/cos/char-1.png',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -374,6 +413,7 @@ describe('api contract - crud routes (behavior)', () => {
|
||||
body: {
|
||||
characters: JSON.stringify(['Alice']),
|
||||
location: 'Old Town',
|
||||
props: JSON.stringify(['Bronze Dagger']),
|
||||
content: 'clip content',
|
||||
screenplay: JSON.stringify({ scenes: [{ id: 1 }] }),
|
||||
},
|
||||
@@ -388,9 +428,42 @@ describe('api contract - crud routes (behavior)', () => {
|
||||
data: {
|
||||
characters: JSON.stringify(['Alice']),
|
||||
location: 'Old Town',
|
||||
props: JSON.stringify(['Bronze Dagger']),
|
||||
content: 'clip content',
|
||||
screenplay: JSON.stringify({ scenes: [{ id: 1 }] }),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('PUT /novel-promotion/[projectId]/panel writes provided props to prisma.novelPromotionPanel.update', async () => {
|
||||
authState.authenticated = true
|
||||
const mod = await import('@/app/api/novel-promotion/[projectId]/panel/route')
|
||||
const req = buildMockRequest({
|
||||
path: '/api/novel-promotion/project-1/panel',
|
||||
method: 'PUT',
|
||||
body: {
|
||||
storyboardId: 'storyboard-1',
|
||||
panelIndex: 0,
|
||||
location: 'Old Town',
|
||||
characters: JSON.stringify(['Alice']),
|
||||
props: JSON.stringify(['Bronze Dagger']),
|
||||
description: 'panel description',
|
||||
},
|
||||
})
|
||||
|
||||
const res = await mod.PUT(req, {
|
||||
params: Promise.resolve({ projectId: 'project-1' }),
|
||||
})
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(prismaMock.novelPromotionPanel.update).toHaveBeenCalledWith({
|
||||
where: { id: 'panel-1' },
|
||||
data: {
|
||||
location: 'Old Town',
|
||||
characters: JSON.stringify(['Alice']),
|
||||
props: JSON.stringify(['Bronze Dagger']),
|
||||
description: 'panel description',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -288,11 +288,12 @@ vi.mock('@/lib/prisma', () => ({
|
||||
prisma: prismaMock,
|
||||
}))
|
||||
|
||||
function toApiPath(routeFile: string): string {
|
||||
function toApiPath(routeFile: string, params?: Record<string, string>): string {
|
||||
return routeFile
|
||||
.replace(/^src\/app/, '')
|
||||
.replace(/\/route\.ts$/, '')
|
||||
.replace('[projectId]', 'project-1')
|
||||
.replace('[projectId]', params?.projectId || 'project-1')
|
||||
.replace('[assetId]', params?.assetId || 'asset-1')
|
||||
}
|
||||
|
||||
function toModuleImportPath(routeFile: string): string {
|
||||
@@ -321,6 +322,62 @@ const DIRECT_CASES: ReadonlyArray<DirectRouteCase> = [
|
||||
expectedTargetType: 'GlobalCharacterAppearance',
|
||||
expectedProjectId: 'global-asset-hub',
|
||||
},
|
||||
{
|
||||
routeFile: 'src/app/api/assets/[assetId]/generate/route.ts',
|
||||
body: {
|
||||
scope: 'global',
|
||||
kind: 'character',
|
||||
appearanceIndex: 0,
|
||||
artStyle: 'realistic',
|
||||
},
|
||||
params: { assetId: 'global-character-1' },
|
||||
expectedTaskType: TASK_TYPE.ASSET_HUB_IMAGE,
|
||||
expectedTargetType: 'GlobalCharacter',
|
||||
expectedProjectId: 'global-asset-hub',
|
||||
},
|
||||
{
|
||||
routeFile: 'src/app/api/assets/[assetId]/generate/route.ts',
|
||||
body: {
|
||||
scope: 'project',
|
||||
kind: 'character',
|
||||
projectId: 'project-1',
|
||||
appearanceId: 'appearance-1',
|
||||
},
|
||||
params: { assetId: 'character-1' },
|
||||
expectedTaskType: TASK_TYPE.IMAGE_CHARACTER,
|
||||
expectedTargetType: 'CharacterAppearance',
|
||||
expectedProjectId: 'project-1',
|
||||
},
|
||||
{
|
||||
routeFile: 'src/app/api/assets/[assetId]/modify-render/route.ts',
|
||||
body: {
|
||||
scope: 'global',
|
||||
kind: 'character',
|
||||
modifyPrompt: 'sharpen details',
|
||||
appearanceIndex: 0,
|
||||
imageIndex: 0,
|
||||
extraImageUrls: ['https://example.com/ref-a.png'],
|
||||
},
|
||||
params: { assetId: 'global-character-1' },
|
||||
expectedTaskType: TASK_TYPE.ASSET_HUB_MODIFY,
|
||||
expectedTargetType: 'GlobalCharacterAppearance',
|
||||
expectedProjectId: 'global-asset-hub',
|
||||
},
|
||||
{
|
||||
routeFile: 'src/app/api/assets/[assetId]/modify-render/route.ts',
|
||||
body: {
|
||||
scope: 'project',
|
||||
kind: 'character',
|
||||
projectId: 'project-1',
|
||||
appearanceId: 'appearance-1',
|
||||
modifyPrompt: 'enhance texture',
|
||||
extraImageUrls: ['https://example.com/ref-b.png'],
|
||||
},
|
||||
params: { assetId: 'character-1' },
|
||||
expectedTaskType: TASK_TYPE.MODIFY_ASSET_IMAGE,
|
||||
expectedTargetType: 'CharacterAppearance',
|
||||
expectedProjectId: 'project-1',
|
||||
},
|
||||
{
|
||||
routeFile: 'src/app/api/asset-hub/voice-design/route.ts',
|
||||
body: { voicePrompt: 'female calm narrator', previewText: '你好世界' },
|
||||
@@ -466,7 +523,7 @@ async function invokePostRoute(routeCase: DirectRouteCase): Promise<Response> {
|
||||
const mod = await import(modulePath)
|
||||
const post = mod.POST as (request: Request, context?: RouteContext) => Promise<Response>
|
||||
const req = buildMockRequest({
|
||||
path: toApiPath(routeCase.routeFile),
|
||||
path: toApiPath(routeCase.routeFile, routeCase.params),
|
||||
method: 'POST',
|
||||
body: routeCase.body,
|
||||
})
|
||||
@@ -486,7 +543,7 @@ describe('api contract - direct submit routes (behavior)', () => {
|
||||
})
|
||||
|
||||
it('keeps expected coverage size', () => {
|
||||
expect(DIRECT_CASES.length).toBe(16)
|
||||
expect(DIRECT_CASES.length).toBe(20)
|
||||
})
|
||||
|
||||
for (const routeCase of DIRECT_CASES) {
|
||||
|
||||
374
tests/integration/api/specific/assets-route.test.ts
Normal file
374
tests/integration/api/specific/assets-route.test.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { buildMockRequest } from '../../../helpers/request'
|
||||
|
||||
const authMock = vi.hoisted(() => ({
|
||||
requireUserAuth: vi.fn(async () => ({
|
||||
session: { user: { id: 'user-1' } },
|
||||
})),
|
||||
requireProjectAuth: vi.fn(async () => ({
|
||||
session: { user: { id: 'user-1' } },
|
||||
project: { id: 'project-1' },
|
||||
})),
|
||||
requireProjectAuthLight: vi.fn(async () => ({
|
||||
session: { user: { id: 'user-1' } },
|
||||
project: { id: 'project-1' },
|
||||
})),
|
||||
isErrorResponse: vi.fn((value: unknown) => value instanceof Response),
|
||||
}))
|
||||
|
||||
const readAssetsMock = vi.hoisted(() => vi.fn())
|
||||
const updateAssetRenderLabelMock = vi.hoisted(() => vi.fn())
|
||||
const submitAssetGenerateTaskMock = vi.hoisted(() => vi.fn())
|
||||
const copyAssetFromGlobalMock = vi.hoisted(() => vi.fn())
|
||||
const createAssetMock = vi.hoisted(() => vi.fn())
|
||||
const updateAssetMock = vi.hoisted(() => vi.fn())
|
||||
const removeAssetMock = vi.hoisted(() => vi.fn())
|
||||
const updateAssetVariantMock = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/lib/api-auth', () => authMock)
|
||||
vi.mock('@/lib/assets/services/read-assets', () => ({
|
||||
readAssets: readAssetsMock,
|
||||
}))
|
||||
vi.mock('@/lib/assets/services/asset-label', () => ({
|
||||
updateAssetRenderLabel: updateAssetRenderLabelMock,
|
||||
}))
|
||||
vi.mock('@/lib/assets/services/asset-actions', () => ({
|
||||
createAsset: createAssetMock,
|
||||
submitAssetGenerateTask: submitAssetGenerateTaskMock,
|
||||
copyAssetFromGlobal: copyAssetFromGlobalMock,
|
||||
updateAsset: updateAssetMock,
|
||||
removeAsset: removeAssetMock,
|
||||
updateAssetVariant: updateAssetVariantMock,
|
||||
submitAssetModifyTask: vi.fn(),
|
||||
selectAssetRender: vi.fn(),
|
||||
revertAssetRender: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('api specific - unified assets routes', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
readAssetsMock.mockResolvedValue([{ id: 'asset-1', kind: 'character' }])
|
||||
updateAssetRenderLabelMock.mockResolvedValue(undefined)
|
||||
submitAssetGenerateTaskMock.mockResolvedValue({ success: true, taskId: 'task-1' })
|
||||
copyAssetFromGlobalMock.mockResolvedValue({ success: true })
|
||||
createAssetMock.mockResolvedValue({ success: true, assetId: 'prop-1' })
|
||||
updateAssetMock.mockResolvedValue({ success: true })
|
||||
removeAssetMock.mockResolvedValue({ success: true })
|
||||
updateAssetVariantMock.mockResolvedValue({ success: true })
|
||||
})
|
||||
|
||||
it('GET /api/assets reads global assets with the authenticated user scope', async () => {
|
||||
const mod = await import('@/app/api/assets/route')
|
||||
const req = buildMockRequest({
|
||||
path: '/api/assets?scope=global&kind=character',
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
const res = await mod.GET(req, { params: Promise.resolve({}) })
|
||||
const body = await res.json()
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(authMock.requireUserAuth).toHaveBeenCalled()
|
||||
expect(readAssetsMock).toHaveBeenCalledWith({
|
||||
scope: 'global',
|
||||
projectId: null,
|
||||
folderId: null,
|
||||
kind: 'character',
|
||||
}, {
|
||||
userId: 'user-1',
|
||||
})
|
||||
expect(body).toEqual({ assets: [{ id: 'asset-1', kind: 'character' }] })
|
||||
})
|
||||
|
||||
it('GET /api/assets reads prop assets through the unified filter contract', async () => {
|
||||
readAssetsMock.mockResolvedValue([{ id: 'prop-1', kind: 'prop' }])
|
||||
const mod = await import('@/app/api/assets/route')
|
||||
const req = buildMockRequest({
|
||||
path: '/api/assets?scope=project&projectId=project-1&kind=prop',
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
const res = await mod.GET(req, { params: Promise.resolve({}) })
|
||||
const body = await res.json()
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(authMock.requireProjectAuthLight).toHaveBeenCalledWith('project-1')
|
||||
expect(readAssetsMock).toHaveBeenCalledWith({
|
||||
scope: 'project',
|
||||
projectId: 'project-1',
|
||||
folderId: null,
|
||||
kind: 'prop',
|
||||
})
|
||||
expect(body).toEqual({ assets: [{ id: 'prop-1', kind: 'prop' }] })
|
||||
})
|
||||
|
||||
it('POST /api/assets creates a project prop through the centralized asset action service', async () => {
|
||||
const mod = await import('@/app/api/assets/route')
|
||||
const req = buildMockRequest({
|
||||
path: '/api/assets',
|
||||
method: 'POST',
|
||||
body: {
|
||||
scope: 'project',
|
||||
kind: 'prop',
|
||||
projectId: 'project-1',
|
||||
name: '青铜匕首',
|
||||
summary: '古旧短刃,雕纹手柄',
|
||||
},
|
||||
})
|
||||
|
||||
const res = await mod.POST(req, { params: Promise.resolve({}) })
|
||||
const body = await res.json()
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(createAssetMock).toHaveBeenCalledWith({
|
||||
kind: 'prop',
|
||||
body: {
|
||||
scope: 'project',
|
||||
kind: 'prop',
|
||||
projectId: 'project-1',
|
||||
name: '青铜匕首',
|
||||
summary: '古旧短刃,雕纹手柄',
|
||||
},
|
||||
access: {
|
||||
scope: 'project',
|
||||
userId: 'user-1',
|
||||
projectId: 'project-1',
|
||||
},
|
||||
})
|
||||
expect(body).toEqual({ success: true, assetId: 'prop-1' })
|
||||
})
|
||||
|
||||
it('POST /api/assets/[assetId]/update-label forwards to the centralized label service', async () => {
|
||||
const mod = await import('@/app/api/assets/[assetId]/update-label/route')
|
||||
const req = buildMockRequest({
|
||||
path: '/api/assets/asset-1/update-label',
|
||||
method: 'POST',
|
||||
body: {
|
||||
scope: 'project',
|
||||
kind: 'character',
|
||||
projectId: 'project-1',
|
||||
newName: '林夏',
|
||||
},
|
||||
})
|
||||
|
||||
const res = await mod.POST(req, {
|
||||
params: Promise.resolve({ assetId: 'asset-1' }),
|
||||
})
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(authMock.requireProjectAuth).toHaveBeenCalledWith('project-1')
|
||||
expect(updateAssetRenderLabelMock).toHaveBeenCalledWith({
|
||||
scope: 'project',
|
||||
kind: 'character',
|
||||
assetId: 'asset-1',
|
||||
projectId: 'project-1',
|
||||
newName: '林夏',
|
||||
})
|
||||
})
|
||||
|
||||
it('PATCH /api/assets/[assetId] updates a global prop through the unified route', async () => {
|
||||
const mod = await import('@/app/api/assets/[assetId]/route')
|
||||
const req = buildMockRequest({
|
||||
path: '/api/assets/prop-1',
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
scope: 'global',
|
||||
kind: 'prop',
|
||||
name: '青铜短刃',
|
||||
summary: '更锋利的版本',
|
||||
},
|
||||
})
|
||||
|
||||
const res = await mod.PATCH(req, {
|
||||
params: Promise.resolve({ assetId: 'prop-1' }),
|
||||
})
|
||||
const body = await res.json()
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(authMock.requireUserAuth).toHaveBeenCalled()
|
||||
expect(updateAssetMock).toHaveBeenCalledWith({
|
||||
kind: 'prop',
|
||||
assetId: 'prop-1',
|
||||
body: {
|
||||
scope: 'global',
|
||||
kind: 'prop',
|
||||
name: '青铜短刃',
|
||||
summary: '更锋利的版本',
|
||||
},
|
||||
access: {
|
||||
scope: 'global',
|
||||
userId: 'user-1',
|
||||
},
|
||||
})
|
||||
expect(body).toEqual({ success: true })
|
||||
})
|
||||
|
||||
it('DELETE /api/assets/[assetId] removes a project prop through the unified route', async () => {
|
||||
const mod = await import('@/app/api/assets/[assetId]/route')
|
||||
const req = buildMockRequest({
|
||||
path: '/api/assets/prop-1',
|
||||
method: 'DELETE',
|
||||
body: {
|
||||
scope: 'project',
|
||||
kind: 'prop',
|
||||
projectId: 'project-1',
|
||||
},
|
||||
})
|
||||
|
||||
const res = await mod.DELETE(req, {
|
||||
params: Promise.resolve({ assetId: 'prop-1' }),
|
||||
})
|
||||
const body = await res.json()
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(removeAssetMock).toHaveBeenCalledWith({
|
||||
kind: 'prop',
|
||||
assetId: 'prop-1',
|
||||
access: {
|
||||
scope: 'project',
|
||||
userId: 'user-1',
|
||||
projectId: 'project-1',
|
||||
},
|
||||
})
|
||||
expect(body).toEqual({ success: true })
|
||||
})
|
||||
|
||||
it('POST /api/assets/[assetId]/generate forwards project asset generation to the unified service', async () => {
|
||||
const mod = await import('@/app/api/assets/[assetId]/generate/route')
|
||||
const req = buildMockRequest({
|
||||
path: '/api/assets/asset-1/generate',
|
||||
method: 'POST',
|
||||
body: {
|
||||
scope: 'project',
|
||||
kind: 'character',
|
||||
projectId: 'project-1',
|
||||
appearanceId: 'appearance-1',
|
||||
count: 2,
|
||||
},
|
||||
})
|
||||
|
||||
const res = await mod.POST(req, {
|
||||
params: Promise.resolve({ assetId: 'asset-1' }),
|
||||
})
|
||||
const body = await res.json()
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(authMock.requireProjectAuthLight).toHaveBeenCalledWith('project-1')
|
||||
expect(submitAssetGenerateTaskMock).toHaveBeenCalledWith({
|
||||
request: req,
|
||||
kind: 'character',
|
||||
assetId: 'asset-1',
|
||||
body: {
|
||||
scope: 'project',
|
||||
kind: 'character',
|
||||
projectId: 'project-1',
|
||||
appearanceId: 'appearance-1',
|
||||
count: 2,
|
||||
},
|
||||
access: {
|
||||
scope: 'project',
|
||||
userId: 'user-1',
|
||||
projectId: 'project-1',
|
||||
},
|
||||
})
|
||||
expect(body).toEqual({ success: true, taskId: 'task-1' })
|
||||
})
|
||||
|
||||
it('PATCH /api/assets/[assetId]/variants/[variantId] updates a prop variant through the unified route', async () => {
|
||||
const mod = await import('@/app/api/assets/[assetId]/variants/[variantId]/route')
|
||||
const req = buildMockRequest({
|
||||
path: '/api/assets/prop-1/variants/prop-image-1',
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
scope: 'project',
|
||||
kind: 'prop',
|
||||
projectId: 'project-1',
|
||||
description: '古旧短刃,雕纹手柄',
|
||||
},
|
||||
})
|
||||
|
||||
const res = await mod.PATCH(req, {
|
||||
params: Promise.resolve({ assetId: 'prop-1', variantId: 'prop-image-1' }),
|
||||
})
|
||||
const body = await res.json()
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(updateAssetVariantMock).toHaveBeenCalledWith({
|
||||
kind: 'prop',
|
||||
assetId: 'prop-1',
|
||||
variantId: 'prop-image-1',
|
||||
body: {
|
||||
scope: 'project',
|
||||
kind: 'prop',
|
||||
projectId: 'project-1',
|
||||
description: '古旧短刃,雕纹手柄',
|
||||
},
|
||||
access: {
|
||||
scope: 'project',
|
||||
userId: 'user-1',
|
||||
projectId: 'project-1',
|
||||
},
|
||||
})
|
||||
expect(body).toEqual({ success: true })
|
||||
})
|
||||
|
||||
it('POST /api/novel-promotion/[projectId]/copy-from-global delegates to the centralized copy service', async () => {
|
||||
const mod = await import('@/app/api/novel-promotion/[projectId]/copy-from-global/route')
|
||||
const req = buildMockRequest({
|
||||
path: '/api/novel-promotion/project-1/copy-from-global',
|
||||
method: 'POST',
|
||||
body: {
|
||||
type: 'voice',
|
||||
targetId: 'character-1',
|
||||
globalAssetId: 'voice-1',
|
||||
},
|
||||
})
|
||||
|
||||
const res = await mod.POST(req, {
|
||||
params: Promise.resolve({ projectId: 'project-1' }),
|
||||
})
|
||||
const body = await res.json()
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(copyAssetFromGlobalMock).toHaveBeenCalledWith({
|
||||
kind: 'voice',
|
||||
targetId: 'character-1',
|
||||
globalAssetId: 'voice-1',
|
||||
access: {
|
||||
userId: 'user-1',
|
||||
projectId: 'project-1',
|
||||
},
|
||||
})
|
||||
expect(body).toEqual({ success: true })
|
||||
})
|
||||
|
||||
it('POST /api/assets/[assetId]/copy delegates prop copy to the centralized copy service', async () => {
|
||||
const mod = await import('@/app/api/assets/[assetId]/copy/route')
|
||||
const req = buildMockRequest({
|
||||
path: '/api/assets/prop-target-1/copy',
|
||||
method: 'POST',
|
||||
body: {
|
||||
kind: 'prop',
|
||||
projectId: 'project-1',
|
||||
globalAssetId: 'prop-global-1',
|
||||
},
|
||||
})
|
||||
|
||||
const res = await mod.POST(req, {
|
||||
params: Promise.resolve({ assetId: 'prop-target-1' }),
|
||||
})
|
||||
const body = await res.json()
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(copyAssetFromGlobalMock).toHaveBeenCalledWith({
|
||||
kind: 'prop',
|
||||
targetId: 'prop-target-1',
|
||||
globalAssetId: 'prop-global-1',
|
||||
access: {
|
||||
userId: 'user-1',
|
||||
projectId: 'project-1',
|
||||
},
|
||||
})
|
||||
expect(body).toEqual({ success: true })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user