import { test, expect, type Page } from "@playwright/test" const devices=[{id:1,name:"Первый",host:"192.0.2.1",site:"Тест",country:"RU",asn:"AS64500",type:"exit-node",enabled:true,status:"online",latency:null}] async function routes(page:Page){ await page.route("**/health",r=>r.fulfill({json:{ok:true}})) await page.route("**/api/**",r=>{const p=new URL(r.request().url()).pathname;return r.fulfill({json:p==="/api/servers"?devices:p==="/api/auth/config"?{required:false}:p.endsWith("/ros-src-address")?{host:"192.0.2.1",ipv4:"192.0.2.1"}:{tunnels:[]}})}) } test.beforeEach(async({page})=>routes(page)) const run=(page:Page)=>page.getByRole("button",{name:"Запустить",exact:true}) for(const [name,data,status] of [["malformed",{},200],["legacy",{output:" error: RouterOS недоступен"},200],["API",{error:"Нет доступа к диагностике"},403]] as const)test("Диагностика: "+name+" сохраняет цель и допускает повтор",async({page})=>{ let fail=true;await page.route("**/api/servers/1/probes/run",r=>r.fulfill(fail?{json:data,status}:{json:{output:"PING подтверждён"}})) await page.goto("/probes?server=1");await expect(run(page)).toBeEnabled();const target=page.getByRole("textbox",{name:"Цель диагностики"});await target.fill("198.51.100.9");await run(page).click() await expect(page.getByText("ошибка",{exact:true})).toBeVisible();await expect(target).toHaveValue("198.51.100.9");await expect(run(page)).toBeEnabled() fail=false;await run(page).click();await expect(page.getByText("PING подтверждён",{exact:true})).toBeVisible() }) test("Диагностика: пустой output явно подтверждён",async({page})=>{ await page.route("**/api/servers/1/probes/run",r=>r.fulfill({json:{output:""}}));await page.goto("/probes?server=1");await run(page).click();await expect(page.getByText("Запрос выполнен: API вернул пустой вывод.",{exact:true})).toBeVisible() }) test("Диагностика: повторный Enter, Стоп и поздний успех",async({page})=>{ let calls=0,release!:()=>void;const gate=new Promise(r=>release=r) await page.route("**/api/servers/1/probes/run",async r=>{calls++;await gate;await r.fulfill({json:{output:"ЗАПОЗДАЛЫЙ УСПЕХ"}}).catch(()=>{})}) await page.goto("/probes?server=1");const target=page.getByRole("textbox",{name:"Цель диагностики"});await target.press("Enter");await target.press("Enter");await expect.poll(()=>calls).toBe(1);await expect(run(page)).toBeDisabled() await page.getByRole("button",{name:"Стоп",exact:true}).click();await expect(page.getByText("ожидание отменено",{exact:true})).toBeVisible();await expect(run(page)).toBeEnabled();release();await expect(page.getByText("ЗАПОЗДАЛЫЙ УСПЕХ",{exact:true})).toHaveCount(0) await expect(page.getByText("Ожидание отменено. Остановка на устройстве не подтверждена.",{exact:true})).toBeVisible() }) test("Диагностика: таймаут сохраняет ввод, без автоматического повтора",async({page})=>{ await page.clock.install();let calls=0,release!:()=>void;const gate=new Promise(r=>release=r) await page.route("**/api/servers/1/probes/run",async r=>{calls++;await gate;await r.fulfill({json:{output:"ПОЗДНО"}}).catch(()=>{})}) await page.goto("/probes?server=1");await run(page).click();await expect.poll(()=>calls).toBe(1);await page.clock.fastForward(60_100) await expect(page.getByText(/Время ожидания истекло/)).toBeVisible();await expect(run(page)).toBeEnabled();await expect(page.getByRole("textbox",{name:"Цель диагностики"})).toHaveValue("8.8.8.8");expect(calls).toBe(1);release() }) test("Диагностика: уход со страницы не переносит поздний вывод",async({page})=>{ let first=true,release!:()=>void;const gate=new Promise(r=>release=r) await page.route("**/api/servers/1/probes/run",async r=>{if(first){first=false;await gate;await r.fulfill({json:{output:"СТАРЫЙ ВЫВОД"}}).catch(()=>{})}else await r.fulfill({json:{output:"НОВЫЙ ВЫВОД"}})}) await page.goto("/probes?server=1");await run(page).click();await expect(run(page)).toBeDisabled();await page.goto("/terminal");release();await page.goto("/probes?server=1");await run(page).click();await expect(page.getByText("НОВЫЙ ВЫВОД",{exact:true})).toBeVisible();await expect(page.getByText("СТАРЫЙ ВЫВОД",{exact:true})).toHaveCount(0) }) for(const malformed of [false,true])test("Диагностика: первоначальный отказ каталога и повтор "+malformed,async({page})=>{ let fail=true;await page.route("**/api/servers",r=>r.fulfill(fail?(malformed?{json:[{...devices[0],enabled:"yes"}]}:{status:403,json:{error:"Каталог запрещён"}}):{json:devices})) await page.goto("/probes?server=1");await expect(page.getByRole("alert",{name:"Каталог диагностики"})).toContainText("Устройства пока неизвестны");await expect(run(page)).toBeDisabled() fail=false;await page.getByRole("button",{name:"Обновить устройства диагностики"}).click();await expect(run(page)).toBeEnabled() }) test("Диагностика: фоновая ошибка сохраняет список и цель до повтора",async({page})=>{ let fail=false;await page.route("**/api/servers",r=>r.fulfill(fail?{status:503,json:{error:"Каталог недоступен"}}:{json:devices})) await page.goto("/probes?server=1");await expect(run(page)).toBeEnabled();await page.getByRole("textbox",{name:"Цель диагностики"}).fill("198.51.100.5");fail=true;await page.getByRole("button",{name:"Обновить устройства диагностики"}).click() await expect(page.getByRole("alert",{name:"Каталог диагностики"})).toContainText("Последний список сохранён");await expect(page.getByRole("option",{name:/Первый/})).toBeVisible();await expect(run(page)).toBeDisabled();await expect(page.getByRole("textbox",{name:"Цель диагностики"})).toHaveValue("198.51.100.5") fail=false;await page.getByRole("button",{name:"Обновить устройства диагностики"}).click();await expect(run(page)).toBeEnabled();await expect(page.getByRole("textbox",{name:"Цель диагностики"})).toHaveValue("198.51.100.5") }) test("Диагностика: пустой каталог отличается от отказа",async({page})=>{ await page.route("**/api/servers",r=>r.fulfill({json:[]}));await page.goto("/probes");await expect(page.getByRole("status").filter({hasText:"В каталоге нет устройств"})).toBeVisible();await expect(page.getByRole("alert",{name:"Каталог диагностики"})).toHaveCount(0);await expect(run(page)).toBeDisabled() }) test("Диагностика: неизвестный статус не становится offline",async({page})=>{ await page.route("**/api/servers",r=>r.fulfill({json:[{...devices[0],status:null}]}));await page.goto("/probes?server=1");await expect(run(page)).toBeEnabled();await expect(page.getByRole("option",{name:/Первый/})).not.toHaveAccessibleName(/Не в сети|offline/i) }) for(const permission of ["mm:servers:read","mm:servers:write"])test("Диагностика: права запуска "+permission,async({page})=>{ await page.addInitScript(p=>localStorage.setItem("mmapp_token","header."+btoa(JSON.stringify({sub:"test",apps:["mm"],permissions:[p],exp:Math.floor(Date.now()/1000)+3600}))+".signature"),permission) await page.route("**/api/auth/config",r=>r.fulfill({json:{required:true}}));let posts=0,greReads=0 await page.route("**/api/filters/gre-tunnels?*",r=>{greReads++;return r.fulfill({json:{tunnels:[]}})}) await page.route("**/api/servers/1/probes/run",r=>{posts++;return r.fulfill({json:{output:"РАЗРЕШЕНО"}})}) await page.goto("/probes?server=1") if(permission==="mm:servers:read"){await expect(page.getByRole("status").filter({hasText:"Для запуска диагностики требуется mm:servers:write"})).toBeVisible();await expect(run(page)).toBeDisabled();await page.getByRole("textbox",{name:"Цель диагностики"}).press("Enter");expect(posts).toBe(0)} else{await expect(run(page)).toBeEnabled();await run(page).click();await expect(page.getByText("РАЗРЕШЕНО",{exact:true})).toBeVisible();expect(posts).toBe(1)} expect(greReads).toBe(0);await page.getByRole("button",{name:/BW-тест/}).click();await expect(page.getByRole("status").filter({hasText:"Для выбора GRE требуется mm:filters:read"})).toBeVisible();await expect(run(page)).toBeDisabled() }) const tunnel={id:"*1",serverId:"1",name:"GRE-Тест",localAddress:"192.0.2.1",remoteAddress:"192.0.2.2",localInnerIp:"10.0.0.1",remoteInnerIp:"10.0.0.2",poolId:"",ipsec:null,mtu:1476,keepaliveInterval:10,keepaliveRetries:3,dscp:"inherit",clampTcpMss:true,allowFastPath:false,comment:"",enabled:true,status:"up"} for(const kind of ["API","malformed","foreign"])test("Диагностика: GRE "+kind+" отличается от пустого списка",async({page})=>{ let fail=true;await page.route("**/api/filters/gre-tunnels?*",r=>r.fulfill(fail?(kind==="API"?{status:503,json:{error:"GRE отказ"}}:{json:{tunnels:kind==="foreign"?[{...tunnel,serverId:"2"}]:[{}]}}):{json:{tunnels:[tunnel]}})) await page.route("**/api/servers/1/probes/run",r=>r.fulfill({json:{output:"BW подтверждён"}})) await page.goto("/probes?server=1");await page.getByRole("button",{name:/BW-тест/}).click();await expect(page.getByRole("alert",{name:"GRE диагностики"})).toBeVisible();await expect(run(page)).toBeDisabled() fail=false;await page.getByRole("button",{name:"Повторить GRE"}).click();await expect(run(page)).toBeEnabled();await run(page).click();await expect(page.getByText("BW подтверждён",{exact:true})).toBeVisible() }) test("Диагностика: подтверждённый пустой GRE каталог",async({page})=>{ await page.goto("/probes?server=1");await page.getByRole("button",{name:/BW-тест/}).click();await expect(page.getByRole("status").filter({hasText:"У выбранного устройства нет GRE-туннелей"})).toBeVisible();await expect(page.getByRole("alert",{name:"GRE диагностики"})).toHaveCount(0);await expect(run(page)).toBeDisabled() }) for(const fails of [false,true])test("Диагностика: поздний GRE прежнего устройства "+fails,async({page})=>{ let started=false,completed=false,release!:()=>void;const gate=new Promise(r=>release=r) await page.route("**/api/servers",r=>r.fulfill({json:[...devices,{...devices[0],id:2,name:"Второй",host:"192.0.2.2"}]})) await page.route("**/api/filters/gre-tunnels?*",async r=>{if(new URL(r.request().url()).searchParams.get("serverId")==="2")return r.fulfill({json:{tunnels:[]}});started=true;await gate;try{await r.fulfill(fails?{status:503,json:{error:"СТАРЫЙ GRE ОТКАЗ"}}:{json:{tunnels:[tunnel]}})}catch{}finally{completed=true}}) await page.goto("/probes?server=1");await expect.poll(()=>started).toBe(true);await page.getByRole("option",{name:/Второй/}).click();await page.getByRole("button",{name:/BW-тест/}).click();await expect(page.getByRole("status").filter({hasText:"У выбранного устройства нет GRE-туннелей"})).toBeVisible();release();await expect.poll(()=>completed).toBe(true) await expect(page.getByRole("alert",{name:"GRE диагностики"})).toHaveCount(0);await expect(page.getByText("GRE-Тест",{exact:true})).toHaveCount(0);await expect(run(page)).toBeDisabled() }) for(const invalid of ["disabled","no-address"])test("Диагностика: BW недоступен для GRE "+invalid,async({page})=>{ let posts=0;await page.route("**/api/filters/gre-tunnels?*",r=>r.fulfill({json:{tunnels:[{...tunnel,enabled:invalid!=="disabled",remoteAddress:invalid==="no-address"?"":tunnel.remoteAddress}]}}));await page.route("**/api/servers/1/probes/run",r=>{posts++;return r.fulfill({json:{output:"НЕ ДОЛЖНО ВЫПОЛНЯТЬСЯ"}})}) await page.goto("/probes?server=1");await page.getByRole("button",{name:/BW-тест/}).click();await expect(page.getByRole("status").filter({hasText:"Выберите включённый GRE с адресом назначения"})).toBeVisible();await expect(run(page)).toBeDisabled();expect(posts).toBe(0) }) test("Диагностика: DNS приложения, inline ошибка и повтор",async({page})=>{ let fail=true;await page.route("**/api/servers/1/probes/run",r=>r.fulfill({json:{output:fail?";; QUESTION: example.test A\n;; ERROR: ENOTFOUND":"example.test. IN A 192.0.2.1"}})) await page.goto("/probes?server=1");await page.getByRole("button",{name:"DNS",exact:true}).click();await page.getByRole("textbox",{name:"Домен"}).fill("example.test");await expect(page.getByText(/DNS приложения: example.test/)).toBeVisible();await expect(page.getByText(/server=1.1.1.1/)).toHaveCount(0) await run(page).click();await expect(page.getByText("ошибка",{exact:true})).toBeVisible();await expect(page.getByRole("textbox",{name:"Домен"})).toHaveValue("example.test");fail=false;await run(page).click();await expect(page.getByText("example.test. IN A 192.0.2.1",{exact:true})).toBeVisible() }) test("Диагностика: без выбора не запрашивает устройство",async({page})=>{ let requests=0;await page.route("**/api/servers/*/**",r=>{requests++;return r.fulfill({json:{host:"192.0.2.1",ipv4:"192.0.2.1"}})}) await page.goto("/probes");await expect(page.getByRole("status").filter({hasText:"Выберите устройство для диагностики"})).toBeVisible();await expect(run(page)).toBeDisabled();expect(requests).toBe(0) }) for(const query of ["server=1&server=2","server=bad%20id","server="])test("Диагностика: неверная ссылка не читает API "+query,async({page})=>{ let requests=0;await page.route("**/api/servers**",r=>{requests++;return r.fulfill({json:devices})}) await page.goto("/probes?keep=1&"+query+"#result");await expect(page.getByRole("alert",{name:"Ссылка диагностики"})).toBeVisible();expect(requests).toBe(0) await page.getByRole("button",{name:"Сбросить контекст диагностики"}).click();await expect(page).toHaveURL(/\/probes\?keep=1#result$/) }) test("Диагностика: неизвестный сервер не заменяется первым",async({page})=>{ let deviceRequests=0;await page.route("**/api/servers/*/**",r=>{deviceRequests++;return r.fulfill({json:{ipv4:null}})}) await page.goto("/probes?server=999&keep=1#result");await expect(page.getByRole("alert",{name:"Устройство диагностики"})).toBeVisible();await expect(run(page)).toBeDisabled();expect(deviceRequests).toBe(0) await page.getByRole("button",{name:"Сбросить выбор устройства"}).click();await expect(page).toHaveURL(/\/probes\?keep=1#result$/) }) test("Диагностика: URL выбор, назад и reload сохраняют query/hash",async({page})=>{ await page.route("**/api/servers",r=>r.fulfill({json:[...devices,{...devices[0],id:2,name:"Второй",host:"192.0.2.2"}]})) await page.goto("/probes?server=2&keep=1#result");await expect(page.getByRole("option",{name:/Второй/})).toHaveAttribute("aria-selected","true") await page.getByRole("option",{name:/Первый/}).click();await expect(page).toHaveURL(/keep=1&server=1#result$/);await page.goBack();await expect(page.getByRole("option",{name:/Второй/})).toHaveAttribute("aria-selected","true");await page.reload();await expect(page.getByRole("option",{name:/Второй/})).toHaveAttribute("aria-selected","true") })