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 { 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', 'C123') vi.stubGlobal('fetch', vi.fn(async () => new Response( JSON.stringify({ ok: true, channel: 'C123', 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') }) })