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
305 lines
9.4 KiB
TypeScript
305 lines
9.4 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { eq } from 'drizzle-orm'
|
|
import { db } from '~/data/db'
|
|
import { apiClients, brands, postTypes, posts, projects } from '~/data/schema'
|
|
import { hashToken } from '~/lib/api-auth'
|
|
import { POST as createPost } from '~/app/api/v1/posts/route'
|
|
import { DELETE as deletePost, GET as readPost, PATCH as patchPost } from '~/app/api/v1/posts/[slug]/route'
|
|
import { PUT as putBlocks } from '~/app/api/v1/posts/[slug]/blocks/route'
|
|
import { GET as getTypes } from '~/app/api/v1/post-types/route'
|
|
|
|
async function seed() {
|
|
const [brand] = await db.insert(brands).values({ slug: 'pocket-rocket', name: 'Pocket Rocket' }).returning()
|
|
const [trakk] = await db.insert(projects).values({ brandId: brand!.id, slug: 'trakk', name: 'Trakk', code: 'TRK' }).returning()
|
|
const [orbit] = await db.insert(projects).values({ brandId: brand!.id, slug: 'orbit', name: 'Orbit', code: 'ORB' }).returning()
|
|
|
|
await db.insert(postTypes).values([
|
|
{ key: 'feature', labelDe: 'Feature', labelEn: 'Feature', color: '#2b4a9b', sort: 1 },
|
|
{ key: 'fix', labelDe: 'Fix', labelEn: 'Fix', color: '#b4761a', sort: 2 },
|
|
{ key: 'alt', labelDe: 'Alt', labelEn: 'Old', color: '#000000', sort: 9, isActive: false },
|
|
]).onConflictDoNothing()
|
|
|
|
await db.insert(apiClients).values([
|
|
{ name: 'Schreiber', tokenHash: hashToken('lb_w'), mode: 'write', scope: 'customer' },
|
|
{ name: 'Leser', tokenHash: hashToken('lb_r'), mode: 'read', scope: 'customer' },
|
|
{ name: 'Gebunden', tokenHash: hashToken('lb_bound'), mode: 'write', scope: 'customer', projectId: trakk!.id },
|
|
{ name: 'Nur intern', tokenHash: hashToken('lb_int'), mode: 'write', scope: 'internal' },
|
|
])
|
|
|
|
return { trakk: trakk!, orbit: orbit! }
|
|
}
|
|
|
|
function post(body: unknown, token = 'lb_w') {
|
|
return new Request('http://localhost/api/v1/posts', {
|
|
method: 'POST',
|
|
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
})
|
|
}
|
|
|
|
function withId(id: string, method: string, body?: unknown, token = 'lb_w') {
|
|
return new Request(`http://localhost/api/v1/posts/${id}`, {
|
|
method,
|
|
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
})
|
|
}
|
|
|
|
function params(id: string) {
|
|
return { params: Promise.resolve({ slug: id }) }
|
|
}
|
|
|
|
const base = { project: 'trakk', title: 'Regel-Engine', type: 'feature' }
|
|
|
|
async function created(body: Record<string, unknown> = {}, token = 'lb_w') {
|
|
const response = await createPost(post({ ...base, ...body }, token))
|
|
|
|
return { response, body: await response.json() }
|
|
}
|
|
|
|
describe('POST /api/v1/posts', () => {
|
|
it('legt einen Entwurf an, niemals etwas Veröffentlichtes', async () => {
|
|
await seed()
|
|
|
|
const { response, body } = await created({ audience: 'customer' })
|
|
|
|
expect(response.status).toBe(201)
|
|
expect(body.status).toBe('draft')
|
|
expect(body.number).toBeNull()
|
|
expect(body.slug).toBe('regel-engine')
|
|
})
|
|
|
|
it('antwortet 401 ohne Token', async () => {
|
|
await seed()
|
|
|
|
const response = await createPost(new Request('http://localhost/api/v1/posts', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify(base),
|
|
}))
|
|
|
|
expect(response.status).toBe(401)
|
|
})
|
|
|
|
it('lässt einen Lesezugang nicht schreiben', async () => {
|
|
await seed()
|
|
|
|
const { response } = await created({}, 'lb_r')
|
|
|
|
expect(response.status).toBe(403)
|
|
})
|
|
|
|
it('lässt einen gebundenen Zugang nicht in ein fremdes Projekt schreiben', async () => {
|
|
await seed()
|
|
|
|
const { response } = await created({ project: 'orbit' }, 'lb_bound')
|
|
|
|
expect(response.status).toBe(403)
|
|
})
|
|
|
|
it('lehnt eine unbekannte Art ab', async () => {
|
|
await seed()
|
|
|
|
const { response } = await created({ type: 'gibtesnicht' })
|
|
|
|
expect(response.status).toBe(422)
|
|
})
|
|
|
|
it('lehnt eine abgeschaltete Art ab', async () => {
|
|
await seed()
|
|
|
|
const { response } = await created({ type: 'alt' })
|
|
|
|
expect(response.status).toBe(422)
|
|
})
|
|
|
|
it('lässt keine Zielgruppe zu, die offener ist als der Zugang', async () => {
|
|
await seed()
|
|
|
|
const offen = await created({ audience: 'public' })
|
|
const eng = await created({ audience: 'customer' }, 'lb_int')
|
|
|
|
expect(offen.response.status).toBe(403)
|
|
expect(eng.response.status).toBe(403)
|
|
})
|
|
|
|
it('verlangt einen Titel', async () => {
|
|
await seed()
|
|
|
|
const { response } = await created({ title: ' ' })
|
|
|
|
expect(response.status).toBe(400)
|
|
})
|
|
|
|
it('lehnt einen unbekannten Blocktyp ab', async () => {
|
|
await seed()
|
|
|
|
const { response } = await created({ blocks: [{ type: 'unbekannt', data: {} }] })
|
|
|
|
expect(response.status).toBe(400)
|
|
})
|
|
|
|
it('gibt bei gleichem Idempotenz-Schlüssel denselben Beitrag zurück', async () => {
|
|
await seed()
|
|
|
|
const first = await created({ idempotency_key: 'abc' })
|
|
const second = await created({ title: 'Anderer Titel', idempotency_key: 'abc' })
|
|
|
|
expect(second.body.id).toBe(first.body.id)
|
|
expect(second.body.title).toBe('Regel-Engine')
|
|
|
|
const rows = await db.select().from(posts)
|
|
|
|
expect(rows).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
describe('PATCH und DELETE /api/v1/posts/:id', () => {
|
|
it('ändert einen Entwurf', async () => {
|
|
await seed()
|
|
const { body } = await created()
|
|
|
|
const response = await patchPost(withId(body.id, 'PATCH', { title: 'Neuer Titel' }), params(body.id))
|
|
const updated = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(updated.title).toBe('Neuer Titel')
|
|
})
|
|
|
|
it('lehnt eine Änderung an einem veröffentlichten Beitrag mit 409 ab', async () => {
|
|
await seed()
|
|
const { body } = await created()
|
|
|
|
await db.update(posts).set({ status: 'published', publishAt: new Date() }).where(eq(posts.id, body.id))
|
|
|
|
const response = await patchPost(withId(body.id, 'PATCH', { title: 'Zu spät' }), params(body.id))
|
|
|
|
expect(response.status).toBe(409)
|
|
})
|
|
|
|
it('verlangt die Kennung statt des Slugs', async () => {
|
|
await seed()
|
|
await created()
|
|
|
|
const response = await patchPost(withId('regel-engine', 'PATCH', { title: 'X' }), params('regel-engine'))
|
|
|
|
expect(response.status).toBe(400)
|
|
})
|
|
|
|
it('löscht einen Entwurf', async () => {
|
|
await seed()
|
|
const { body } = await created()
|
|
|
|
const response = await deletePost(withId(body.id, 'DELETE'), params(body.id))
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(await db.select().from(posts)).toHaveLength(0)
|
|
})
|
|
|
|
it('löscht nichts, was veröffentlicht ist', async () => {
|
|
await seed()
|
|
const { body } = await created()
|
|
|
|
await db.update(posts).set({ status: 'published', publishAt: new Date() }).where(eq(posts.id, body.id))
|
|
|
|
const response = await deletePost(withId(body.id, 'DELETE'), params(body.id))
|
|
|
|
expect(response.status).toBe(409)
|
|
expect(await db.select().from(posts)).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
describe('PUT /api/v1/posts/:id/blocks', () => {
|
|
it('setzt die Blöcke in der übergebenen Reihenfolge', async () => {
|
|
await seed()
|
|
const { body } = await created()
|
|
|
|
const response = await putBlocks(
|
|
withId(body.id, 'PUT', {
|
|
blocks: [
|
|
{ type: 'quote', data: { text: 'Zitat', source: '' } },
|
|
{ type: 'text', data: { text: 'Text' } },
|
|
],
|
|
}),
|
|
params(body.id),
|
|
)
|
|
const updated = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(updated.blocks.map((block: { type: string }) => block.type)).toEqual(['quote', 'text'])
|
|
})
|
|
|
|
it('lehnt zu viele Blöcke ab', async () => {
|
|
await seed()
|
|
const { body } = await created()
|
|
|
|
const blocks = Array.from({ length: 101 }, () => ({ type: 'text', data: { text: 'x' } }))
|
|
const response = await putBlocks(withId(body.id, 'PUT', { blocks }), params(body.id))
|
|
|
|
expect(response.status).toBe(400)
|
|
})
|
|
})
|
|
|
|
describe('GET /api/v1/post-types', () => {
|
|
it('liefert nur aktive Arten', async () => {
|
|
await seed()
|
|
|
|
const response = await getTypes(new Request('http://localhost/api/v1/post-types', {
|
|
headers: { authorization: 'Bearer lb_r' },
|
|
}))
|
|
const body = await response.json()
|
|
|
|
const keys = body.items.map((item: { key: string }) => item.key)
|
|
|
|
expect(keys).toContain('feature')
|
|
expect(keys).not.toContain('alt')
|
|
})
|
|
})
|
|
|
|
describe('GET /api/v1/posts/:id', () => {
|
|
it('gibt einem Schreibzugang seinen Entwurf zurück', async () => {
|
|
await seed()
|
|
const { body } = await created()
|
|
|
|
const response = await readPost(withId(body.id, 'GET'), params(body.id))
|
|
const read = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(read.id).toBe(body.id)
|
|
expect(read.status).toBe('draft')
|
|
})
|
|
})
|
|
|
|
describe('GET /api/v1/posts/:id gibt nichts heraus, was nicht sichtbar ist', () => {
|
|
it('gibt einem Lesezugang keinen Entwurf', async () => {
|
|
await seed()
|
|
const { body } = await created()
|
|
|
|
const response = await readPost(withId(body.id, 'GET', undefined, 'lb_r'), params(body.id))
|
|
|
|
expect(response.status).toBe(404)
|
|
})
|
|
|
|
it('gibt einem Kundenzugang keinen internen Beitrag', async () => {
|
|
await seed()
|
|
const { body } = await created({ audience: 'internal' }, 'lb_int')
|
|
|
|
await db.update(posts).set({ status: 'published', publishAt: new Date() }).where(eq(posts.id, body.id))
|
|
|
|
const response = await readPost(withId(body.id, 'GET', undefined, 'lb_r'), params(body.id))
|
|
|
|
expect(response.status).toBe(404)
|
|
})
|
|
|
|
it('gibt einem Lesezugang einen veröffentlichten Beitrag seiner Zielgruppe', async () => {
|
|
await seed()
|
|
const { body } = await created({ audience: 'customer' })
|
|
|
|
await db.update(posts).set({ status: 'published', publishAt: new Date() }).where(eq(posts.id, body.id))
|
|
|
|
const response = await readPost(withId(body.id, 'GET', undefined, 'lb_r'), params(body.id))
|
|
|
|
expect(response.status).toBe(200)
|
|
})
|
|
})
|