feat(hq): GET/PUT /settings/template + logo upload (owner-gated, audited)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>feat/client-detail-redesign
parent
c2f94df06d
commit
48731fdb2b
@ -0,0 +1,66 @@
|
||||
// apps/hq/test/settings-template.test.ts
|
||||
import { describe, it, expect, afterAll } from 'vitest'
|
||||
import express from 'express'
|
||||
import { openDb } from '../src/db'
|
||||
import { seedIfEmpty } from '../src/seed'
|
||||
import { createStaff } from '../src/auth'
|
||||
import { getSetting } from '../src/repos-reminders'
|
||||
import { apiRouter } from '../src/api'
|
||||
|
||||
function appWith() {
|
||||
const db = openDb(':memory:'); seedIfEmpty(db)
|
||||
createStaff(db, { email: 'owner@test.in', displayName: 'Owner', role: 'owner', password: 'owner-password' })
|
||||
createStaff(db, { email: 'staff@test.in', displayName: 'Staff', role: 'staff', password: 'staff-password' })
|
||||
const app = express(); app.use(express.json({ limit: '2mb' })); app.locals['db'] = db; app.use('/api', apiRouter(db))
|
||||
const server = app.listen(0)
|
||||
const base = `http://localhost:${(server.address() as { port: number }).port}/api`
|
||||
return { db, server, base }
|
||||
}
|
||||
|
||||
describe('template settings', () => {
|
||||
const { db, server, base } = appWith()
|
||||
afterAll(() => server.close())
|
||||
const login = async (email: string, password: string) =>
|
||||
(await (await fetch(`${base}/auth/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email, password }) })).json() as any).token
|
||||
const call = async (token: string, method: string, path: string, body?: unknown) => {
|
||||
const res = await fetch(base + path, { method, headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` }, ...(body ? { body: JSON.stringify(body) } : {}) })
|
||||
return { status: res.status, json: await res.json() as any }
|
||||
}
|
||||
|
||||
it('owner GET returns company.* + template.*; PUT writes template fields + a title, each audited', async () => {
|
||||
const token = await login('owner@test.in', 'owner-password')
|
||||
const before = await call(token, 'GET', '/settings/template')
|
||||
expect(before.json.settings['company.name']).toBe('Tecnostac')
|
||||
const put = await call(token, 'PUT', '/settings/template', {
|
||||
template: { footerNote: 'Thank you!', declaration: 'We declare…', signatoryLabel: 'Proprietor' },
|
||||
titles: { INVOICE: 'GST INVOICE' },
|
||||
})
|
||||
expect(put.status).toBe(200)
|
||||
expect(getSetting(db, 'template.footer_note')).toBe('Thank you!')
|
||||
expect(getSetting(db, 'template.signatory_label')).toBe('Proprietor')
|
||||
expect(getSetting(db, 'template.title_INVOICE')).toBe('GST INVOICE')
|
||||
const audits = db.prepare(`SELECT COUNT(*) AS n FROM audit_log WHERE entity='setting' AND entity_id LIKE 'template.%'`).get() as { n: number }
|
||||
expect(audits.n).toBeGreaterThanOrEqual(4)
|
||||
})
|
||||
it('rejects a staff PUT (owner only) and an invalid GSTIN', async () => {
|
||||
const staff = await login('staff@test.in', 'staff-password')
|
||||
expect((await call(staff, 'PUT', '/settings/template', { template: { terms: 'x' } })).status).toBe(403)
|
||||
const owner = await login('owner@test.in', 'owner-password')
|
||||
expect((await call(owner, 'PUT', '/settings/template', { company: { gstin: 'NOTAGSTIN' } })).status).toBe(400)
|
||||
})
|
||||
|
||||
it('logo: accepts a small image data URI (owner, audited), rejects non-image, oversize, and staff', async () => {
|
||||
const owner = await login('owner@test.in', 'owner-password')
|
||||
const tiny = 'data:image/png;base64,' + 'A'.repeat(200)
|
||||
expect((await call(owner, 'POST', '/settings/template/logo', { dataUri: tiny })).status).toBe(200)
|
||||
expect(getSetting(db, 'template.logo')).toBe(tiny)
|
||||
expect((await call(owner, 'POST', '/settings/template/logo', { dataUri: 'data:text/html;base64,AAAA' })).status).toBe(400)
|
||||
const huge = 'data:image/png;base64,' + 'A'.repeat(400_000) // ~300 KB decoded > 200 KB cap
|
||||
expect((await call(owner, 'POST', '/settings/template/logo', { dataUri: huge })).status).toBe(400)
|
||||
const staff = await login('staff@test.in', 'staff-password')
|
||||
expect((await call(staff, 'POST', '/settings/template/logo', { dataUri: tiny })).status).toBe(403)
|
||||
// clearing is allowed
|
||||
expect((await call(owner, 'POST', '/settings/template/logo', { dataUri: '' })).status).toBe(200)
|
||||
expect(getSetting(db, 'template.logo')).toBe('')
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue