// @vitest-environment jsdom import { describe, expect, it } from 'vitest'; import { attachCoverFiles, buildTemplateImportFormData, deriveTemplateUploadRow, parseTemplateTags, sanitizeTemplateId, TEMPLATE_IMPORT_MAX_BATCH, type TemplateUploadRow, validateTemplateUploadRows, } from './adminAgcTemplateUploadModel'; function zipFile(name: string) { return new File([new Uint8Array([0x50, 0x4b, 0x03, 0x04])], name, { type: 'application/zip', }); } function coverFile(name: string) { return new File([new Uint8Array([0x89, 0x50, 0x4e, 0x47])], name, { type: 'image/png', }); } function row(zipName: string, patch: Partial = {}) { return { ...deriveTemplateUploadRow(zipFile(zipName)), ...patch }; } describe('sanitizeTemplateId', () => { it('lowercases, keeps whitelisted characters and drops traversal', () => { expect(sanitizeTemplateId('Cocos Empty 2D.zip')).toBe('cocos-empty-2d'); expect(sanitizeTemplateId('../../evil.zip')).toBe('evil'); expect(sanitizeTemplateId('a..b.zip')).toBe('a.b'); expect(sanitizeTemplateId('__hidden__')).toBe('hidden__'); expect(sanitizeTemplateId(`${'x'.repeat(90)}.zip`)).toHaveLength(64); }); }); describe('deriveTemplateUploadRow', () => { it('starts from safe defaults so a batch only needs covers attached', () => { const derived = row('my-template.zip'); expect(derived.id).toBe('my-template'); expect(derived.title).toBe('my-template'); expect(derived.runtime).toBe('html'); expect(derived.templateVersion).toBe('0.1.0'); expect(derived.entry).toBe('index.html'); expect(derived.coverFile).toBeNull(); }); }); describe('attachCoverFiles', () => { it('matches covers by template id and ignores non-image files', () => { const rows = [row('alpha.zip'), row('beta.zip')]; const covers = [ coverFile('alpha.png'), coverFile('beta.webp'), coverFile('notes.txt'), ]; const next = attachCoverFiles(rows, covers); expect(next[0]?.coverFile?.name).toBe('alpha.png'); expect(next[1]?.coverFile?.name).toBe('beta.webp'); }); it('keeps an already matched cover when the new selection has no partner', () => { const rows = [row('alpha.zip', { coverFile: coverFile('alpha.png') })]; const next = attachCoverFiles(rows, [coverFile('other.png')]); expect(next[0]?.coverFile?.name).toBe('alpha.png'); }); }); describe('validateTemplateUploadRows', () => { it('accepts a complete batch', () => { const rows = [ row('alpha.zip', { coverFile: coverFile('alpha.png') }), row('beta.zip', { coverFile: coverFile('beta.png'), runtime: 'cocos' }), ]; expect(validateTemplateUploadRows(rows)).toEqual({}); }); it('reports missing cover, duplicate id and invalid fields per row', () => { const rows = [ row('alpha.zip'), row('alpha.zip', { coverFile: coverFile('alpha.png'), key: 'second' }), row('gamma.zip', { coverFile: coverFile('gamma.png'), runtime: 'docker', templateVersion: 'Bad Version', entry: '../escape.js', title: ' ', }), ]; const errors = validateTemplateUploadRows(rows); expect(errors['alpha.zip']).toContain('缺少封面'); expect(errors.second).toContain('ID 在本批次内重复'); expect(errors['gamma.zip']).toContain('名称必须是 1-80 个字符'); }); it('rejects a batch larger than the server limit', () => { const rows = Array.from( { length: TEMPLATE_IMPORT_MAX_BATCH + 1 }, (_, index) => row(`template-${index}.zip`, { coverFile: coverFile(`template-${index}.png`), }), ); const errors = validateTemplateUploadRows(rows); expect(Object.keys(errors)).toHaveLength(TEMPLATE_IMPORT_MAX_BATCH + 1); expect(errors['template-0.zip']).toContain('单批最多上传'); }); }); describe('buildTemplateImportFormData', () => { it('writes manifest field names and attaches zip / cover per row', () => { const rows = [ row('alpha.zip', { coverFile: coverFile('alpha.png'), tags: '起步, 起步 2d', title: ' Alpha ', }), row('beta.zip', { coverFile: coverFile('beta.png') }), ]; const form = buildTemplateImportFormData('a'.repeat(64), rows); const manifest = JSON.parse(String(form.get('manifest'))); expect(manifest.expectedRevision).toBe('a'.repeat(64)); expect(manifest.templates).toHaveLength(2); expect(manifest.templates[0]).toMatchObject({ id: 'alpha', title: 'Alpha', tags: ['起步', '2d'], zipField: 'zip_0', coverField: 'cover_0', }); expect(manifest.templates[1]).toMatchObject({ id: 'beta', zipField: 'zip_1', coverField: 'cover_1', }); expect((form.get('zip_0') as File).name).toBe('alpha.zip'); expect((form.get('cover_1') as File).name).toBe('beta.png'); }); it('parses tags with dedupe and caps them at the server limit', () => { expect(parseTemplateTags(' a, a,b c ')).toEqual(['a', 'b', 'c']); const many = Array.from({ length: 20 }, (_, index) => `t${index}`).join( ',', ); expect(parseTemplateTags(many)).toHaveLength(16); }); });