test(d28): lock module directory + portals (fieldSpec, login-only, no secret leak)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>feat/client-detail-redesign
parent
69c59815f0
commit
90864b7067
@ -0,0 +1,71 @@
|
|||||||
|
// apps/hq/test/module-access.test.ts — D28: module directory + portals quick-list.
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { openDb } from '../src/db'
|
||||||
|
import { seedIfEmpty } from '../src/seed'
|
||||||
|
import { createClient } from '../src/repos-clients'
|
||||||
|
import { createModule, assignModule, setModuleFieldValues, setModulePassword, moduleDirectory, listPortals } from '../src/repos-modules'
|
||||||
|
|
||||||
|
const KEY = '0'.repeat(64) // valid 32-byte hex key for the test's encryption
|
||||||
|
|
||||||
|
describe('moduleDirectory (D28)', () => {
|
||||||
|
it('returns the fieldSpec and every active client with its values', async () => {
|
||||||
|
const db = openDb(':memory:'); await seedIfEmpty(db)
|
||||||
|
const m = await createModule(db, 'u1', {
|
||||||
|
code: 'RTGS', name: 'RTGS', sac: '998314', allowedKinds: ['yearly'],
|
||||||
|
fieldSpec: [
|
||||||
|
{ key: 'ip', label: 'IP', type: 'text' },
|
||||||
|
{ key: 'portal_url', label: 'Portal URL', type: 'text' },
|
||||||
|
{ key: 'admin_password', label: 'Admin password', type: 'secret' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const a = await createClient(db, 'u1', { name: 'Alpha Bank', stateCode: '32' })
|
||||||
|
const b = await createClient(db, 'u1', { name: 'Beta Bank', stateCode: '32' })
|
||||||
|
const cmA = await assignModule(db, 'u1', { clientId: a.id, moduleId: m.id, kind: 'yearly' })
|
||||||
|
await assignModule(db, 'u1', { clientId: b.id, moduleId: m.id, kind: 'yearly' })
|
||||||
|
await setModuleFieldValues(db, 'u1', cmA.id, { ip: '10.0.0.1', portal_url: 'https://rtgs.example/login' })
|
||||||
|
|
||||||
|
const dir = await moduleDirectory(db, m.id)
|
||||||
|
expect(dir).not.toBeNull()
|
||||||
|
expect(dir!.code).toBe('RTGS')
|
||||||
|
expect(dir!.fieldSpec.length).toBe(3)
|
||||||
|
expect(dir!.total).toBe(2)
|
||||||
|
const rowA = dir!.rows.find((r) => r.clientName === 'Alpha Bank')!
|
||||||
|
expect(rowA.fieldValues.ip).toBe('10.0.0.1')
|
||||||
|
expect(rowA.fieldValues.portal_url).toBe('https://rtgs.example/login')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is null for an unknown module', async () => {
|
||||||
|
const db = openDb(':memory:'); await seedIfEmpty(db)
|
||||||
|
expect(await moduleDirectory(db, 'nope')).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('listPortals (D28)', () => {
|
||||||
|
it('lists only subscriptions with a login and surfaces URL fields (never the password)', async () => {
|
||||||
|
const db = openDb(':memory:'); await seedIfEmpty(db)
|
||||||
|
const m = await createModule(db, 'u1', {
|
||||||
|
code: 'RECOVERX', name: 'RecoverX', sac: '998314', allowedKinds: ['yearly'],
|
||||||
|
fieldSpec: [
|
||||||
|
{ key: 'portal_url', label: 'Portal URL', type: 'text' },
|
||||||
|
{ key: 'note', label: 'Note', type: 'text' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const a = await createClient(db, 'u1', { name: 'Alpha', stateCode: '32' })
|
||||||
|
const b = await createClient(db, 'u1', { name: 'Beta', stateCode: '32' })
|
||||||
|
const cmA = await assignModule(db, 'u1', { clientId: a.id, moduleId: m.id, kind: 'yearly' })
|
||||||
|
const cmB = await assignModule(db, 'u1', { clientId: b.id, moduleId: m.id, kind: 'yearly' })
|
||||||
|
// Alpha has a URL + a stored password; Beta has only a note → no login → excluded.
|
||||||
|
await setModuleFieldValues(db, 'u1', cmA.id, { portal_url: 'https://recoverx.example' })
|
||||||
|
await setModulePassword(db, 'u1', cmA.id, 'secret', KEY)
|
||||||
|
await setModuleFieldValues(db, 'u1', cmB.id, { note: 'nothing here' })
|
||||||
|
|
||||||
|
const portals = await listPortals(db)
|
||||||
|
expect(portals.length).toBe(1)
|
||||||
|
const p = portals[0]!
|
||||||
|
expect(p.clientName).toBe('Alpha')
|
||||||
|
expect(p.hasPassword).toBe(true)
|
||||||
|
expect(p.links.map((l) => l.url)).toContain('https://recoverx.example')
|
||||||
|
// the password itself is never in the payload
|
||||||
|
expect(JSON.stringify(p)).not.toContain('secret')
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue