feat(mim): admin QR/brand API, public config hooks, and deploy workflow fix.
Split mim-api into modular routes, refresh nav brand assets, and pin reusable validate workflow to Order-of-Hospitallers/miracles_in_motion@main. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { defaultMimCustomizations } from '../src/lib/qrcg.js'
|
||||
import { defaultMonkeyConfig } from '../src/lib/qr-monkey.js'
|
||||
import { mimQrBrandSpec } from '../src/lib/qr-brand.js'
|
||||
import { getQrProvider } from '../src/lib/qr-provider.js'
|
||||
|
||||
describe('qr brand customizations', () => {
|
||||
it('qrcg defaults use forest green / gold diamond styling', () => {
|
||||
const c = defaultMimCustomizations()
|
||||
assert.equal(c.background.color, '#1a3c34')
|
||||
assert.equal(c.pattern.color, '#c5a059')
|
||||
assert.equal(c.pattern.shape, 'diamond')
|
||||
})
|
||||
|
||||
it('qrcode monkey defaults match approved reference', () => {
|
||||
const c = defaultMonkeyConfig('https://mim4u.org')
|
||||
assert.equal(c.bgColor, '#1a3c34')
|
||||
assert.equal(c.bodyColor, '#c5a059')
|
||||
assert.equal(c.body, 'diamond')
|
||||
assert.equal(c.eye, 'frame14')
|
||||
assert.match(c.logo, /logo-square/)
|
||||
})
|
||||
|
||||
it('exposes brand spec with reference image path', () => {
|
||||
const spec = mimQrBrandSpec('https://mim4u.org')
|
||||
assert.equal(spec.referenceImage, '/brand/qr-code-branded-reference.png')
|
||||
assert.equal(spec.colors.background, '#1a3c34')
|
||||
})
|
||||
|
||||
it('defaults to qrcode-monkey provider', () => {
|
||||
const prev = process.env.MIM_QR_PROVIDER
|
||||
delete process.env.MIM_QR_PROVIDER
|
||||
assert.equal(getQrProvider(), 'qrcode-monkey')
|
||||
if (prev) process.env.MIM_QR_PROVIDER = prev
|
||||
})
|
||||
})
|
||||
+114
-16
@@ -1,21 +1,9 @@
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
/** Inline validation mirror — keep in sync with server.js validateAssistance */
|
||||
function validateAssistance(body) {
|
||||
const errors = []
|
||||
if (!body?.requestType) errors.push('requestType required')
|
||||
if (!body?.studentInfo?.firstName?.trim()) errors.push('studentInfo.firstName required')
|
||||
if (!body?.studentInfo?.lastName?.trim()) errors.push('studentInfo.lastName required')
|
||||
if (!body?.studentInfo?.school?.trim()) errors.push('studentInfo.school required')
|
||||
if (!body?.contactInfo?.parentName?.trim()) errors.push('contactInfo.parentName required')
|
||||
if (!body?.contactInfo?.phone?.trim()) errors.push('contactInfo.phone required')
|
||||
if (!body?.contactInfo?.email?.trim()) errors.push('contactInfo.email required')
|
||||
if (!body?.contactInfo?.relationship?.trim()) errors.push('contactInfo.relationship required')
|
||||
if (!body?.details?.trim()) errors.push('details required')
|
||||
if (body?.website) errors.push('spam detected')
|
||||
return errors
|
||||
}
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { createApp, validateAssistance } from '../src/app.js'
|
||||
|
||||
describe('assistance validation shape', () => {
|
||||
it('requires core fields', () => {
|
||||
@@ -28,3 +16,113 @@ describe('assistance validation shape', () => {
|
||||
assert.ok(errors.includes('spam detected'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('admin auth and settings', () => {
|
||||
it('logs in admin and updates stripe settings', async () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'mim-api-test-'))
|
||||
const { app, AUTH_SECRET } = createApp({ dataDir: tmp, authSecret: 'test-secret', port: 0 })
|
||||
const server = app.listen(0)
|
||||
const { port } = server.address()
|
||||
|
||||
const loginRes = await fetch(`http://127.0.0.1:${port}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: '[email protected]', password: 'demo123' }),
|
||||
})
|
||||
assert.equal(loginRes.status, 200)
|
||||
const { token } = await loginRes.json()
|
||||
assert.ok(token)
|
||||
|
||||
const settingsRes = await fetch(`http://127.0.0.1:${port}/api/admin/settings`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
assert.equal(settingsRes.status, 200)
|
||||
|
||||
const patchRes = await fetch(`http://127.0.0.1:${port}/api/admin/settings`, {
|
||||
method: 'PATCH',
|
||||
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
donationsEnabled: true,
|
||||
stripePublishableKey: 'pk_test_abc',
|
||||
stripeSecretKey: 'sk_test_abc',
|
||||
stripeWebhookSecret: 'whsec_test',
|
||||
}),
|
||||
})
|
||||
assert.equal(patchRes.status, 200)
|
||||
const patched = await patchRes.json()
|
||||
assert.equal(patched.donationsEnabled, true)
|
||||
assert.match(patched.stripeSecretKey, /…/)
|
||||
|
||||
const publicRes = await fetch(`http://127.0.0.1:${port}/api/public/config`)
|
||||
const pub = await publicRes.json()
|
||||
assert.equal(pub.stripeConfigured, true)
|
||||
assert.equal(pub.stripePublishableKey, 'pk_test_abc')
|
||||
|
||||
server.close()
|
||||
fs.rmSync(tmp, { recursive: true, force: true })
|
||||
assert.ok(AUTH_SECRET)
|
||||
})
|
||||
})
|
||||
|
||||
describe('qr admin routes', () => {
|
||||
it('reports qrcode-monkey as default provider', async () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'mim-api-qr-'))
|
||||
const { app } = createApp({ dataDir: tmp, authSecret: 'test-secret', port: 0 })
|
||||
const server = app.listen(0)
|
||||
const { port } = server.address()
|
||||
|
||||
const loginRes = await fetch(`http://127.0.0.1:${port}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: '[email protected]', password: 'demo123' }),
|
||||
})
|
||||
const { token } = await loginRes.json()
|
||||
|
||||
const statusRes = await fetch(`http://127.0.0.1:${port}/api/admin/qrcodes/status`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
assert.equal(statusRes.status, 200)
|
||||
const status = await statusRes.json()
|
||||
assert.equal(status.provider, 'qrcode-monkey')
|
||||
assert.equal(status.configured, true)
|
||||
assert.equal(status.dynamicTracking, false)
|
||||
|
||||
server.close()
|
||||
fs.rmSync(tmp, { recursive: true, force: true })
|
||||
})
|
||||
})
|
||||
describe('brand manifest', () => {
|
||||
it('seeds and serves public brand manifest', async () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'mim-api-brand-'))
|
||||
const { app } = createApp({ dataDir: tmp, authSecret: 'test-secret', port: 0 })
|
||||
const server = app.listen(0)
|
||||
const { port } = server.address()
|
||||
|
||||
const publicRes = await fetch(`http://127.0.0.1:${port}/api/public/brand`)
|
||||
assert.equal(publicRes.status, 200)
|
||||
const manifest = await publicRes.json()
|
||||
assert.ok(manifest.groups?.length > 0)
|
||||
assert.ok(manifest.colors?.length > 0)
|
||||
|
||||
const loginRes = await fetch(`http://127.0.0.1:${port}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: '[email protected]', password: 'demo123' }),
|
||||
})
|
||||
const { token } = await loginRes.json()
|
||||
|
||||
const patchRes = await fetch(`http://127.0.0.1:${port}/api/admin/brand`, {
|
||||
method: 'PATCH',
|
||||
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ published: false }),
|
||||
})
|
||||
assert.equal(patchRes.status, 200)
|
||||
|
||||
const hidden = await fetch(`http://127.0.0.1:${port}/api/public/brand`)
|
||||
const hiddenJson = await hidden.json()
|
||||
assert.equal(hiddenJson.published, false)
|
||||
|
||||
server.close()
|
||||
fs.rmSync(tmp, { recursive: true, force: true })
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user