Files
logbuch/tests/lib/admin-clients.test.ts
T
Matthias Giesselmann b90ff252d1 Add Logbuch: project update blog with admin, media and API
Public archive with sidebar navigation, project pages, month archive,
search and entry pages with image blocks. Admin area for brands,
projects, post types, users, api clients, media and the entry editor
with drag and drop images, preview per audience, scheduling and
publish checks. Read and write API with bearer tokens, audience
scoping, idempotent creation, OpenAPI document and editorial guide.
Magic link login with configurable allowed domains, whole app behind
the session gate. 456 tests including design rule checks.
2026-07-31 21:33:42 +02:00

160 lines
4.7 KiB
TypeScript

import { beforeEach, describe, expect, it } from 'vitest'
import { findActiveClientByTokenHash, listClients } from '~/data/repositories/clients'
import { performCreateClient, performRevokeClient, tokenPrefix } from '~/lib/admin-clients'
import { hashToken } from '~/lib/api-auth'
import {
adminViewer,
auditEntries,
form,
makeBrand,
makeProject,
moderatorViewer,
resetAccounts,
} from '../support/admin'
beforeEach(resetAccounts)
describe('performCreateClient', () => {
it('legt einen Zugang an, zeigt das Token einmal und speichert nur den Hash', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
const state = await performCreateClient(
adminViewer(),
form({ name: 'Trakk Web', mode: 'read', scope: 'customer', projectId: project.id }),
)
expect(state.status).toBe('ok')
expect(state.token?.startsWith(tokenPrefix)).toBe(true)
const clients = await listClients()
expect(clients).toHaveLength(1)
expect(clients[0]).toMatchObject({
name: 'Trakk Web',
mode: 'read',
scope: 'customer',
projectId: project.id,
canPublish: false,
revokedAt: null,
})
expect(clients[0]?.tokenHash).not.toBe(state.token)
expect(clients[0]?.tokenHash).toBe(hashToken(state.token!))
const found = await findActiveClientByTokenHash(hashToken(state.token!))
expect(found?.id).toBe(clients[0]?.id)
})
it('protokolliert das Anlegen ohne das Token', async () => {
await performCreateClient(adminViewer(), form({ name: 'Trakk Web', mode: 'read', scope: 'customer' }))
const entries = await auditEntries()
expect(entries).toHaveLength(1)
expect(entries[0]).toMatchObject({ action: 'client.create', entity: 'api_client' })
expect(JSON.stringify(entries[0]?.data)).not.toContain(tokenPrefix)
})
it('lässt die Projektbindung leer, wenn kein Projekt gewählt ist', async () => {
const state = await performCreateClient(
adminViewer(),
form({ name: 'Alle Projekte', mode: 'read', scope: 'internal', projectId: '' }),
)
expect(state.status).toBe('ok')
expect((await listClients())[0]?.projectId).toBeNull()
})
it('lehnt ein unbekanntes Projekt ab', async () => {
const state = await performCreateClient(
adminViewer(),
form({
name: 'Fremd',
mode: 'read',
scope: 'customer',
projectId: '11111111-1111-4111-8111-111111111111',
}),
)
expect(state.fields?.projectId).toBe('projectUnknown')
expect(await listClients()).toHaveLength(0)
})
it('lehnt einen unbekannten Modus ab', async () => {
const state = await performCreateClient(
adminViewer(),
form({ name: 'Fremd', mode: 'alles', scope: 'customer' }),
)
expect(state.fields?.mode).toBe('modeInvalid')
})
it('lässt einen Moderator keinen Zugang anlegen', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
const state = await performCreateClient(
moderatorViewer([project.id]),
form({ name: 'Eigener Zugang', mode: 'read', scope: 'customer', projectId: project.id }),
)
expect(state.message).toBe('forbidden')
expect(await listClients()).toHaveLength(0)
expect(await auditEntries()).toHaveLength(0)
})
})
describe('performRevokeClient', () => {
it('widerruft einen Zugang und protokolliert das', async () => {
const created = await performCreateClient(
adminViewer(),
form({ name: 'Trakk Web', mode: 'read', scope: 'customer' }),
)
const state = await performRevokeClient(adminViewer(), form({ id: created.id! }))
expect(state.status).toBe('ok')
const clients = await listClients()
expect(clients[0]?.revokedAt).toBeInstanceOf(Date)
expect((await auditEntries())[0]?.action).toBe('client.revoke')
})
it('lässt ein widerrufenes Token nicht mehr durch', async () => {
const created = await performCreateClient(
adminViewer(),
form({ name: 'Trakk Web', mode: 'read', scope: 'customer' }),
)
await performRevokeClient(adminViewer(), form({ id: created.id! }))
expect(await findActiveClientByTokenHash(hashToken(created.token!))).toBeUndefined()
})
it('meldet einen zweiten Widerruf als unbekannt', async () => {
const created = await performCreateClient(
adminViewer(),
form({ name: 'Trakk Web', mode: 'read', scope: 'customer' }),
)
await performRevokeClient(adminViewer(), form({ id: created.id! }))
const state = await performRevokeClient(adminViewer(), form({ id: created.id! }))
expect(state.message).toBe('notFound')
})
it('lässt einen Moderator keinen Zugang widerrufen', async () => {
const created = await performCreateClient(
adminViewer(),
form({ name: 'Trakk Web', mode: 'read', scope: 'customer' }),
)
const state = await performRevokeClient(moderatorViewer(), form({ id: created.id! }))
expect(state.message).toBe('forbidden')
expect((await listClients())[0]?.revokedAt).toBeNull()
})
})