Files
logbuch/tests/lib/report-settings.test.ts
T
Matthias G 5ef9cd704e Deleting a digest makes it sendable again
The dispatch timestamp survived the deletion, so removing a message in Slack
left the digest permanently swallowed. Deleting now rewinds the window to the
newest digest that still stands.
2026-08-03 13:32:01 +02:00

181 lines
6.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { db } from '~/data/db'
import { appSettings } from '~/data/schema'
import { listReportRuns, writeSetting } from '~/data/repositories/settings'
import { readSlackConfig, reportScope, runWeeklyReport } from '~/lib/report-settings'
import { performDeleteMessage } from '~/lib/admin-settings'
import { makeBrand, makeProject } from '../support/admin'
import { makePost } from '../support/entries'
const origin = 'https://logbuch.nyo.de'
beforeEach(async () => {
await db.delete(appSettings)
delete process.env.SLACK_WEBHOOK_URL
vi.unstubAllGlobals()
})
describe('readSlackConfig', () => {
it('nimmt den hinterlegten Webhook vor der Umgebungsvariable', async () => {
process.env.SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/aus/der/umgebung'
await writeSetting('slack.webhook', 'https://hooks.slack.com/services/aus/der/verwaltung')
const config = await readSlackConfig()
expect(config.target).toMatchObject({ kind: 'webhook' })
expect(config.target && 'url' in config.target ? config.target.url : '').toContain('verwaltung')
expect(config.fromEnv).toBe(false)
})
it('fällt auf die Umgebungsvariable zurück', async () => {
process.env.SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/aus/der/umgebung'
const config = await readSlackConfig()
expect(config.fromEnv).toBe(true)
})
it('schickt interne Beiträge mit, solange nichts anderes gesetzt ist', async () => {
expect(reportScope(await readSlackConfig())).toBe('internal')
await writeSetting('slack.includeInternal', 'false')
expect(reportScope(await readSlackConfig())).toBe('customer')
})
})
describe('runWeeklyReport', () => {
it('vermerkt einen Lauf ohne Veröffentlichungen, ohne zu senden', async () => {
const fetchMock = vi.fn()
vi.stubGlobal('fetch', fetchMock)
const outcome = await runWeeklyReport({ origin, trigger: 'test' })
expect(outcome.reason).toBe('nothing_published')
expect(fetchMock).not.toHaveBeenCalled()
const runs = await listReportRuns(1)
expect(runs[0]).toMatchObject({ sent: false, total: 0, trigger: 'test', detail: 'nothing_published' })
})
})
describe('Bot-Token', () => {
it('nimmt Bot-Token und Kanal vor dem Webhook', async () => {
await writeSetting('slack.webhook', 'https://hooks.slack.com/services/a/b/c')
await writeSetting('slack.botToken', 'xoxb-1234')
await writeSetting('slack.channel', 'logbuch')
const config = await readSlackConfig()
expect(config.target).toEqual({ kind: 'bot', token: 'xoxb-1234', channel: 'logbuch' })
expect(config.hasBot).toBe(true)
})
it('braucht zum Bot-Token einen Kanal', async () => {
await writeSetting('slack.botToken', 'xoxb-1234')
const config = await readSlackConfig()
expect(config.target).toBeUndefined()
expect(config.hasBot).toBe(true)
})
it('merkt sich Kanal und Nachrichtenkennung eines Versands', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
const post = await makePost(project.id, { number: 1, slug: 'eins', status: 'published', publishAt: new Date() })
expect(post.id).toBeDefined()
await writeSetting('slack.botToken', 'xoxb-1234')
await writeSetting('slack.channel', 'logbuch')
vi.stubGlobal('fetch', vi.fn(async () => new Response(
JSON.stringify({ ok: true, channel: 'C999', ts: '1785600000.000100' }),
{ headers: { 'content-type': 'application/json' } },
)))
const outcome = await runWeeklyReport({ origin, trigger: 'test' })
expect(outcome.reason).toBe('sent')
expect(outcome.run).toMatchObject({ sent: true, channel: 'C999', messageTs: '1785600000.000100' })
})
it('vermerkt eine Ablehnung mit dem Grund von Slack', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
await makePost(project.id, { number: 1, slug: 'eins', status: 'published', publishAt: new Date() })
await writeSetting('slack.botToken', 'xoxb-1234')
await writeSetting('slack.channel', 'logbuch')
vi.stubGlobal('fetch', vi.fn(async () => new Response(
JSON.stringify({ ok: false, error: 'channel_not_found' }),
{ headers: { 'content-type': 'application/json' } },
)))
const outcome = await runWeeklyReport({ origin, trigger: 'test' })
expect(outcome.reason).toBe('rejected')
expect(outcome.run).toMatchObject({ sent: false, detail: 'channel_not_found' })
})
})
describe('Doppelter Versand', () => {
it('schickt nach einem Versand nichts Zweites, solange nichts Neues erscheint', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
await makePost(project.id, { number: 1, slug: 'eins', status: 'published', publishAt: new Date() })
await writeSetting('slack.botToken', 'xoxb-1234')
await writeSetting('slack.channel', 'C0123456')
vi.stubGlobal('fetch', vi.fn(async () => new Response(
JSON.stringify({ ok: true, channel: 'C0123456', ts: '1785600000.000100' }),
{ headers: { 'content-type': 'application/json' } },
)))
const first = await runWeeklyReport({ origin, trigger: 'test' })
const second = await runWeeklyReport({ origin, trigger: 'test' })
expect(first.reason).toBe('sent')
expect(second.reason).toBe('nothing_published')
})
})
describe('Löschen nimmt den Versand zurück', () => {
it('macht den Bericht nach dem Löschen wieder sendbar', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
await makePost(project.id, { number: 1, slug: 'eins', status: 'published', publishAt: new Date() })
await writeSetting('slack.botToken', 'xoxb-1234')
await writeSetting('slack.channel', 'C0123456')
vi.stubGlobal('fetch', vi.fn(async () => new Response(
JSON.stringify({ ok: true, channel: 'C0123456', ts: '1785600000.000100' }),
{ headers: { 'content-type': 'application/json' } },
)))
const first = await runWeeklyReport({ origin, trigger: 'test' })
expect(first.reason).toBe('sent')
expect((await runWeeklyReport({ origin, trigger: 'test' })).reason).toBe('nothing_published')
const form = new FormData()
form.set('runId', first.run.id)
const deleted = await performDeleteMessage(
{ id: 'u1', name: 'Admin', email: '[email protected]', isAdmin: true, assignments: [] },
form,
)
expect(deleted.message).toBe('messageDeleted')
expect(deleted.status).toBe('ok')
expect((await runWeeklyReport({ origin, trigger: 'test' })).reason).toBe('sent')
})
})