Files
auth-portal/packages/shared/test/contracts.test.ts
T

200 lines
7.6 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
APP_IDS,
allPermissionKeys,
appsMetaFromSwitcher,
buildSsoRedirectUrl,
createOidcClientRequestSchema,
defaultAppSwitcherConfig,
hasPermission,
isJwtExpired,
isPortalOidcAuthorizeUrl,
isReturnToAllowed,
loginRequestSchema,
normalizeAppSwitcherConfig,
normalizePermissionKeys,
oidcGroupsForUser,
parseAppSwitcherConfig,
permissionKey,
publicAppSwitcherConfig,
readJwtExp,
readJwtPayload,
tenantsClaimForUser,
} from '../src/index.js'
function jwt(payload: object): string {
const b64 = (o: object) => Buffer.from(JSON.stringify(o)).toString('base64url')
return `${b64({ alg: 'none' })}.${b64(payload)}.sig`
}
describe('permissions', () => {
it('иерархия admin ⊃ write ⊃ read в пределах секции', () => {
expect(hasPermission(['dns:dns:admin'], 'dns:dns:write')).toBe(true)
expect(hasPermission(['dns:dns:admin'], 'dns:dns:read')).toBe(true)
expect(hasPermission(['dns:dns:write'], 'dns:dns:read')).toBe(true)
expect(hasPermission(['dns:dns:read'], 'dns:dns:write')).toBe(false)
expect(hasPermission(['dns:dns:admin'], 'dns:dhcp:read')).toBe(false)
expect(hasPermission([], 'broken')).toBe(false)
})
it('permissionKey / allPermissionKeys согласованы', () => {
expect(permissionKey('dns', 'dns', 'read')).toBe('dns:dns:read')
const all = allPermissionKeys()
expect(new Set(all).size).toBe(all.length)
expect(all).toContain('dns:dns:read')
})
it('normalizePermissionKeys: легаси мапятся, неизвестные отбрасываются, результат отсортирован', () => {
const out = normalizePermissionKeys([
'bgp:peers:read',
'bgp:settings:admin',
'nope:x:read',
'dns:dns:read',
'dns:dns:read',
])
expect(out).toEqual([...out].sort())
expect(out).toContain('bgp:network:read')
expect(out).toContain('bgp:tenant_settings:admin')
expect(out).toContain('dns:dns:read')
expect(out).not.toContain('nope:x:read')
expect(out.filter((k) => k === 'dns:dns:read')).toHaveLength(1)
})
})
describe('oidcGroupsForUser', () => {
it('админ получает все группы Technitium', () => {
expect(oidcGroupsForUser({ isAdmin: true, permissions: [] })).toEqual([
'technitium_admins',
'technitium_dns_admins',
'technitium_dhcp_admins',
])
})
it('права dns/dhcp дают соответствующие группы', () => {
expect(
oidcGroupsForUser({ isAdmin: false, permissions: ['dns:dns:write'] }),
).toEqual(['technitium_dns_admins'])
expect(
oidcGroupsForUser({ isAdmin: false, permissions: ['dns:dhcp:admin'] }),
).toEqual(['technitium_dhcp_admins'])
expect(oidcGroupsForUser({ isAdmin: false, permissions: [] })).toEqual([])
})
})
describe('isReturnToAllowed', () => {
it.each([
['https://cfdm.shnt.top/x', '.shnt.top', true],
['https://shnt.top', '.shnt.top', true],
['https://evilshnt.top', '.shnt.top', false],
['http://localhost:5173/cb', 'localhost', true],
['http://192.168.1.5/x', 'private', true],
['http://172.32.0.1/x', 'private', false],
['https://8.8.8.8', 'private', false],
['http://localhost:5173/a', 'http://localhost:5173', true],
['http://localhost:5174/a', 'http://localhost:5173', false],
['javascript:alert(1)', 'localhost', false],
['not a url', 'localhost', false],
])('%s vs %s → %s', (url, list, expected) => {
expect(isReturnToAllowed(url, list)).toBe(expected)
})
})
describe('JWT helpers', () => {
it('читает payload и exp, определяет протухание с запасом 30 c', () => {
const exp = Math.floor(Date.now() / 1000) + 3600
const t = jwt({ sub: 'u1', exp })
expect(readJwtPayload(t)).toMatchObject({ sub: 'u1' })
expect(readJwtExp(t)).toBe(exp)
expect(isJwtExpired(t)).toBe(false)
expect(isJwtExpired(jwt({ exp: 1 }))).toBe(true)
const justExpired = jwt({ exp: Math.floor(Date.now() / 1000) - 10 })
expect(isJwtExpired(justExpired)).toBe(false)
expect(isJwtExpired(jwt({ sub: 'no-exp' }))).toBe(true)
expect(readJwtPayload('garbage')).toBeNull()
expect(readJwtExp('a.b')).toBeNull()
})
it('buildSsoRedirectUrl кладёт токен во фрагмент и заменяет старый', () => {
const url = buildSsoRedirectUrl('https://a.shnt.top/p?x=1#old', 'T', 'E')
expect(url.startsWith('https://a.shnt.top/p?x=1#')).toBe(true)
expect(url).toContain('access_token=T')
expect(url).not.toContain('old')
})
})
describe('app switcher', () => {
it('default содержит все приложения', () => {
const d = defaultAppSwitcherConfig()
expect(d.apps.map((a) => a.id).sort()).toEqual([...APP_IDS].sort())
})
it('parseAppSwitcherConfig: мусор → default, неполный конфиг дополняется', () => {
expect(parseAppSwitcherConfig({ nope: 1 })).toEqual(defaultAppSwitcherConfig())
const partial = parseAppSwitcherConfig({
menuLabel: 'Меню',
apps: [
{
id: 'dns',
name: 'DNS!',
url: 'https://dns.example.com',
icon: 'globe',
enabled: false,
tenantId: ' ',
},
],
})
expect(partial.menuLabel).toBe('Меню')
expect(partial.apps).toHaveLength(APP_IDS.length)
const dns = partial.apps.find((a) => a.id === 'dns')!
expect(dns.name).toBe('DNS!')
expect(dns.enabled).toBe(false)
expect(dns.tenantId).toBeUndefined()
})
it('public-конфиг без tenantId; tenants-claim только по доступным приложениям', () => {
const base = defaultAppSwitcherConfig()
const cfg = normalizeAppSwitcherConfig({
...base,
apps: base.apps.map((a) => (a.id === 'bgp' ? { ...a, tenantId: 'tenant-1' } : a)),
})
expect(JSON.stringify(publicAppSwitcherConfig(cfg))).not.toContain('tenant-1')
expect(tenantsClaimForUser(cfg, ['bgp'])).toEqual({ bgp: 'tenant-1' })
expect(tenantsClaimForUser(cfg, ['dns'])).toEqual({})
})
it('appsMetaFromSwitcher скрывает отключённые приложения', () => {
const cfg = defaultAppSwitcherConfig()
cfg.apps = cfg.apps.map((a) => (a.id === 'dns' ? { ...a, enabled: false } : a))
expect(appsMetaFromSwitcher(cfg).some((a) => a.id === 'dns')).toBe(false)
})
})
describe('zod-схемы', () => {
it('login: email и непустой пароль обязательны', () => {
expect(loginRequestSchema.safeParse({ email: '[email protected]', password: 'x' }).success).toBe(true)
expect(loginRequestSchema.safeParse({ email: 'bad', password: 'x' }).success).toBe(false)
expect(loginRequestSchema.safeParse({ email: '[email protected]', password: '' }).success).toBe(false)
})
it('createOidcClient: scopes по умолчанию, redirect_uris не пустой', () => {
const ok = createOidcClientRequestSchema.parse({
name: 'c',
redirect_uris: ['https://x.example/cb'],
})
expect(ok.scopes).toContain('openid')
expect(ok.enabled).toBe(true)
expect(
createOidcClientRequestSchema.safeParse({ name: 'c', redirect_uris: [] }).success,
).toBe(false)
})
it('isPortalOidcAuthorizeUrl', () => {
const iss = 'https://auth.shnt.top'
expect(isPortalOidcAuthorizeUrl(`${iss}/oauth/authorize?x=1`, iss)).toBe(true)
expect(isPortalOidcAuthorizeUrl(`${iss}/oauth/authorize/`, iss)).toBe(true)
expect(isPortalOidcAuthorizeUrl('https://evil.example/oauth/authorize', iss)).toBe(false)
expect(isPortalOidcAuthorizeUrl(`${iss}/other`, iss)).toBe(false)
expect(isPortalOidcAuthorizeUrl('nope', iss)).toBe(false)
})
})