gongxiangp2/lib/__tests__/desktop-project-viewer.test.ts

77 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** @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()
})