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 { 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) }) })