import { beforeEach, describe, expect, it, vi } from 'vitest'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { EscalationsPage } from '../pages/EscalationsPage'
import { ChannelsPage } from '../pages/ChannelsPage'
import { session } from '../api/session'
import { fetchRouter, res, type Call } from './support'
/**
* Канал, на який посилається сходинка драбини.
*
* ЧОГО ЦЕЙ ФАЙЛ БОЇТЬСЯ. Драбина зберігалась із будь-яким
* ідентифікатором каналу, а видалення каналу не чіпало сходинок, що на
* нього посилались. Ззовні це виглядало як робоче нічне чергування:
* сходинка стоїть, хвилини стоять, у стовпчику каналів — прочерк, який
* читається як «не заповнено» й нікого не турбує. Насправді ця
* сходинка не йде нікуди й списується мовчки.
*
* У формі це давало ще й глухий кут: галочки для видаленого каналу в
* переліку немає, тобто прибрати його руками неможливо, а сервер
* (після виправлення) такої драбини вже не приймає.
*
* Форма підставних відповідей звірена з
* `server/internal/httpapi/alerts.go` (`handleListEscalationPolicies`,
* `handleListChannels`) і зі `store.EscalationPolicy` / `store.Channel`.
*/
const duty = {
id: 'ch-duty',
kind: 'telegram',
name: 'Черговий',
config: {},
min_severity: 'warning',
enabled: true,
has_secret: true,
}
/** Драбина, друга сходинка якої посилається на вже видалений канал. */
const policy = {
id: 'p-1',
name: 'Нічне чергування',
description: '',
steps: [
{ after_min: 15, channel_ids: ['ch-duty'] },
{ after_min: 45, channel_ids: ['ch-gone'] },
],
repeat_after_min: 0,
max_repeats: 0,
rule_count: 2,
}
beforeEach(() => {
vi.stubGlobal('WebSocket', class {})
session.set('tok', {
userID: 'u-me',
username: 'me',
tenantID: 't-1',
permissions: ['alerts:read', 'alerts:write'],
})
})
describe('сходинка, що посилається на видалений канал', () => {
it('у переліку її видно словами, а не прочерком', async () => {
fetchRouter({
'GET /api/v1/escalation-policies': { policies: [policy] },
'GET /api/v1/channels': { channels: [duty] },
})
render()
await screen.findByText('Черговий')
// Прочерк читався б як «нічого не налаштовано», тобто як спокійний
// стан. Тут стан протилежний.
expect(await screen.findByText('канал видалено')).toBeTruthy()
})
it('форма прибирає привида, говорить про це і не шле його на сервер', async () => {
const srv = fetchRouter({
'GET /api/v1/escalation-policies': { policies: [policy] },
'GET /api/v1/channels': { channels: [duty] },
'PUT /api/v1/escalation-policies/p-1': () => res(200, { id: 'p-1' }),
})
render()
fireEvent.click(await screen.findByRole('button', { name: 'Змінити' }))
await screen.findByText('Драбина: Нічне чергування')
expect(screen.getByText(/лишались посилання на видалені канали/)).toBeTruthy()
// Друга сходинка лишилась без каналу — і форма має сказати про це
// ДО збереження, а не віддати серверу відмову без пояснення.
expect(screen.getByText(/сходинка 2 не має жодного каналу/)).toBeTruthy()
// Дали сходинці канал — тепер зберігається.
//
// Шукаємо саме галочку каналу, а не «останню на формі»: у формі є й
// галочки, що каналів не стосуються (тиха година), і «остання»
// мовчки перестала б робити те, заради чого цей рядок написано —
// тест ловив би відсутність PUT замість зниклого каналу.
fireEvent.click(screen.getAllByRole('checkbox', { name: /Черговий/ })[1])
fireEvent.click(screen.getByRole('button', { name: 'Зберегти' }))
let put: Call | undefined
await waitFor(() => {
put = srv.calls.find((c) => c.method === 'PUT')
expect(put).toBeTruthy()
})
const body = put!.body as { steps: { channel_ids: string[] }[] }
expect(body.steps.map((s) => s.channel_ids)).toEqual([['ch-duty'], ['ch-duty']])
})
})
describe('видалення каналу, на який спирається драбина', () => {
it('називає драбини, які через це замовкнуть', async () => {
fetchRouter({
'GET /api/v1/channels': {
channels: [{ ...duty, escalations: ['Нічне чергування', 'Вихідні'] }],
},
})
render()
fireEvent.click(await screen.findByRole('button', { name: 'Видалити' }))
// Те саме, що вже показує видалення драбини: скільки чужих
// налаштувань зараз перестане працювати, і яких саме.
expect(await screen.findByText(/Нічне чергування, Вихідні/)).toBeTruthy()
})
it('без драбин попередження про них не вигадується', async () => {
fetchRouter({ 'GET /api/v1/channels': { channels: [duty] } })
render()
fireEvent.click(await screen.findByRole('button', { name: 'Видалити' }))
await screen.findByText(/більше не доставлятимуться/)
expect(screen.queryByText(/драбини ескалації/)).toBeNull()
})
})