b90ff252d1
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.
237 lines
5.9 KiB
TypeScript
237 lines
5.9 KiB
TypeScript
import { beforeEach, describe, expect, it } from 'vitest'
|
|
import { listAllProjects } from '~/data/repositories/projects'
|
|
import { performSaveProject } from '~/lib/admin-projects'
|
|
import {
|
|
adminViewer,
|
|
auditEntries,
|
|
form,
|
|
makeBrand,
|
|
makeProject,
|
|
moderatorViewer,
|
|
resetAccounts,
|
|
} from '../support/admin'
|
|
|
|
beforeEach(resetAccounts)
|
|
|
|
describe('performSaveProject legt an', () => {
|
|
it('speichert ein Projekt und schreibt einen Eintrag ins audit_log', async () => {
|
|
const brand = await makeBrand()
|
|
|
|
const state = await performSaveProject(
|
|
adminViewer(),
|
|
form({
|
|
name: 'MTA Telematik',
|
|
slug: '',
|
|
code: 'tel',
|
|
description: 'Telemetrie für die Flotte',
|
|
color: '#B4761A',
|
|
brandId: brand.id,
|
|
sort: '3',
|
|
isActive: 'on',
|
|
}),
|
|
)
|
|
|
|
expect(state.status).toBe('ok')
|
|
expect(state.created).toBe(true)
|
|
|
|
const projects = await listAllProjects()
|
|
|
|
expect(projects).toHaveLength(1)
|
|
expect(projects[0]).toMatchObject({
|
|
slug: 'mta-telematik',
|
|
name: 'MTA Telematik',
|
|
code: 'TEL',
|
|
color: '#b4761a',
|
|
sort: 3,
|
|
isActive: true,
|
|
brandId: brand.id,
|
|
})
|
|
|
|
const entries = await auditEntries()
|
|
|
|
expect(entries).toHaveLength(1)
|
|
expect(entries[0]).toMatchObject({
|
|
action: 'project.create',
|
|
entity: 'project',
|
|
entityId: projects[0]!.id,
|
|
actorId: 'admin-1',
|
|
})
|
|
})
|
|
|
|
it('legt ein stillgelegtes Projekt an, wenn der Haken fehlt', async () => {
|
|
const brand = await makeBrand()
|
|
|
|
await performSaveProject(adminViewer(), form({ name: 'Altprojekt', brandId: brand.id, color: '#191c20' }))
|
|
|
|
const projects = await listAllProjects()
|
|
|
|
expect(projects[0]?.isActive).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('performSaveProject prüft den Slug', () => {
|
|
it('lehnt einen vergebenen Slug ab', async () => {
|
|
const brand = await makeBrand()
|
|
await makeProject(brand.id, 'trakk', 'Trakk')
|
|
|
|
const state = await performSaveProject(
|
|
adminViewer(),
|
|
form({ name: 'Trakk Zwei', slug: 'trakk', brandId: brand.id, color: '#191c20', isActive: 'on' }),
|
|
)
|
|
|
|
expect(state.status).toBe('error')
|
|
expect(state.message).toBe('slugTaken')
|
|
expect(state.fields?.slug).toBe('slugTaken')
|
|
expect(await listAllProjects()).toHaveLength(1)
|
|
})
|
|
|
|
it('erlaubt beim Bearbeiten den eigenen Slug', async () => {
|
|
const brand = await makeBrand()
|
|
const project = await makeProject(brand.id, 'trakk', 'Trakk')
|
|
|
|
const state = await performSaveProject(
|
|
adminViewer(),
|
|
form({
|
|
id: project.id,
|
|
name: 'Trakk',
|
|
slug: 'trakk',
|
|
brandId: brand.id,
|
|
color: '#2e7d5b',
|
|
sort: '1',
|
|
isActive: 'on',
|
|
}),
|
|
)
|
|
|
|
expect(state.status).toBe('ok')
|
|
expect(state.created).toBe(false)
|
|
|
|
const entries = await auditEntries()
|
|
|
|
expect(entries[0]?.action).toBe('project.update')
|
|
})
|
|
|
|
it('schreibt einen eingetippten Slug klein', async () => {
|
|
const brand = await makeBrand()
|
|
|
|
const state = await performSaveProject(
|
|
adminViewer(),
|
|
form({ name: 'Trakk', slug: 'Trakk', brandId: brand.id, color: '#191c20' }),
|
|
)
|
|
|
|
expect(state.status).toBe('ok')
|
|
|
|
const projects = await listAllProjects()
|
|
|
|
expect(projects[0]?.slug).toBe('trakk')
|
|
})
|
|
|
|
it('lehnt einen Slug mit Leerzeichen ab', async () => {
|
|
const brand = await makeBrand()
|
|
|
|
const state = await performSaveProject(
|
|
adminViewer(),
|
|
form({ name: 'Trakk', slug: 'trakk web', brandId: brand.id, color: '#191c20' }),
|
|
)
|
|
|
|
expect(state.status).toBe('error')
|
|
expect(state.fields?.slug).toBe('slugInvalid')
|
|
})
|
|
})
|
|
|
|
describe('performSaveProject prüft die übrigen Felder', () => {
|
|
it('verlangt einen Namen', async () => {
|
|
const brand = await makeBrand()
|
|
|
|
const state = await performSaveProject(adminViewer(), form({ name: '', brandId: brand.id, color: '#191c20' }))
|
|
|
|
expect(state.fields?.name).toBe('nameRequired')
|
|
})
|
|
|
|
it('lehnt eine unbekannte Marke ab', async () => {
|
|
await makeBrand()
|
|
|
|
const state = await performSaveProject(
|
|
adminViewer(),
|
|
form({
|
|
name: 'Trakk',
|
|
brandId: '11111111-1111-4111-8111-111111111111',
|
|
color: '#191c20',
|
|
}),
|
|
)
|
|
|
|
expect(state.fields?.brandId).toBe('brandUnknown')
|
|
expect(await listAllProjects()).toHaveLength(0)
|
|
})
|
|
|
|
it('lehnt eine Farbe ab, die kein Hexwert ist', async () => {
|
|
const brand = await makeBrand()
|
|
|
|
const state = await performSaveProject(
|
|
adminViewer(),
|
|
form({ name: 'Trakk', brandId: brand.id, color: 'rot' }),
|
|
)
|
|
|
|
expect(state.fields?.color).toBe('colorInvalid')
|
|
})
|
|
|
|
it('lehnt eine Sortierung ab, die keine Zahl ist', async () => {
|
|
const brand = await makeBrand()
|
|
|
|
const state = await performSaveProject(
|
|
adminViewer(),
|
|
form({ name: 'Trakk', brandId: brand.id, color: '#191c20', sort: 'oben' }),
|
|
)
|
|
|
|
expect(state.fields?.sort).toBe('sortInvalid')
|
|
})
|
|
})
|
|
|
|
describe('performSaveProject prüft die Rechte', () => {
|
|
it('lässt einen Moderator kein Projekt anlegen', async () => {
|
|
const brand = await makeBrand()
|
|
const project = await makeProject(brand.id)
|
|
|
|
const state = await performSaveProject(
|
|
moderatorViewer([project.id]),
|
|
form({ name: 'Neues Projekt', brandId: brand.id, color: '#191c20', isActive: 'on' }),
|
|
)
|
|
|
|
expect(state.status).toBe('error')
|
|
expect(state.message).toBe('forbidden')
|
|
expect(await listAllProjects()).toHaveLength(1)
|
|
expect(await auditEntries()).toHaveLength(0)
|
|
})
|
|
|
|
it('lässt einen Moderator sein eigenes Projekt nicht bearbeiten', async () => {
|
|
const brand = await makeBrand()
|
|
const project = await makeProject(brand.id)
|
|
|
|
const state = await performSaveProject(
|
|
moderatorViewer([project.id]),
|
|
form({ id: project.id, name: 'Umbenannt', brandId: brand.id, color: '#191c20', isActive: 'on' }),
|
|
)
|
|
|
|
expect(state.message).toBe('forbidden')
|
|
|
|
const projects = await listAllProjects()
|
|
|
|
expect(projects[0]?.name).toBe('Trakk')
|
|
})
|
|
|
|
it('meldet einen unbekannten Datensatz beim Bearbeiten', async () => {
|
|
const brand = await makeBrand()
|
|
|
|
const state = await performSaveProject(
|
|
adminViewer(),
|
|
form({
|
|
id: '11111111-1111-4111-8111-111111111111',
|
|
name: 'Trakk',
|
|
brandId: brand.id,
|
|
color: '#191c20',
|
|
}),
|
|
)
|
|
|
|
expect(state.message).toBe('notFound')
|
|
})
|
|
})
|