Files
zpan/server/routes/system.test.ts
T
saltbo 310e4950b3 refactor: flatten monorepo to single-package CF Pages Functions structure
Replace pnpm workspace monorepo (packages/server, packages/web, packages/shared)
with a flat single-package layout following Hono's pages-stack pattern. Switch from
pnpm to npm and from Workers+Assets to CF Pages Functions deployment model.

- Move source: packages/server/src/ → server/, packages/web/src/ → src/, packages/shared/src/ → shared/
- Add functions/api/[[route]].ts as CF Pages Functions entry (replaces entry-cloudflare.ts)
- Update 22 import paths: server uses relative, web uses @shared/@server aliases
- Merge three package.json into one, switch to npm
- Update wrangler.toml: remove main/assets (Pages auto-detects functions/ dir)
- Add per-directory tsconfig.json for VS Code type resolution
- Simplify Dockerfile for flat layout
- Fix react-pdf CSS import path (dist/esm/ → dist/)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 00:27:38 -04:00

87 lines
3.4 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { adminHeaders, createTestApp } from '../test/setup.js'
async function putOption(
app: ReturnType<typeof createTestApp>['app'],
headers: Record<string, string>,
key: string,
body: Record<string, unknown>,
) {
return app.request(`/api/system/options/${key}`, {
method: 'PUT',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
describe('System API — options CRUD', () => {
it('GET unknown key returns 404', async () => {
const { app } = createTestApp()
const res = await app.request('/api/system/options/site_name')
expect(res.status).toBe(404)
})
it('full admin CRUD lifecycle with public/private visibility', async () => {
const { app } = createTestApp()
const admin = await adminHeaders(app)
// Create public option
const put = await putOption(app, admin, 'site_name', { value: 'ZPan', public: true })
expect(put.status).toBe(201)
expect(await put.json()).toEqual({ key: 'site_name', value: 'ZPan', public: true })
// Anonymous can read public option
const anonGet = await app.request('/api/system/options/site_name')
expect(anonGet.status).toBe(200)
expect(await anonGet.json()).toEqual({ key: 'site_name', value: 'ZPan', public: true })
// Update preserves public flag when omitted
const put2 = await putOption(app, admin, 'site_name', { value: 'v2' })
expect(put2.status).toBe(200)
expect(await put2.json()).toEqual({ key: 'site_name', value: 'v2', public: true })
// Private option hidden from anonymous
await putOption(app, admin, 'smtp_password', { value: 'secret', public: false })
const anonPrivate = await app.request('/api/system/options/smtp_password')
expect(anonPrivate.status).toBe(403)
// Admin can read private option
const adminPrivate = await app.request('/api/system/options/smtp_password', { headers: admin })
expect(adminPrivate.status).toBe(200)
expect(await adminPrivate.json()).toEqual({ key: 'smtp_password', value: 'secret', public: false })
// List: anon sees only public, admin sees all
const anonList = await app.request('/api/system/options')
const anonBody = (await anonList.json()) as { items: { key: string }[]; total: number }
expect(anonBody.total).toBe(1)
expect(anonBody.items[0].key).toBe('site_name')
const adminList = await app.request('/api/system/options', { headers: admin })
const adminBody = (await adminList.json()) as { total: number }
expect(adminBody.total).toBe(2)
// Non-string value rejected
const bad = await putOption(app, admin, 'site_name', { value: 123 })
expect(bad.status).toBe(400)
// Delete
const del = await app.request('/api/system/options/site_name', { method: 'DELETE', headers: admin })
expect(del.status).toBe(200)
const afterDel = await app.request('/api/system/options/site_name')
expect(afterDel.status).toBe(404)
})
it('unauthenticated mutations are rejected', async () => {
const { app } = createTestApp()
const res = await app.request('/api/system/options/site_name', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ value: 'ZPan' }),
})
expect(res.status).toBe(401)
const del = await app.request('/api/system/options/site_name', { method: 'DELETE' })
expect(del.status).toBe(401)
})
})