Files
MikrotikManager/tests/e2e/users-catalog.spec.ts
T

50 lines
9.2 KiB
TypeScript

import {test,expect,type Page} from "@playwright/test"
import {user,server,catalogResponse,catalogState} from "./fixtures/users"
const iface={name:"ether1",type:"ether",running:true,disabled:false,boundUserId:null,boundUserLogin:null}
const peer={publicKey:"client-cn",name:"Клиент VPN",comment:"",allowedIps:[],boundUserId:null,boundUserLogin:null}
test.beforeEach(async({page})=>{
await page.route("**/health",r=>catalogResponse(r, {json:{ok:true}}));await page.route("**/api/**",r=>{const p=new URL(r.request().url()).pathname;return catalogResponse(r, {json:p==="/api/auth/config"?{required:false}:p==="/api/users"?{users:[user]}:p==="/api/servers"?[server]:{interfaces:[]}})})
})
async function open(page:Page){await page.goto("/users");await page.getByRole("button",{name:"Добавить пользователя",exact:true}).click();await page.getByPlaceholder("Иван Иванов").fill("Каталог тест");await page.getByPlaceholder("[email protected]").fill("[email protected]");for(let i=0;i<3;i++)await page.getByRole("dialog").getByRole("button",{name:"Далее",exact:true}).click()}
for(const kind of ["API","malformed","duplicate","missing-peers","duplicate-peers"])test("Каталог пользователя: отказ и повтор "+kind,async({page})=>{
let fail=true;await page.route("**/api/users/interface-catalog-state?*",r=>catalogResponse(r, fail?(kind==="API"?{status:503,json:{error:"КАТАЛОГ НЕДОСТУПЕН"}}:{json:{interfaces:kind==="malformed"?[{}]:kind==="duplicate"?[iface,iface]:kind==="missing-peers"?[{...iface,type:"wg"}]:[{...iface,type:"wg",peers:[peer,peer]}]}}):{json:{interfaces:[iface]}}))
await open(page);const dialog=page.getByRole("dialog");await expect(dialog.getByRole("alert",{name:"Каталог интерфейсов"})).toBeVisible();await expect(dialog.getByText("Нет интерфейсов в каталоге",{exact:true})).toHaveCount(0);await expect(dialog.getByRole("button",{name:"Привязать",exact:true})).toBeDisabled()
fail=false;await dialog.getByRole("button",{name:"Повторить каталог интерфейсов"}).click();await dialog.getByRole("button",{name:/ether1.*Ethernet/}).click();await expect(dialog.getByRole("button",{name:"Привязать",exact:true})).toBeEnabled()
})
test("Каталог пользователя: подтверждённое empty отличается от ожидания",async({page})=>{
let release!:()=>void;const gate=new Promise<void>(r=>release=r);await page.route("**/api/users/interface-catalog-state?*",async r=>{await gate;await catalogResponse(r, {json:{interfaces:[]}})})
await open(page);await expect(page.getByRole("status").filter({hasText:"Чтение каталога интерфейсов"})).toBeVisible();await expect(page.getByText("Нет интерфейсов в каталоге",{exact:true})).toHaveCount(0);release();await expect(page.getByText("Нет интерфейсов в каталоге",{exact:true})).toBeVisible()
})
for(const fails of [false,true])test("Каталог пользователя: поздний ответ другого устройства "+fails,async({page})=>{
let started=false,completed=false,release!:()=>void;const gate=new Promise<void>(r=>release=r);await page.route("**/api/servers",r=>catalogResponse(r, {json:[server,{...server,id:2,name:"Второй",host:"192.0.2.2"}]}))
await page.route("**/api/users/interface-catalog-state?*",async r=>{if(new URL(r.request().url()).searchParams.get("serverId")==="2")return catalogResponse(r, {json:{interfaces:[{...iface,name:"ether2"}]}});started=true;await gate;try{await catalogResponse(r, fails?{status:503,json:{error:"СТАРЫЙ КАТАЛОГ"}}:{json:{interfaces:[iface]}})}catch{}finally{completed=true}})
await open(page);await expect.poll(()=>started).toBe(true);await page.getByRole("combobox",{name:"Сервер каталога интерфейсов"}).selectOption("2");await expect(page.getByRole("button",{name:/ether2.*Ethernet/})).toBeEnabled();release();await expect.poll(()=>completed).toBe(true);await expect(page.getByRole("button",{name:/ether1.*Ethernet/})).toHaveCount(0);await expect(page.getByText("СТАРЫЙ КАТАЛОГ",{exact:true})).toHaveCount(0)
})
test("Каталог пользователя: ошибка пиров блокирует выбор, повтор восстанавливает",async({page})=>{
let fail=true;await page.route("**/api/users/interface-catalog-state?*",r=>catalogResponse(r, {json:{interfaces:[{...iface,name:"wg1",type:"wg",peers:[peer],...(fail?{peersError:"WG ОТКАЗ"}:{})}]}}))
await open(page);await page.getByRole("button",{name:/wg1.*WireGuard/}).click();await expect(page.getByRole("alert").filter({hasText:"WG ОТКАЗ"})).toBeVisible();await expect(page.getByRole("button",{name:"Клиент VPN",exact:true})).toBeDisabled();await expect(page.getByRole("button",{name:"Привязать",exact:true})).toBeDisabled()
fail=false;await page.getByRole("button",{name:"Повторить каталог интерфейсов"}).click();await page.getByRole("button",{name:"Клиент VPN",exact:true}).click();await expect(page.getByRole("button",{name:"Привязать",exact:true})).toBeEnabled()
})
test("Каталог пользователя: IPsec привязывает клиента по CN",async({page})=>{
let data:Record<string,unknown>|null=null
await page.route("**/api/users/interface-catalog-state?*",r=>catalogResponse(r, {json:{interfaces:[{...iface,name:"ipsec-vpn",type:"ipsec",peers:[peer]}]}}))
await page.route("**/api/users",r=>catalogResponse(r, {json:r.request().method()==="POST"?{user:{...user,...r.request().postDataJSON(),id:"created"}}:{users:[user]}}))
await page.route("**/api/users/created/bindings",r=>{data=r.request().postDataJSON();return catalogResponse(r, {status:201,json:{binding:{...data,id:"binding",userId:"created",serverName:"Роутер",serverSite:"",serverCountry:"RU",comment:"",createdAt:"",updatedAt:""}}})})
await open(page);await page.getByRole("button",{name:/Клиенты IPsec.*IPsec/}).click();await page.getByRole("button",{name:"Клиент VPN",exact:true}).click();await page.getByRole("button",{name:"Привязать",exact:true}).click();await page.getByRole("dialog").getByRole("button",{name:"Создать",exact:true}).click();await expect(page.getByRole("dialog")).toHaveCount(0);expect(data).toMatchObject({serverId:1,interfaceName:"ipsec-vpn",interfaceType:"ipsec",peerPublicKey:"client-cn"})
})
for(const kind of ["missing","foreign","timestamp","inconsistent"])test("Каталог state: отклоняет неверные метаданные "+kind,async({page})=>{
const payload=kind==="missing"?{interfaces:[iface]}:catalogState([iface],kind==="foreign"?2:1,kind==="timestamp"?{sampledAt:"bad-time"}:kind==="inconsistent"?{source:"traffic-history",inventoryComplete:true}:{})
await page.route("**/api/users/interface-catalog-state?*",route=>route.fulfill({json:payload}));await open(page);await expect(page.getByRole("alert",{name:"Каталог интерфейсов"})).toBeVisible();await expect(page.getByRole("button",{name:/ether1.*Ethernet/})).toHaveCount(0);await expect(page.getByRole("button",{name:"Привязать",exact:true})).toBeDisabled()
})
test("Каталог state: история и неизвестные флаги не называются текущим состоянием",async({page})=>{
await page.route("**/api/users/interface-catalog-state?*",route=>route.fulfill({json:catalogState([{...iface,running:null,disabled:null}],1,{source:"traffic-history",inventoryComplete:false})}));await open(page)
await expect(page.getByRole("status").filter({hasText:"Частичный каталог из истории трафика"})).toBeVisible();await expect(page.getByRole("status").filter({hasText:"Состояния относятся к этому срезу"})).toBeVisible();await expect(page.getByTitle("Running в срезе неизвестен",{exact:true})).toBeVisible()
})
test("Каталог state: unknown и IPsec failure видны без раскрытия группы",async({page})=>{
await page.route("**/api/users/interface-catalog-state?*",route=>route.fulfill({json:catalogState([],1,{source:"unknown",sampledAt:null,inventoryComplete:false,failures:[{resource:"interfaces",error:"Нет снимка"},{resource:"ipsec",error:"IPsec отказ"}]})}));await open(page);await expect(page.getByRole("alert",{name:"Частичные сбои каталога"})).toContainText("IPsec отказ");await expect(page.getByText("Список интерфейсов неизвестен",{exact:true})).toBeVisible();await expect(page.getByText("Нет интерфейсов в каталоге",{exact:true})).toHaveCount(0)
})
test("Каталог state: пустой срез истории не означает отсутствие интерфейсов",async({page})=>{
await page.route("**/api/users/interface-catalog-state?*",route=>route.fulfill({json:catalogState([],1,{source:"traffic-history",inventoryComplete:false})}));await open(page);await expect(page.getByText("В этом срезе нет интерфейсов",{exact:true})).toBeVisible();await expect(page.getByText("Нет интерфейсов в каталоге",{exact:true})).toHaveCount(0)
})