import { describe, expect, test, vi } from 'vitest'; import { uploadCoursewareZip } from '@/lib/bundle/client-upload'; function jsonResponse(status: number, body: unknown): Response { return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' }, }); } describe('uploadCoursewareZip', () => { test('sends multipart fields + bearer token and returns the record', async () => { const fetchImpl = vi.fn(async (_input: unknown, _init?: RequestInit) => { return jsonResponse(201, { success: true, record: { coursewareId: 'cw-1', version: 3, title: '示例课件', status: 'published', publishedAt: '2026-08-15T00:00:00.000Z', contentHash: 'ab'.repeat(32), sceneCount: 3, quizSceneCount: 1, bundleUrl: 'http://localhost/api/coursewares/cw-1/bundles/3/download', }, }); }); const result = await uploadCoursewareZip({ zip: new Blob([new Uint8Array([1, 2, 3])], { type: 'application/zip' }), coursewareId: 'cw-1', token: 'secret', fetchImpl, }); expect(result.ok).toBe(true); expect(result.record?.version).toBe(3); expect(fetchImpl).toHaveBeenCalledTimes(1); const [endpoint, init] = fetchImpl.mock.calls[0] as unknown as [string, RequestInit]; expect(endpoint).toBe('/api/coursewares'); expect((init.headers as Record).authorization).toBe('Bearer secret'); const form = init.body as FormData; expect(form.get('coursewareId')).toBe('cw-1'); expect(form.get('status')).toBeNull(); // default published → omitted expect((form.get('zip') as File).name).toBe('bundle.zip'); }); test('includes explicit version and status', async () => { const fetchImpl = vi.fn(async () => jsonResponse(201, { success: true })); await uploadCoursewareZip({ zip: new Blob(['x']), coursewareId: 'cw-2', token: 't', version: 7, status: 'unpublished', endpoint: 'https://registry.example.com/api/coursewares', fetchImpl, }); const [endpoint, init] = fetchImpl.mock.calls[0] as unknown as [string, RequestInit]; expect(endpoint).toBe('https://registry.example.com/api/coursewares'); const form = init.body as FormData; expect(form.get('version')).toBe('7'); expect(form.get('status')).toBe('unpublished'); }); test('surfaces server error body details', async () => { const fetchImpl = vi.fn(async () => jsonResponse(400, { success: false, errorCode: 'INVALID_REQUEST', error: 'Courseware publish failed', details: 'Bundle content hash mismatch', }), ); const result = await uploadCoursewareZip({ zip: new Blob(['x']), coursewareId: 'cw-1', token: 't', fetchImpl, }); expect(result.ok).toBe(false); expect(result.status).toBe(400); expect(result.errorCode).toBe('INVALID_REQUEST'); expect(result.details).toBe('Bundle content hash mismatch'); }); test('reports network failure as status 0', async () => { const fetchImpl = vi.fn(async () => { throw new Error('boom'); }); const result = await uploadCoursewareZip({ zip: new Blob(['x']), coursewareId: 'cw-1', token: 't', fetchImpl, }); expect(result.ok).toBe(false); expect(result.status).toBe(0); expect(result.error).toContain('Network error'); }); });