77 lines
7.9 KiB
TypeScript
77 lines
7.9 KiB
TypeScript
/** @jest-environment node */
|
||
import { getServerSession } from 'next-auth'
|
||
import { projectViewerRoute } from '@/lib/desktop-client/project-viewer'
|
||
import { deleteDesktopProject, desktopUser, getDesktopAsset, getDesktopProject, listDesktopProjects } from '@/lib/desktop-client/service'
|
||
import { detailGeneratorClientRoute } from '@/lib/detail-generator/access'
|
||
|
||
jest.mock('next-auth', () => ({ getServerSession: jest.fn() }))
|
||
jest.mock('@/lib/auth', () => ({ authOptions: {} }))
|
||
jest.mock('@/lib/desktop-client/service', () => ({ desktopUser: jest.fn(), listDesktopProjects: jest.fn(), getDesktopProject: jest.fn(), getDesktopAsset: jest.fn(), deleteDesktopProject: jest.fn() }))
|
||
jest.mock('@/lib/prisma', () => ({ prisma: { user: { findUnique: jest.fn(async () => ({ isActive: true, status: 'active' })) } } }))
|
||
jest.mock('@/lib/department-permission', () => ({ isSuperAdmin: jest.fn(async () => true) }))
|
||
|
||
beforeEach(() => {
|
||
jest.clearAllMocks(); process.env.DETAIL_GENERATOR_ENABLED = 'true'
|
||
;(getServerSession as jest.Mock).mockResolvedValue({ user: { id: 'owner' } })
|
||
;(desktopUser as jest.Mock).mockResolvedValue({ id: 'owner' })
|
||
})
|
||
test('browser list rechecks the current session and only reads the account project service', async () => {
|
||
;(listDesktopProjects as jest.Mock).mockResolvedValue({ items: [], nextCursor: null, deleted: [{ id: 'private-tombstone' }] })
|
||
const response = await projectViewerRoute(new Request('https://sc.jiansun.vip/api/detail-projects'))
|
||
expect(response.status).toBe(200); expect(await response.json()).toEqual({ items: [], nextCursor: null })
|
||
expect(desktopUser).toHaveBeenCalledWith('owner'); expect(listDesktopProjects).toHaveBeenCalledWith('owner', null)
|
||
})
|
||
test('view projection strips credentials, bindings and internal storage keys', async () => {
|
||
;(getDesktopProject as jest.Mock).mockResolvedValue({ source: 'desktop', project: { id: 'p', revision: 2, status: 'draft', document: { name: '桌子', screens: [], modelBindings: { provider: 'secret' }, apiKey: 'secret' } }, assets: [{ source: 'local:a', mime: 'image/png', storageKey: 'private/path' }] })
|
||
const response = await projectViewerRoute(new Request('https://sc.jiansun.vip/api/detail-projects/desktop/p'), ['desktop', 'p'])
|
||
const text = await response.text(); expect(text).not.toContain('secret'); expect(text).not.toContain('private/path')
|
||
})
|
||
|
||
test('native projects expose only rendered previews and creation summaries, never archives or Pi sessions', async () => {
|
||
;(getDesktopProject as jest.Mock).mockResolvedValue({ source: 'desktop', project: { id: 'p', revision: 3, status: 'completed', conversation: [{ text: 'private-session' }], document: { version: 3, name: '五屏', native: { documentSource: 'native:private-archive', files: [{ path: 'session-backup.json' }] }, screens: [{ id: 'one', width: 1440, height: 2200, previewSource: 'local:preview' }], creation: { brief: { name: '电脑桌', note: '橡木色' }, facts: { items: [] } } } }, assets: [{ source: 'local:preview', mime: 'image/png' }, { source: 'native:private-archive', mime: 'application/x-photocraft-resource', storageKey: 'private' }] })
|
||
const response = await projectViewerRoute(new Request('https://sc.jiansun.vip/api/detail-projects/desktop/p'), ['desktop', 'p'])
|
||
const value = await response.json(), text = JSON.stringify(value)
|
||
expect(value.project.document.version).toBe(3); expect(value.project.document.screens[0].previewSource).toBe('local:preview')
|
||
expect(value.assets).toEqual([{ source: 'local:preview', mime: 'image/png' }])
|
||
for (const privateValue of ['private-session','private-archive','session-backup.json','storageKey']) expect(text).not.toContain(privateValue)
|
||
})
|
||
|
||
test('native preparation-only drafts expose safe product metadata without leaking reference image objects', async () => {
|
||
const brief = { name: '电脑桌', brandName: '顾家', identifierType: 'sku', identifier: 'GBGJ-CYJ0056', colorName: '胡桃木色', dimensions: { length: '1200', width: '600', height: '750', unit: 'mm' }, source: 'library', note: '', images: [{ source: 'local:private-product-path', purpose: 'product' }, { source: 'local:private-product-two', purpose: 'product' }, { source: 'local:private-scene', purpose: 'scene' }], privateField: 'internal-reference-detail' }
|
||
;(getDesktopProject as jest.Mock).mockResolvedValue({ source: 'desktop', project: { id: 'p', revision: 1, status: 'draft', document: { version: 3, name: '新项目', screens: [], creation: { brief } } }, assets: [] })
|
||
const response = await projectViewerRoute(new Request('https://sc.jiansun.vip/api/detail-projects/desktop/p'), ['desktop', 'p'])
|
||
const value = await response.json()
|
||
expect(value.project.document.brief).toEqual({ name: brief.name, brandName: brief.brandName, identifierType: brief.identifierType, identifier: brief.identifier, colorName: brief.colorName, dimensions: brief.dimensions, source: 'library', referenceImageCount: 3, note: '' })
|
||
const text = JSON.stringify(value)
|
||
for (const privateValue of ['local:private', 'purpose', 'images', 'privateField', 'internal-reference-detail']) expect(text).not.toContain(privateValue)
|
||
})
|
||
|
||
test('older desktop and Web projects retain readable metadata without inventing unsupported source fields', async () => {
|
||
;(getDesktopProject as jest.Mock).mockResolvedValue({ source: 'desktop', project: { id: 'p', revision: 1, document: { version: 2, screens: [], brief: { name: '旧桌面工程', source: 'upload', identifierType: 'barcode', identifier: '6901234567890', images: [{ source: 'local:private' }] } } }, assets: [] })
|
||
const desktop = await projectViewerRoute(new Request('https://sc.jiansun.vip/api/detail-projects/desktop/p'), ['desktop', 'p'])
|
||
expect((await desktop.json()).project.document.brief).toEqual({ name: '旧桌面工程', source: 'upload', identifierType: 'barcode', identifier: '6901234567890', referenceImageCount: 1 })
|
||
const product = { name: '旧网页工程', identifierType: 'spu', identifier: 'SPU-1', assetIds: ['image-one'], specifications: { color: '橡木色', dimensions: '1200×600' } }
|
||
;(getDesktopProject as jest.Mock).mockResolvedValue({ source: 'web', project: { id: 'p', document: { screens: [], inputSource: 'library', product } }, assets: [] })
|
||
const web = await projectViewerRoute(new Request('https://sc.jiansun.vip/api/detail-projects/web/p'), ['web', 'p'])
|
||
expect((await web.json()).project.document).toEqual({ screens: [], inputSource: 'library', product })
|
||
})
|
||
test('browser deletion requires an allowed origin and sends the exact confirmation revision once', async () => {
|
||
const url = 'https://sc.jiansun.vip/api/detail-projects/desktop/p'
|
||
;(deleteDesktopProject as jest.Mock).mockResolvedValue({ deleted: true })
|
||
const absent = await projectViewerRoute(new Request(url, { method: 'DELETE', body: '{}' }), ['desktop', 'p'])
|
||
expect(absent.status).toBe(403); expect(deleteDesktopProject).not.toHaveBeenCalled()
|
||
const response = await projectViewerRoute(new Request(url, { method: 'DELETE', headers: { origin: 'https://sc.jiansun.vip' }, body: JSON.stringify({ revision: 4 }) }), ['desktop', 'p'])
|
||
expect(response.status).toBe(200); expect(deleteDesktopProject).toHaveBeenCalledTimes(1)
|
||
expect(deleteDesktopProject).toHaveBeenCalledWith('owner', 'desktop', 'p', { revision: 4 })
|
||
})
|
||
test('project images remain owner scoped and only image MIME types are served inline', async () => {
|
||
;(getDesktopAsset as jest.Mock).mockResolvedValue({ bytes: Buffer.from('test'), mime: 'application/octet-stream' })
|
||
const response = await projectViewerRoute(new Request('https://sc.jiansun.vip/api/detail-projects/desktop/p/assets/a'), ['desktop', 'p', 'assets', 'a'])
|
||
expect(getDesktopAsset).toHaveBeenCalledWith('owner', 'desktop', 'p', 'a'); expect(response.status).toBe(415)
|
||
})
|
||
test('web editing endpoints do not execute their action or dispatch models', async () => {
|
||
const action = jest.fn()
|
||
const response = await detailGeneratorClientRoute(action)
|
||
expect(response.status).toBe(409); expect((await response.json()).code).toBe('client_required'); expect(action).not.toHaveBeenCalled()
|
||
})
|