9f0938daea
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
145 lines
6.4 KiB
TypeScript
145 lines
6.4 KiB
TypeScript
import { render, screen, waitFor } from "@testing-library/react"
|
|
import userEvent from "@testing-library/user-event"
|
|
import { MemoryRouter, Routes, Route } from "react-router-dom"
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
|
import { DomainDiffPage } from "./DomainDiffPage"
|
|
import { AuthProvider } from "@/auth/AuthContext"
|
|
import { api } from "@/api/client"
|
|
import { vi, beforeEach, test, expect } from "vitest"
|
|
import type { Domain } from "@/api/types"
|
|
|
|
const PROJECT_ID = "p1"
|
|
|
|
const domainWithTemplate: Domain = {
|
|
id: "d1",
|
|
providerAccountId: "acc1",
|
|
zoneName: "example.com.",
|
|
zoneId: "z1",
|
|
templateId: "t1",
|
|
lastCheckStatus: "drift",
|
|
}
|
|
|
|
function renderPage(qc: QueryClient = new QueryClient()) {
|
|
return render(
|
|
<QueryClientProvider client={qc}>
|
|
<AuthProvider>
|
|
<MemoryRouter initialEntries={["/domains/d1"]}>
|
|
<Routes><Route path="/domains/:id" element={<DomainDiffPage />} /></Routes>
|
|
</MemoryRouter>
|
|
</AuthProvider>
|
|
</QueryClientProvider>,
|
|
)
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks()
|
|
vi.spyOn(api.auth, "me").mockResolvedValue({
|
|
user: { id: "u1", email: "a@b.com" },
|
|
project: { id: PROJECT_ID, name: "Default" },
|
|
})
|
|
vi.spyOn(api, "domainHistory").mockResolvedValue([])
|
|
vi.spyOn(api, "listDomains").mockResolvedValue([domainWithTemplate])
|
|
})
|
|
|
|
test("apply sends applyPrunes=false by default, true only after opting in", async () => {
|
|
vi.spyOn(api, "checkDomain").mockResolvedValue({
|
|
updates: [{ kind: "update", type: "A", name: "a.", desired: ["1"], actual: ["2"], readOnly: false }],
|
|
prunes: [{ kind: "delete", type: "A", name: "b.", actual: ["3"], readOnly: false }],
|
|
readOnly: [], inSyncCount: 0,
|
|
})
|
|
const applySpy = vi.spyOn(api, "applyDomain").mockResolvedValue({ updates: [], prunes: [], readOnly: [], inSyncCount: 0 })
|
|
const zoneRecordsSpy = vi.spyOn(api, "zoneRecords")
|
|
const user = userEvent.setup()
|
|
renderPage()
|
|
|
|
const applyBtn = await screen.findByRole("button", { name: /apply/i })
|
|
await user.click(applyBtn)
|
|
await waitFor(() => expect(applySpy).toHaveBeenCalled())
|
|
expect(applySpy.mock.calls[0]).toEqual([PROJECT_ID, "d1", { applyUpdates: true, applyPrunes: false }])
|
|
|
|
// включить prune и применить снова
|
|
const pruneToggle = screen.getByRole("checkbox", { name: /prune|удал/i })
|
|
await user.click(pruneToggle)
|
|
await user.click(screen.getByRole("button", { name: /apply/i }))
|
|
await waitFor(() => expect(applySpy).toHaveBeenCalledTimes(2))
|
|
expect(applySpy.mock.calls[1]).toEqual([PROJECT_ID, "d1", { applyUpdates: true, applyPrunes: true }])
|
|
|
|
// домен с шаблоном: записи зоны не нужны для диффа — запрос не должен уходить к провайдеру
|
|
expect(zoneRecordsSpy).not.toHaveBeenCalled()
|
|
})
|
|
|
|
test("пока список доменов грузится — показан общий лоадер, а не баннер об отсутствии шаблона", async () => {
|
|
let resolveListDomains: (domains: Domain[]) => void
|
|
vi.spyOn(api, "listDomains").mockReturnValue(
|
|
new Promise((resolve) => {
|
|
resolveListDomains = resolve
|
|
}),
|
|
)
|
|
const checkSpy = vi.spyOn(api, "checkDomain").mockResolvedValue({
|
|
updates: [], prunes: [], readOnly: [], inSyncCount: 0,
|
|
})
|
|
const zoneRecordsSpy = vi.spyOn(api, "zoneRecords")
|
|
renderPage()
|
|
|
|
expect(await screen.findByText(/загрузка/i)).toBeInTheDocument()
|
|
expect(screen.queryByText(/шаблон не привязан/i)).not.toBeInTheDocument()
|
|
expect(checkSpy).not.toHaveBeenCalled()
|
|
expect(zoneRecordsSpy).not.toHaveBeenCalled()
|
|
|
|
resolveListDomains!([domainWithTemplate])
|
|
|
|
expect(await screen.findByRole("button", { name: /apply/i })).toBeInTheDocument()
|
|
})
|
|
|
|
test("домен без шаблона показывает записи зоны и не вызывает check", async () => {
|
|
vi.spyOn(api, "listDomains").mockResolvedValue([
|
|
{ id: "d1", providerAccountId: "acc1", zoneName: "example.com.", zoneId: "z1", templateId: null },
|
|
])
|
|
const checkSpy = vi.spyOn(api, "checkDomain")
|
|
vi.spyOn(api, "zoneRecords").mockResolvedValue([
|
|
{ type: "A", name: "example.com.", ttl: 3600, values: ["1.2.3.4"] },
|
|
])
|
|
renderPage()
|
|
|
|
expect(await screen.findByText(/шаблон не привязан/i)).toBeInTheDocument()
|
|
expect(screen.getByRole("button", { name: /создать шаблон из этой зоны/i })).toBeInTheDocument()
|
|
expect(await screen.findByText("example.com.")).toBeInTheDocument()
|
|
expect(screen.getByText("1.2.3.4")).toBeInTheDocument()
|
|
|
|
expect(screen.queryByText(/вычисляю дифф/i)).not.toBeInTheDocument()
|
|
expect(checkSpy).not.toHaveBeenCalled()
|
|
})
|
|
|
|
test("ошибка загрузки списка доменов показывает баннер ошибки и не уходит в ветку без шаблона", async () => {
|
|
vi.spyOn(api, "listDomains").mockRejectedValue(new Error("network down"))
|
|
const checkSpy = vi.spyOn(api, "checkDomain")
|
|
const zoneRecordsSpy = vi.spyOn(api, "zoneRecords")
|
|
// retry:false — иначе react-query переретраит listDomains с экспоненциальной
|
|
// задержкой и findByText не успевает дождаться финального isError.
|
|
renderPage(new QueryClient({ defaultOptions: { queries: { retry: false } } }))
|
|
|
|
expect(await screen.findByText(/не удалось загрузить список доменов/i)).toBeInTheDocument()
|
|
expect(screen.getByText("network down")).toBeInTheDocument()
|
|
|
|
expect(screen.queryByText(/шаблон не привязан/i)).not.toBeInTheDocument()
|
|
expect(checkSpy).not.toHaveBeenCalled()
|
|
expect(zoneRecordsSpy).not.toHaveBeenCalled()
|
|
})
|
|
|
|
test("создание шаблона из зоны вызывает templateFromZone", async () => {
|
|
vi.spyOn(api, "listDomains").mockResolvedValue([
|
|
{ id: "d1", providerAccountId: "acc1", zoneName: "example.com.", zoneId: "z1", templateId: null },
|
|
])
|
|
vi.spyOn(api, "zoneRecords").mockResolvedValue([])
|
|
const templateFromZoneSpy = vi.spyOn(api, "templateFromZone").mockResolvedValue({
|
|
id: "t9", name: "example.com. snapshot", records: [], version: 1,
|
|
})
|
|
const user = userEvent.setup()
|
|
renderPage()
|
|
|
|
const createBtn = await screen.findByRole("button", { name: /создать шаблон из этой зоны/i })
|
|
await user.click(createBtn)
|
|
|
|
await waitFor(() => expect(templateFromZoneSpy).toHaveBeenCalledWith(PROJECT_ID, "d1"))
|
|
})
|