2fd71fd406
- 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
206 lines
6.7 KiB
TypeScript
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)
|
|
})
|
|
})
|