ci: покрытие и пороги, тесты shared/api, smoke образа и бюджеты, документация
This commit is contained in:
1 parent
22dd6f9937
commit
5cbf4d29d7
15 files changed
+952
-11
No files matched your search
@@ -0,0 +1,199 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user