Files
logbuch/tests/data/entries.test.ts
T
Matthias G 2fd71fd406 Number entries on publish, delete media, read the cover
- drafts stay unnumbered, the number is handed out when an entry goes live
- DELETE /api/v1/media/:id and a delete control in the media library
- cover_media_id in the draft read, coverMediaId and coverUrl in the archive read
2026-08-01 12:25:01 +02:00

206 lines
6.7 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { asc, eq } from 'drizzle-orm'
import { db } from '~/data/db'
import { posts } from '~/data/schema'
import {
assignEntryNumber,
createEntry,
listEntries,
listEntryBlocks,
replaceEntryBlocks,
setEntryStatus,
type EntryCreateValues,
} from '~/data/repositories/entries'
import { makeBrand, makeProject } from '../support/admin'
import { makePost } from '../support/entries'
import { postTypeId } from '../support/post-types'
function values(projectId: string, slug: string, title = 'Beitrag'): EntryCreateValues {
return {
projectId,
title,
teaser: null,
slug,
typeId: postTypeId('feature'),
audience: 'internal',
coverMediaId: null,
authorName: 'Paul',
}
}
async function numbersOf(projectId: string): Promise<(number | null)[]> {
const rows = await db
.select({ number: posts.number })
.from(posts)
.where(eq(posts.projectId, projectId))
.orderBy(asc(posts.number))
return rows.map(row => row.number)
}
async function publish(id: string): Promise<number | null> {
const post = await setEntryStatus(id, 'published', new Date())
return post?.number ?? null
}
describe('Beitragsnummern', () => {
it('lässt Entwürfe ohne Nummer', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
const created = await createEntry(values(project.id, 'entwurf'))
expect(created.number).toBeNull()
expect(await numbersOf(project.id)).toEqual([null])
})
it('zählt beim Veröffentlichen je Projekt getrennt hoch', async () => {
const brand = await makeBrand()
const trakk = await makeProject(brand.id, 'trakk', 'Trakk')
const orbit = await makeProject(brand.id, 'orbit', 'Orbit')
const first = await createEntry(values(trakk.id, 'eins'))
const second = await createEntry(values(trakk.id, 'zwei'))
const third = await createEntry(values(orbit.id, 'eins'))
expect(await publish(first.id)).toBe(1)
expect(await publish(second.id)).toBe(2)
expect(await publish(third.id)).toBe(1)
})
it('lässt keine Lücke, wenn ein Entwurf dazwischen gelöscht wird', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
const first = await createEntry(values(project.id, 'eins'))
const scrapped = await createEntry(values(project.id, 'verworfen'))
const second = await createEntry(values(project.id, 'zwei'))
await publish(first.id)
await db.delete(posts).where(eq(posts.id, scrapped.id))
await publish(second.id)
expect(await numbersOf(project.id)).toEqual([1, 2])
})
it('vergibt bei gleichzeitigem Veröffentlichen lückenlose Nummern ohne Doppelung', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
const created = await Promise.all(
Array.from({ length: 20 }, (_, index) => createEntry(values(project.id, `beitrag-${index}`))),
)
await Promise.all(created.map(entry => publish(entry.id)))
expect(await numbersOf(project.id)).toEqual(Array.from({ length: 20 }, (_, index) => index + 1))
})
it('setzt hinter bereits vergebenen Nummern fort', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
await makePost(project.id, { number: 142, slug: 'alter-beitrag' })
const created = await createEntry(values(project.id, 'neuer-beitrag'))
expect(await publish(created.id)).toBe(143)
})
it('vergibt eine bereits gesetzte Nummer nicht neu', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
const created = await createEntry(values(project.id, 'eins'))
await publish(created.id)
const again = await assignEntryNumber(created.id)
expect(again?.number).toBe(1)
})
it('hängt eine Zahl an, wenn der Slug im Projekt schon vergeben ist', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
const first = await createEntry(values(project.id, 'regel-engine'))
const second = await createEntry(values(project.id, 'regel-engine'))
expect(first.slug).toBe('regel-engine')
expect(second.slug).toBe('regel-engine-2')
})
it('übernimmt Titel, Autor und Zielgruppe', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
const created = await createEntry({ ...values(project.id, 'regel-engine', 'Regel-Engine'), audience: 'customer' })
expect(created).toMatchObject({
title: 'Regel-Engine',
authorName: 'Paul',
audience: 'customer',
status: 'draft',
number: null,
})
})
})
describe('replaceEntryBlocks', () => {
it('speichert Blöcke in der übergebenen Reihenfolge', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
const entry = await createEntry(values(project.id, 'mit-bloecken'))
await replaceEntryBlocks(entry.id, [
{ type: 'text', data: { text: 'Erster' } },
{ type: 'quote', data: { text: 'Zweiter', source: 'Paul' } },
{ type: 'text', data: { text: 'Dritter' } },
])
const rows = await listEntryBlocks(entry.id)
expect(rows.map(row => row.type)).toEqual(['text', 'quote', 'text'])
expect(rows.map(row => row.sort)).toEqual([0, 1, 2])
expect(rows[1]?.data).toMatchObject({ text: 'Zweiter', source: 'Paul' })
})
it('ersetzt vorhandene Blöcke vollständig', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
const entry = await createEntry(values(project.id, 'ersetzt'))
await replaceEntryBlocks(entry.id, [{ type: 'text', data: { text: 'Alt' } }])
await replaceEntryBlocks(entry.id, [{ type: 'callout', data: { text: 'Neu', title: '', tone: 'info' } }])
const rows = await listEntryBlocks(entry.id)
expect(rows).toHaveLength(1)
expect(rows[0]?.type).toBe('callout')
})
})
describe('listEntries', () => {
it('grenzt auf Projekte, Status und Suche ein', async () => {
const brand = await makeBrand()
const trakk = await makeProject(brand.id, 'trakk', 'Trakk')
const orbit = await makeProject(brand.id, 'orbit', 'Orbit')
await makePost(trakk.id, { number: 1, slug: 'regel-engine', title: 'Regel-Engine', status: 'draft' })
await makePost(trakk.id, { number: 2, slug: 'sla-uhr', title: 'SLA-Uhr', status: 'published' })
await makePost(orbit.id, { number: 1, slug: 'eingang', title: 'Ein Eingang', status: 'draft' })
const mine = await listEntries({ projectIds: [trakk.id], page: 1, perPage: 25 })
const drafts = await listEntries({ status: 'draft', page: 1, perPage: 25 })
const found = await listEntries({ search: 'sla', page: 1, perPage: 25 })
const none = await listEntries({ projectIds: [], page: 1, perPage: 25 })
expect(mine.total).toBe(2)
expect(drafts.items.map(item => item.slug).sort()).toEqual(['eingang', 'regel-engine'])
expect(found.items.map(item => item.slug)).toEqual(['sla-uhr'])
expect(none.total).toBe(0)
})
})