Merge pull request 'auto: PR for feat/zip-validation-phase4' (#25) from feat/zip-validation-phase4 into main
CI / test (push) Successful in 47s
CI / test (push) Successful in 47s
This commit was merged in pull request #25.
This commit is contained in:
@@ -5,6 +5,8 @@ export type BuildZipInput = {
|
||||
css: string
|
||||
js: string
|
||||
assets?: Record<string, Uint8Array>
|
||||
// root-level extra files like favicon.ico, site.webmanifest, robots.txt
|
||||
extras?: Record<string, Uint8Array>
|
||||
}
|
||||
|
||||
export async function buildZip(input: BuildZipInput): Promise<Uint8Array> {
|
||||
@@ -19,6 +21,22 @@ export async function buildZip(input: BuildZipInput): Promise<Uint8Array> {
|
||||
zip.file(path, buf as unknown as Buffer)
|
||||
}
|
||||
}
|
||||
if (input.extras) {
|
||||
// validate manifest if present
|
||||
if (Object.prototype.hasOwnProperty.call(input.extras, 'site.webmanifest')) {
|
||||
try {
|
||||
const manifestBytes = input.extras['site.webmanifest']
|
||||
const manifestStr = Buffer.from(manifestBytes).toString('utf8')
|
||||
JSON.parse(manifestStr)
|
||||
} catch {
|
||||
throw new Error('Invalid site.webmanifest JSON')
|
||||
}
|
||||
}
|
||||
for (const [path, bytes] of Object.entries(input.extras)) {
|
||||
const buf = Buffer.from(bytes)
|
||||
zip.file(path, buf as unknown as Buffer)
|
||||
}
|
||||
}
|
||||
const content = await zip.generateAsync({ type: 'uint8array' })
|
||||
return content
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import JSZip from 'jszip'
|
||||
import { buildZip } from '@/lib/exporter/zip'
|
||||
import type { BuildZipInput } from '@/lib/exporter/zip'
|
||||
|
||||
const TE = new TextEncoder()
|
||||
|
||||
describe('Export ZIP 검증 강화 4차: 루트 파일(favicon/manifest/robots)', () => {
|
||||
it('루트 파일들을 포함한다: favicon.ico / site.webmanifest / robots.txt', async () => {
|
||||
const manifest = {
|
||||
name: 'Landing Builder',
|
||||
short_name: 'LB',
|
||||
start_url: '/',
|
||||
display: 'standalone',
|
||||
icons: [{ src: '/favicon.png', sizes: '512x512', type: 'image/png' }],
|
||||
}
|
||||
const input: BuildZipInput = {
|
||||
html: '<!DOCTYPE html><html><head><title>a</title></head><body>ok</body></html>',
|
||||
css: 'body{color:#111}',
|
||||
js: 'console.log("ok")',
|
||||
assets: {},
|
||||
extras: {
|
||||
'favicon.ico': new Uint8Array([0, 1, 2, 3]),
|
||||
'site.webmanifest': TE.encode(JSON.stringify(manifest)),
|
||||
'robots.txt': TE.encode('User-agent: *\nDisallow:'),
|
||||
},
|
||||
}
|
||||
const zipBytes = await buildZip(input)
|
||||
|
||||
const zip = await JSZip.loadAsync(zipBytes)
|
||||
const names = Object.keys(zip.files)
|
||||
expect(names).toContain('favicon.ico')
|
||||
expect(names).toContain('site.webmanifest')
|
||||
expect(names).toContain('robots.txt')
|
||||
|
||||
const robots = await zip.files['robots.txt'].async('string')
|
||||
expect(robots).toMatch(/User-agent: \*/)
|
||||
|
||||
const parsed = JSON.parse(await zip.files['site.webmanifest'].async('string'))
|
||||
expect(parsed.name).toBe('Landing Builder')
|
||||
expect(parsed.icons[0].type).toBe('image/png')
|
||||
})
|
||||
|
||||
it('잘못된 manifest JSON이면 실패한다', async () => {
|
||||
const badInput: BuildZipInput = {
|
||||
html: '<!DOCTYPE html><html><head><title>a</title></head><body>ok</body></html>',
|
||||
css: 'body{color:#111}',
|
||||
js: 'console.log("ok")',
|
||||
extras: { 'site.webmanifest': TE.encode('{ invalid json') },
|
||||
}
|
||||
await expect(buildZip(badInput)).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user