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: 'a@b.co', password: 'x' }).success).toBe(true) expect(loginRequestSchema.safeParse({ email: 'bad', password: 'x' }).success).toBe(false) expect(loginRequestSchema.safeParse({ email: 'a@b.co', 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) }) })