/** @jest-environment node */ import { createHash } from 'node:crypto' import { access, mkdtemp, readdir, rm } from 'node:fs/promises' import os from 'node:os' import path from 'node:path' import { prisma } from '@/lib/prisma' import { getNativeFile, nativeRange, putNativeFile } from '@/lib/desktop-client/native-storage' jest.mock('@/lib/prisma', () => ({ prisma: { $queryRawUnsafe: jest.fn(), $executeRawUnsafe: jest.fn(), $transaction: jest.fn() } })) let directory = '' jest.mock('@/lib/detail-generator/storage', () => ({ getDetailStorageRoot: () => directory })) type Asset = { id: string; owner: string; project: string; source: string; sha256: string; key: string; mime: string; bytes: number } const assets: Asset[] = [] let alive = true const authorize = jest.fn(async () => undefined) function upload(bytes: Buffer, hash?: string) { const sha = hash || createHash('sha256').update(bytes).digest('hex') const stream = new ReadableStream({ start(controller) { for (let offset = 0; offset < bytes.length; offset += 16384) controller.enqueue(bytes.subarray(offset, offset + 16384)); controller.close() } }) return { source: 'native:' + sha, request: new Request('https://sc.jiansun.vip/native', { method: 'PUT', headers: { 'Content-Type': 'application/octet-stream', 'Content-Length': String(bytes.length), 'X-Content-SHA256': sha }, body: stream, duplex: 'half' } as RequestInit) } } beforeEach(async () => { directory = await mkdtemp(path.join(os.tmpdir(), 'desktop-native-')); assets.length = 0; alive = true; jest.clearAllMocks() ;(prisma.$transaction as jest.Mock).mockImplementation(async action => action(prisma)) ;(prisma.$queryRawUnsafe as jest.Mock).mockImplementation(async (sql: string, ...args: any[]) => { if (sql.includes('pg_advisory')) return [] if (sql.includes('FROM desktop_workspace_projects')) return alive ? [{ id: 'project' }] : [] if (sql.includes('SUM(byte_size)')) return [{ bytes: assets.reduce((sum, asset) => sum + asset.bytes, 0), count: assets.length }] if (sql.includes('FROM desktop_workspace_assets')) return assets.filter(asset => asset.owner === args[0] && asset.project === args[1] && asset.source === args[2]) throw new Error(sql) }) ;(prisma.$executeRawUnsafe as jest.Mock).mockImplementation(async (_sql: string, id: string, project: string, owner: string, source: string, sha256: string, key: string, mime: string, bytes: number) => { assets.push({ id, project, owner, source, sha256, key, mime, bytes }); return 1 }) }) afterEach(async () => { await rm(directory, { recursive: true, force: true }) }) test('streams exact native bytes, deduplicates immutable hashes, and supports bounded range downloads', async () => { const bytes = Buffer.alloc(2 * 1024 * 1024 + 17, 0x61), first = upload(bytes) expect(await putNativeFile('owner', 'project', first.source, first.request, authorize)).toMatchObject({ bytes: bytes.length, existing: false }) expect(authorize).toHaveBeenCalledTimes(2) const second = upload(bytes) expect(await putNativeFile('owner', 'project', second.source, second.request, authorize)).toMatchObject({ existing: true }) expect(assets).toHaveLength(1) const response = await getNativeFile('owner', 'project', first.source, new Request('https://sc.jiansun.vip/native', { headers: { range: 'bytes=1048576-1048607' } }), authorize) expect(response.status).toBe(206); expect(response.headers.get('content-length')).toBe('32') expect(Buffer.from(await response.arrayBuffer())).toEqual(bytes.subarray(1048576, 1048608)) await expect(getNativeFile('other', 'project', first.source, new Request('https://sc.jiansun.vip/native'), authorize)).rejects.toMatchObject({ status: 404 }) }) test('checksum failure and revoked authorization at completion leave no native bytes or lock', async () => { const bad = upload(Buffer.from('wrong'), '0'.repeat(64)) await expect(putNativeFile('owner', 'project', bad.source, bad.request, authorize)).rejects.toMatchObject({ code: 'checksum_mismatch' }) expect(assets).toHaveLength(0) const good = upload(Buffer.from('native')) authorize.mockResolvedValueOnce(undefined).mockRejectedValueOnce(Object.assign(new Error('revoked'), { status: 401 })) await expect(putNativeFile('owner', 'project', good.source, good.request, authorize)).rejects.toMatchObject({ status: 401 }) expect(assets).toHaveLength(0) const ownerDir = path.join(directory, 'desktop-native', createHash('sha256').update('owner').digest('hex'), 'project') expect(await readdir(ownerDir)).toEqual([]) }) test('deletion during upload prevents registration and clears the partial file', async () => { const good = upload(Buffer.from('native')) authorize.mockImplementationOnce(async () => undefined).mockImplementationOnce(async () => { alive = false }) await expect(putNativeFile('owner', 'project', good.source, good.request, authorize)).rejects.toMatchObject({ status: 404 }) expect(assets).toHaveLength(0) }) test('oversized, quota-full, and invalid multi-range requests are rejected without reading the archive', async () => { const good = upload(Buffer.from('native')) good.request.headers.set('content-length', String(512 * 1024 * 1024 + 1)) await expect(putNativeFile('owner', 'project', good.source, good.request, authorize)).rejects.toMatchObject({ status: 413 }) assets.push({ id: 'old', owner: 'owner', project: 'project', source: 'native:' + 'f'.repeat(64), sha256: 'f'.repeat(64), key: '', mime: '', bytes: 1024 * 1024 * 1024 }) const full = upload(Buffer.from('native')) await expect(putNativeFile('owner', 'project', full.source, full.request, authorize)).rejects.toMatchObject({ code: 'project_quota' }) expect(nativeRange('bytes=-4', 10)).toEqual({ start: 6, end: 9, partial: true }) expect(nativeRange('bytes=7-999', 10)).toEqual({ start: 7, end: 9, partial: true }) for (const value of ['bytes=1-2,4-5','bytes=10-','bytes=-0','bytes=-','items=1-2']) expect(() => nativeRange(value, 10)).toThrow() }) test('concurrent native uploads cannot multiply a project transfer and quota is rechecked at commit', async () => { const bytes = Buffer.from('streamed'), hash = createHash('sha256').update(bytes).digest('hex') let controller!: ReadableStreamDefaultController const request = new Request('https://sc.jiansun.vip/native', { method: 'PUT', headers: { 'Content-Type': 'application/octet-stream', 'Content-Length': String(bytes.length), 'X-Content-SHA256': hash }, body: new ReadableStream({ start(value) { controller = value } }), duplex: 'half' } as RequestInit) const running = putNativeFile('owner','project','native:' + hash,request,authorize) const lockFile = path.join(directory,'desktop-native',createHash('sha256').update('owner').digest('hex'),'project','.upload.lock') for (let index = 0; index < 100; index++) { if (await access(lockFile).then(() => true, () => false)) break; await new Promise(resolve => setTimeout(resolve, 1)) } const concurrent = upload(Buffer.from('second')) await expect(putNativeFile('owner','project',concurrent.source,concurrent.request,authorize)).rejects.toMatchObject({ code: 'upload_busy' }) // Simulates a preview committed by another request while the stream was arriving. assets.push({ id: 'large', owner: 'owner', project: 'project', source: 'local:large', sha256: '', key: '', mime: 'image/png', bytes: 1024 * 1024 * 1024 }) controller.enqueue(bytes); controller.close() await expect(running).rejects.toMatchObject({ code: 'project_quota' }) expect(assets).toHaveLength(1) expect(await readdir(path.dirname(lockFile))).toEqual([]) })