test: audit-driven coverage for security-relevant pure logic

+16 tests (432 -> 448): permissions-catalog least-privilege guard
(non-board roles carry no high-risk capability; member/associate are
read-only); new invite-logic suite (exact-match creator role, non-admin
grants nothing, invalid/mis-cased targets); door-logic SSRF host-spoof
edges (userinfo-spoofed host rejected, case-insensitive pin match,
trailing-dot FQDN fails closed) + redactDoorSecrets regex-special
password and end-of-string e= value; classes pickPromotion unlimited-
capacity branch + equal-timestamp tie-break.
This commit is contained in:
Moheeb Zara
2026-05-16 22:29:58 -07:00
parent c4cba104d1
commit dc96d6f46a
4 changed files with 104 additions and 0 deletions
+18
View File
@@ -90,3 +90,21 @@ describe('signupFormEligibility', () => {
expect(signupFormEligibility({ requiresForm: true, memberHasForm: false, managerOverride: true }).ok).toBe(true) expect(signupFormEligibility({ requiresForm: true, memberHasForm: false, managerOverride: true }).ok).toBe(true)
}) })
}) })
describe('pickPromotion — unlimited capacity + tie-break', () => {
it('promotes the earliest waitlisted when capacity is null (unlimited)', () => {
const s = [
{ id: 'r1', status: 'registered', signed_up_at: '2026-01-01T00:00:00Z' },
{ id: 'w2', status: 'waitlisted', signed_up_at: '2026-01-03T00:00:00Z' },
{ id: 'w1', status: 'waitlisted', signed_up_at: '2026-01-02T00:00:00Z' },
]
expect(pickPromotion(s, null)).toBe('w1')
})
it('is deterministic on equal signed_up_at (stable earliest by list order)', () => {
const s = [
{ id: 'wA', status: 'waitlisted', signed_up_at: '2026-01-02T00:00:00Z' },
{ id: 'wB', status: 'waitlisted', signed_up_at: '2026-01-02T00:00:00Z' },
]
expect(pickPromotion(s, 5)).toBe('wA')
})
})
+20
View File
@@ -111,4 +111,24 @@ describe('redactDoorSecrets', () => {
it('leaves unrelated text intact', () => { it('leaves unrelated text intact', () => {
expect(redactDoorSecrets('status 200 ok', null)).toBe('status 200 ok') expect(redactDoorSecrets('status 200 ok', null)).toBe('status 200 ok')
}) })
it('redacts a regex-special password literally (split/join, not regex)', () => {
expect(redactDoorSecrets('controller said a.*b ok', 'a.*b')).toBe('controller said <redacted> ok')
})
it('redacts an e= value with no trailing delimiter (end of string)', () => {
expect(redactDoorSecrets('GET /?o1&e=1234')).toBe('GET /?o1&e=<redacted>')
})
})
describe('validateDoorTarget — SSRF host-spoof edges', () => {
it('rejects a userinfo-spoofed host (real host is what after @)', () => {
// new URL(...).hostname is "evil.com", not the pinned LAN IP -> reject
const r = validateDoorTarget('http://[email protected]/?o1', '192.168.1.50')
expect(r.ok).toBe(false)
})
it('matches the pin case-insensitively for hostnames', () => {
expect(validateDoorTarget('http://Door.Local/?9', 'door.local').ok).toBe(true)
})
it('a trailing-dot FQDN does not equal the bare pin (fails closed)', () => {
expect(validateDoorTarget('http://door.local./?9', 'door.local').ok).toBe(false)
})
}) })
+42
View File
@@ -0,0 +1,42 @@
import { describe, it, expect } from 'vitest'
import { canAssignInviteRole, isInviteRole, INVITE_ROLES } from '@/lib/invite-logic'
describe('isInviteRole', () => {
it('accepts exactly the known invite roles', () => {
for (const r of INVITE_ROLES) expect(isInviteRole(r)).toBe(true)
})
it('rejects unknown / mis-cased / padded values', () => {
expect(isInviteRole('Admin')).toBe(false)
expect(isInviteRole(' board ')).toBe(false)
expect(isInviteRole('superuser')).toBe(false)
expect(isInviteRole('')).toBe(false)
})
})
describe('canAssignInviteRole', () => {
it('admin may assign any valid role', () => {
for (const r of INVITE_ROLES) expect(canAssignInviteRole('admin', r)).toBe(true)
})
it('admin cannot assign an invalid target', () => {
expect(canAssignInviteRole('admin', 'superuser')).toBe(false)
})
it('board may assign anything except admin', () => {
expect(canAssignInviteRole('board', 'admin')).toBe(false)
expect(canAssignInviteRole('board', 'board')).toBe(true)
expect(canAssignInviteRole('board', 'treasurer')).toBe(true)
expect(canAssignInviteRole('board', 'member')).toBe(true)
expect(canAssignInviteRole('board', 'associate')).toBe(true)
})
it('treasurer / member / associate may assign nothing', () => {
for (const creator of ['treasurer', 'member', 'associate', '']) {
for (const target of INVITE_ROLES) {
expect(canAssignInviteRole(creator, target)).toBe(false)
}
}
})
it('creatorRole match is exact (no case/whitespace escalation)', () => {
expect(canAssignInviteRole('Admin', 'member')).toBe(false)
expect(canAssignInviteRole(' admin', 'member')).toBe(false)
expect(canAssignInviteRole('BOARD', 'member')).toBe(false)
})
})
+24
View File
@@ -37,4 +37,28 @@ describe('permissions catalog', () => {
it('does not grant admin defaults (admin is implicit and cannot be locked out)', () => { it('does not grant admin defaults (admin is implicit and cannot be locked out)', () => {
expect(DEFAULT_ROLE_PERMISSIONS.admin).toBeUndefined() expect(DEFAULT_ROLE_PERMISSIONS.admin).toBeUndefined()
}) })
// Least-privilege guard: a future catalog edit must not silently hand
// non-board roles a high-risk capability. (treasurer legitimately has
// payments.manage; member/associate should be read-only.)
it('keeps high-risk capabilities off non-board default roles', () => {
const HIGH_RISK = [
'door.manage', 'door.operate', 'members.manage', 'governance.manage',
'settings.manage', 'customize.manage', 'forms.manage',
'certifications.grant', 'ops.secrets.read', 'ops.secrets.write',
'forum.moderate',
]
for (const role of ['treasurer', 'member', 'associate'] as const) {
const perms = DEFAULT_ROLE_PERMISSIONS[role] ?? []
expect(perms.filter(p => HIGH_RISK.includes(p))).toEqual([])
}
})
it('member and associate defaults are read-only', () => {
for (const role of ['member', 'associate'] as const) {
for (const p of DEFAULT_ROLE_PERMISSIONS[role] ?? []) {
expect(p.endsWith('.read')).toBe(true)
}
}
})
}) })