375 lines
11 KiB
TypeScript
375 lines
11 KiB
TypeScript
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 })
|
|
})
|
|
})
|