import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { describe, it } from 'node:test'
import { fileURLToPath } from 'node:url'
import {
assembleSite,
buildManifest,
contentTypeFor,
hashAsset,
hashAssets,
isIgnoredSitePath,
normalizeSitePath,
planUploadBatches,
renderHeaders,
mergeHeadersText,
renderRedirects,
SPA_REDIRECT_RULE,
} from '../src/static-site.ts'
import {
entriesToSiteFiles,
readArchive,
selectSiteEntries,
} from '../src/archive.ts'
import { buildFormData, buildRequestBody } from '../src/request-body.ts'
import { nextPageQuery, extractListItems } from '../src/api-v4-list.ts'
import { scanSiteFiles, DEFAULT_FORBID } from '../src/scan.ts'
import {
assertPagesProjectName,
buildDeploymentForm,
deploymentState,
} from '../src/pages-direct-upload.ts'
import { accounts, resolveKnownAccountId, KCD_CLOUDFLARE_ACCOUNT_ID } from '../src/auth.ts'
const fixtures = join(dirname(fileURLToPath(import.meta.url)), 'fixtures')
describe('normalizeSitePath', () => {
it('strips leading slash and dots', () => {
assert.equal(normalizeSitePath('/assets/app.js'), 'assets/app.js')
assert.equal(normalizeSitePath('./index.html'), 'index.html')
})
it('rejects .. and empty', () => {
assert.throws(() => normalizeSitePath('../x'), /Invalid/)
assert.throws(() => normalizeSitePath(''), /Invalid/)
assert.throws(() => normalizeSitePath('a/'), /directory/)
})
})
describe('content types and ignore', () => {
it('maps common extensions', () => {
assert.equal(contentTypeFor('a.html'), 'text/html')
assert.equal(contentTypeFor('a.js'), 'application/javascript')
assert.equal(contentTypeFor('a.weird'), 'application/octet-stream')
})
it('ignores junk segments', () => {
assert.equal(isIgnoredSitePath('.DS_Store'), true)
assert.equal(isIgnoredSitePath('node_modules/x.js'), true)
assert.equal(isIgnoredSitePath('assets/app.js'), false)
})
})
describe('hash + manifest', () => {
it('hashes base64+ext to 32 hex chars and is stable', async () => {
const a = await hashAsset(btoa('hello'), 'index.html')
const b = await hashAsset(btoa('hello'), 'index.html')
assert.equal(a, b)
assert.match(a, /^[0-9a-f]{32}$/)
const c = await hashAsset(btoa('hello'), 'index.txt')
assert.notEqual(a, c)
})
it('builds a sorted path→hash manifest', async () => {
const assets = await hashAssets([
{ path: 'b.txt', bytes: new TextEncoder().encode('b'), contentType: 'text/plain' },
{ path: 'a.txt', bytes: new TextEncoder().encode('a'), contentType: 'text/plain' },
])
assert.deepEqual(Object.keys(buildManifest(assets)), ['/a.txt', '/b.txt'])
})
it('batches uploads by size with unique hashes', () => {
const files = [
{ hash: 'a', size: 6 },
{ hash: 'b', size: 5 },
{ hash: 'a', size: 6 },
{ hash: 'c', size: 4 },
]
const batches = planUploadBatches(files, { maxBatchBytes: 10, maxBatchFiles: 10 })
assert.equal(batches.flat().length, 3)
assert.ok(batches.every((batch) => batch.reduce((s, f) => s + f.size, 0) <= 10))
})
})
describe('headers / redirects', () => {
it('renders object headers and rule redirects', () => {
assert.equal(
renderHeaders({ '/*': { 'X-Frame-Options': 'DENY' } }),
'/*\n X-Frame-Options: DENY',
)
assert.equal(renderRedirects([{ from: '/a', to: '/b', status: 301 }]), '/a /b 301')
})
it('merges repeated _headers patterns into one block', () => {
assert.equal(
mergeHeadersText('/*\n X-A: 1\n# note\n/assets/*\n Cache-Control: max-age=60\n/*\n X-B: 2'),
'/*\n X-A: 1\n X-B: 2\n/assets/*\n Cache-Control: max-age=60',
)
})
it('rejects bad redirects', () => {
assert.throws(() => renderRedirects([{ from: '/a space', to: '/b' } as never]), /whitespace/)
})
})
describe('assembleSite', () => {
it('merges spa + noindex and removes config from assets', () => {
const site = assembleSite({
files: {
'index.html': '
hi
',
'_headers': '/*\n X-Test: 1',
},
spa: true,
noindex: true,
})
assert.equal(site.assets.some((f) => f.path === '_headers'), false)
assert.match(site.config._headers ?? '', /X-Test/)
assert.match(site.config._headers ?? '', /noindex/)
assert.equal((site.config._headers ?? '').match(/^\/\*$/gm)?.length, 1)
assert.equal((site.config._redirects ?? '').trim(), SPA_REDIRECT_RULE)
})
it('spa:false adds a 404.html', () => {
const site = assembleSite({ files: { 'index.html': 'x' }, spa: false })
assert.ok(site.assets.some((f) => f.path === '404.html'))
})
it('rejects _worker.js', () => {
assert.throws(
() => assembleSite({ files: { 'index.html': 'x', '_worker.js': 'export default {}' } }),
/_worker\.js/,
)
})
it('files override archive entries', () => {
const site = assembleSite({
archiveFiles: [{ path: 'index.html', bytes: new TextEncoder().encode('old'), contentType: 'text/html' }],
files: { 'index.html': 'new' },
})
assert.equal(new TextDecoder().decode(site.assets[0].bytes), 'new')
assert.ok(site.warnings.some((w) => /overrode/.test(w)))
})
})
describe('archives', () => {
it('reads zip and strips a shared top folder', async () => {
const { entries } = await readArchive(readFileSync(join(fixtures, 'site.zip')))
const selected = selectSiteEntries(entries)
assert.equal(selected.root, 'site')
const files = entriesToSiteFiles(selected.entries)
assert.ok(files.some((f) => f.path === 'index.html'))
assert.ok(files.some((f) => f.path === 'assets/app.js'))
assert.equal(files.some((f) => f.path === '.DS_Store'), false)
assert.equal(files.some((f) => f.path === '_headers'), true)
})
it('reads stored (uncompressed) zip entries', async () => {
const { entries } = await readArchive(readFileSync(join(fixtures, 'site-stored.zip')))
const files = entriesToSiteFiles(selectSiteEntries(entries).entries)
assert.deepEqual(files.map((f) => f.path).sort(), ['a/b.txt', 'index.html'])
assert.equal(new TextDecoder().decode(files.find((f) => f.path === 'a/b.txt')!.bytes), 'bee')
})
it('reads deflated zip bytes correctly', async () => {
const { entries } = await readArchive(readFileSync(join(fixtures, 'site.zip')))
const index = entries.find((e) => e.path === 'site/index.html')!
assert.match(new TextDecoder().decode(index.data), /Hello<\/h1>/)
})
it('reads tar.gz and tar', async () => {
const tgz = await readArchive(readFileSync(join(fixtures, 'site.tgz')))
assert.equal(tgz.format, 'tar.gz')
assert.ok(tgz.entries.some((e) => e.path === 'index.html' || e.path.endsWith('/index.html')))
const tar = await readArchive(readFileSync(join(fixtures, 'site.tar')))
assert.equal(tar.format, 'tar')
})
it('sourceRoot with * strips a GitHub-style top folder', async () => {
const { entries } = await readArchive(readFileSync(join(fixtures, 'gh.tgz')))
const selected = selectSiteEntries(entries, '*/dist')
assert.match(selected.root, /repo-/)
const files = entriesToSiteFiles(selected.entries)
assert.deepEqual(files.map((f) => f.path), ['index.html'])
})
})
describe('scan', () => {
it('includes built-in credential patterns by default', () => {
assert.ok(DEFAULT_FORBID.length >= 5)
const result = scanSiteFiles([
{ path: 'a.txt', bytes: new TextEncoder().encode('token sk_live_abcdefghijklmnopqrstuvwxyz end'), contentType: 'text/plain' },
])
assert.equal(result.ok, false)
assert.equal(result.findings[0].pattern, 'stripe-secret-key')
assert.match(result.findings[0].preview, /\*/)
})
it('accepts /regex/ strings and literals', () => {
const files = [{ path: 'a.html', bytes: new TextEncoder().encode('INTERNAL_ONLY_MARKER'), contentType: 'text/html' }]
assert.equal(scanSiteFiles(files, { forbid: ['INTERNAL_ONLY_MARKER'], defaults: false }).ok, false)
assert.equal(scanSiteFiles(files, { forbid: ['/internal_only_marker/i'], defaults: false }).ok, false)
assert.equal(scanSiteFiles(files, { forbid: ['nope'], defaults: false }).ok, true)
})
it('skips binary files', () => {
const result = scanSiteFiles(
[{ path: 'a.png', bytes: new Uint8Array([1, 2, 3]), contentType: 'image/png' }],
{ forbid: ['x'], defaults: false },
)
assert.equal(result.scannedFiles, 0)
assert.equal(result.skippedBinaryFiles, 1)
})
})
describe('form / request body', () => {
it('builds multipart form fields', () => {
const form = buildFormData({
manifest: '{}',
_headers: { filename: '_headers', content: '/*\n X: 1' },
})
assert.ok(form.get('manifest') === '{}')
assert.ok(form.get('_headers') instanceof File)
})
it('rejects body+form together and ignores JSON body on GET', () => {
assert.throws(() => buildRequestBody({ body: {}, form: { a: '1' } }, 'POST'), /only one/)
assert.equal(buildRequestBody({ body: { a: 1 } }, 'GET').kind, 'none')
assert.equal(buildRequestBody({ body: { a: 1 } }, 'POST').kind, 'json')
assert.equal(buildRequestBody({ form: { a: '1' } }, 'POST').kind, 'form')
})
})
describe('api-v4-list helpers (fixtures)', () => {
it('walks page-number result_info from a real Pages list shape', () => {
const info = JSON.parse(readFileSync(join(fixtures, 'result-info.json'), 'utf8'))
assert.deepEqual(
nextPageQuery(info.pagesProjectsPage1, { page: 1, received: 10, perPage: 10 }),
{ page: 2 },
)
assert.equal(nextPageQuery(info.pagesProjectsPage5, { page: 5, received: 7, perPage: 10 }), null)
})
it('walks cursor result_info from a real R2 list shape', () => {
const info = JSON.parse(readFileSync(join(fixtures, 'result-info.json'), 'utf8'))
assert.deepEqual(
nextPageQuery(info.r2BucketsCursor, { page: 1, received: 20 }),
{ cursor: info.r2BucketsCursor.cursor },
)
assert.equal(nextPageQuery(info.r2BucketsCursor, { page: 1, received: 0 }), null)
})
it('extracts items from array or single-array-property results', () => {
assert.deepEqual(extractListItems([{ id: 1 }]), [{ id: 1 }])
assert.deepEqual(extractListItems({ buckets: [{ name: 'a' }] }), [{ name: 'a' }])
assert.deepEqual(extractListItems({ buckets: [{ name: 'a' }] }, 'buckets'), [{ name: 'a' }])
})
})
describe('pages helpers', () => {
it('validates project names and reads deployment fixtures', () => {
assert.equal(assertPagesProjectName('mega-week3-plan'), 'mega-week3-plan')
assert.throws(() => assertPagesProjectName('Bad Name'), /Invalid/)
const live = JSON.parse(readFileSync(join(fixtures, 'pages-deployment-live.json'), 'utf8'))
assert.deepEqual(deploymentState(live), { stage: 'deploy:success', live: true, failed: false })
assert.equal(deploymentState({ id: 'x', latest_stage: { name: 'queued', status: 'idle' } }).live, false)
})
it('builds deployment form fields', () => {
const form = buildDeploymentForm({
manifest: { '/index.html': 'abc' },
branch: 'main',
message: 'hi',
config: { _headers: '/*\n X: 1\n', _redirects: '/* /index.html 200\n' },
})
assert.equal(form.manifest, '{"/index.html":"abc"}')
assert.equal(form.branch, 'main')
assert.deepEqual(form._headers, { filename: '_headers', content: '/*\n X: 1\n' })
})
})
describe('accounts metadata', () => {
it('documents every alias with can/cannot and known account ids', () => {
const list = accounts()
assert.deepEqual(list.map((a) => a.account), ['default', 'kody', 'pages'])
const pages = list.find((a) => a.account === 'pages')!
assert.equal(pages.accountId, KCD_CLOUDFLARE_ACCOUNT_ID)
assert.equal(pages.accountName, 'KCD')
assert.ok(pages.can.some((c) => /Pages/.test(c)))
assert.ok(pages.cannot.some((c) => /GET \/accounts/.test(c)))
assert.equal(resolveKnownAccountId({ account: 'pages' }), KCD_CLOUDFLARE_ACCOUNT_ID)
assert.equal(resolveKnownAccountId({ account: 'default' }), null)
})
})