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:
@@ -90,3 +90,21 @@ describe('signupFormEligibility', () => {
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -111,4 +111,24 @@ describe('redactDoorSecrets', () => {
|
||||
it('leaves unrelated text intact', () => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -37,4 +37,28 @@ describe('permissions catalog', () => {
|
||||
it('does not grant admin defaults (admin is implicit and cannot be locked out)', () => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user