66 lines
16 KiB
TypeScript
66 lines
16 KiB
TypeScript
import {test,expect,type Page} from "@playwright/test"
|
|
import {netflowSettings as settings,keyedNetflowSettings as keyed} from "./fixtures/netflow-settings"
|
|
async function setup(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(p==="/api/auth/config"?{json:{required:false}}:p==="/api/traffic/flow/settings"?{json:settings}:{status:503,json:{error:"outside NetFlow fixture"}})})
|
|
}
|
|
const field=(page:Page)=>page.getByRole("textbox",{name:"UDP IPFIX",exact:true})
|
|
const save=(page:Page)=>page.getByRole("button",{name:"Сохранить NetFlow",exact:true})
|
|
const refresh=(page:Page)=>page.getByRole("button",{name:"Обновить статус NetFlow",exact:true})
|
|
async function open(page:Page){await page.goto("/data-collection");await expect(field(page)).toHaveValue("4739")}
|
|
test("NetFlow: неизвестный DTO не заменяется defaults и блокирует операции",async({page})=>{
|
|
await setup(page);await page.route("**/api/traffic/flow/settings",r=>r.fulfill({json:{}}));await page.goto("/data-collection");await expect(page.getByText("NetFlow не получен",{exact:true})).toBeVisible();await expect(field(page)).toHaveValue("");await expect(field(page)).toBeDisabled();await expect(page.getByText("Приём UDP: —",{exact:true})).toBeVisible();await expect(page.getByRole("button",{name:"Ключи хоста",exact:true})).toBeDisabled();await expect(page.getByRole("button",{name:"Сбросить",exact:true})).toBeDisabled()
|
|
})
|
|
test("NetFlow: false/zero известны, Save отправляет только dirty port и показывает listener warning",async({page})=>{
|
|
await setup(page);const writes:object[]=[];await page.route("**/api/traffic/flow/settings",r=>{if(r.request().method()!=="PUT")return r.fallback();const patch=r.request().postDataJSON();writes.push(patch);return r.fulfill({json:{ok:true,settings:{...settings,...patch},listenerError:"listener unavailable"}})})
|
|
await open(page);await expect(page.getByRole("switch",{name:"Принимать IPFIX",exact:true})).not.toBeChecked();await expect(page.getByRole("textbox",{name:"Минимум % окна",exact:true})).toHaveValue("0");await expect(save(page)).toBeDisabled();await field(page).fill("4740");await save(page).click();await expect(page.getByText("Сохранение настроек NetFlow подтверждено",{exact:true})).toBeVisible();await expect(page.getByText(/Настройки сохранены; запуск приёма требует внимания: listener unavailable/)).toBeVisible();expect(writes).toEqual([{flowListenPort:4740}]);await expect(save(page)).toBeDisabled()
|
|
})
|
|
test("NetFlow: invalid port не округляется и не заменяется default",async({page})=>{
|
|
await setup(page);let writes=0;page.on("request",r=>{if(r.method()==="PUT")writes++});await open(page)
|
|
for(const value of ["0","65536","4739x","4.5",""]){await field(page).fill(value);await save(page).click();await expect(page.getByText("UDP IPFIX: укажите целое число от 1 до 65535",{exact:true})).toBeVisible();await expect(field(page)).toHaveValue(value)}expect(writes).toBe(0)
|
|
})
|
|
test("NetFlow: ошибка PUT сохраняет draft без повторов",async({page})=>{
|
|
await setup(page);let writes=0;await page.route("**/api/traffic/flow/settings",r=>{if(r.request().method()!=="PUT")return r.fallback();writes++;return r.fulfill({json:{}})});await open(page);await field(page).fill("4740");await save(page).click();await expect(page.getByText(/Подтверждение сохранения не получено; черновик сохранён/)).toBeVisible();await expect(field(page)).toHaveValue("4740");await expect(save(page)).toBeEnabled();expect(writes).toBe(1)
|
|
})
|
|
test("NetFlow: refresh сохраняет dirty, обновляет untouched и время при ошибке",async({page})=>{
|
|
await setup(page);let failed=false,value=settings;await page.route("**/api/traffic/flow/settings",r=>r.fulfill(failed?{status:503,json:{error:"settings unavailable"}}:{json:value}));await open(page);await field(page).fill("4740");value={...settings,topN:201};await refresh(page).click();await expect(page.getByRole("textbox",{name:"Top-N разговоров",exact:true})).toHaveValue("201");await expect(field(page)).toHaveValue("4740");const stamp=page.getByRole("status").filter({hasText:/^Настройки NetFlow получены:/});const before=await stamp.textContent();failed=true;await refresh(page).click();await expect(page.getByText("NetFlow не обновлён",{exact:true})).toBeVisible();await expect(stamp).toHaveText(before!);await expect(field(page)).toHaveValue("4740");await expect(save(page)).toBeDisabled()
|
|
})
|
|
test("NetFlow: keys receipt сохраняет dirty port",async({page})=>{
|
|
await setup(page);let posts=0;await page.route("**/api/traffic/flow/settings/generate-keys",r=>{posts++;return r.fulfill({json:{ok:true,created:false,publicKey:keyed.hostPublicKey,settings:keyed}})});await open(page);await field(page).fill("4740");await page.getByRole("button",{name:"Ключи хоста",exact:true}).click();await expect(page.getByText("Ключи NetFlow уже созданы",{exact:true})).toBeVisible();await expect(field(page)).toHaveValue("4740");await expect(page.getByRole("textbox",{name:"Public key хоста",exact:true})).toHaveValue(keyed.hostPublicKey);expect(posts).toBe(1)
|
|
})
|
|
test("NetFlow: неизвестные keys требуют явной проверки перед повтором",async({page})=>{
|
|
await setup(page);let posts=0;await page.route("**/api/traffic/flow/settings/generate-keys",r=>{posts++;return r.fulfill({json:{}})});await open(page);const keys=page.getByRole("button",{name:"Ключи хоста",exact:true});await keys.click();await expect(page.getByText(/Результат операции неизвестен; проверьте статус и журнал перед повтором/)).toBeVisible();await expect(keys).toBeDisabled();await expect(page.getByRole("button",{name:"Сбросить",exact:true})).toBeDisabled();expect(posts).toBe(1);await page.getByRole("button",{name:"Разрешить новую операцию NetFlow",exact:true}).click();await expect(keys).toBeEnabled();expect(posts).toBe(1)
|
|
})
|
|
test("NetFlow: экспорт существующей пары использует GET и сохраняет draft",async({page})=>{
|
|
await setup(page);await page.route("**/api/traffic/flow/settings",r=>r.fulfill({json:keyed}));let exports=0;await page.route("**/api/traffic/flow/host-files",r=>{expect(r.request().method()).toBe("GET");exports++;return r.fulfill({json:{files:[{id:"wg",label:"WG",filename:"wg-flow.conf",code:"fixture-only"}]}})});await open(page);await field(page).fill("4740");await page.getByRole("button",{name:"Скачать файлы хоста NetFlow",exact:true}).click();await expect(page.getByRole("dialog",{name:"Файлы для хоста Docker MM",exact:true})).toBeVisible();expect(exports).toBe(1);await page.keyboard.press("Escape");await expect(field(page)).toHaveValue("4740")
|
|
})
|
|
test("NetFlow: purge подтверждает все 6 счётчиков, partial и refresh error отдельно",async({page})=>{
|
|
await setup(page);let posts=0,failed=false;await page.route("**/api/traffic/flow/settings",r=>r.fulfill(failed?{status:503,json:{error:"settings unavailable"}}:{json:settings}));await page.route("**/api/traffic/flow/purge",r=>{posts++;failed=true;return r.fulfill({json:{ok:true,deleted:{buckets:1,minuteStats:2,minuteDims:3,dailyDims:4,hourFacts:5,dailyFacts:6},fileBytesBefore:null,fileBytesAfter:null,vacuumed:false,eventError:"journal unavailable",listenerError:"listener unavailable",partitionError:"partition unavailable"}})})
|
|
await open(page);await field(page).fill("4740");await page.getByRole("button",{name:"Сбросить",exact:true}).click();await page.getByRole("alertdialog").getByRole("button",{name:"Сбросить",exact:true}).click();await expect(page.getByText("Сброс данных NetFlow подтверждён. Удалено строк: 21.",{exact:true})).toBeVisible();await expect(page.getByText(/Запись журнала: journal unavailable/)).toBeVisible();await expect(page.getByText("NetFlow не обновлён",{exact:true})).toBeVisible();await expect(field(page)).toHaveValue("4740");expect(posts).toBe(1)
|
|
})
|
|
test("NetFlow: неизвестный purge закрывает confirmation и блокирует повтор",async({page})=>{
|
|
await setup(page);let posts=0;await page.route("**/api/traffic/flow/purge",r=>{posts++;return r.fulfill({status:503,json:{error:"purge unavailable"}})});await open(page);await page.getByRole("button",{name:"Сбросить",exact:true}).click();await page.getByRole("alertdialog").getByRole("button",{name:"Сбросить",exact:true}).click();await expect(page.getByRole("alertdialog")).toHaveCount(0);await expect(page.getByText(/Результат операции неизвестен; проверьте статус и журнал перед повтором/)).toBeVisible();await expect(page.getByRole("button",{name:"Сбросить",exact:true})).toBeDisabled();expect(posts).toBe(1)
|
|
})
|
|
test("NetFlow: lock удерживает purge dialog и блокирует соседние операции",async({page})=>{
|
|
await setup(page);let posts=0,release!:()=>void;const gate=new Promise<void>(resolve=>release=resolve);await page.route("**/api/traffic/flow/purge",async r=>{posts++;await gate;await r.fulfill({json:{ok:true,deleted:{buckets:0,minuteStats:0,minuteDims:0,dailyDims:0},fileBytesBefore:null,fileBytesAfter:null,vacuumed:false}})})
|
|
try{await open(page);await page.getByRole("button",{name:"Сбросить",exact:true}).click();await page.getByRole("alertdialog").getByRole("button",{name:"Сбросить",exact:true}).click();await expect.poll(()=>posts).toBe(1);await expect(page.getByRole("alertdialog")).toBeVisible();await expect(page.getByRole("alertdialog").getByRole("button",{name:"Отмена",exact:true})).toBeDisabled();release();await expect(page.getByRole("alertdialog")).toHaveCount(0);expect(posts).toBe(1)}finally{release()}
|
|
})
|
|
test("NetFlow: reader claims видят статус без edit/export/mutations",async({page})=>{
|
|
const token="header."+Buffer.from(JSON.stringify({sub:"fixture",apps:["mm"],permissions:["mm:dashboard:read","mm:traffic:read"],exp:Math.floor(Date.now()/1000)+3600})).toString("base64url")+".fixture";await page.addInitScript(value=>localStorage.setItem("mmapp_token",value),token);await setup(page);await page.route("**/api/auth/config",r=>r.fulfill({json:{required:true}}));await page.route("**/api/traffic/flow/settings",r=>r.fulfill({json:keyed}));await open(page);await expect(field(page)).toBeDisabled();await expect(page.getByRole("button",{name:"Скачать файлы хоста NetFlow",exact:true})).toBeDisabled();await expect(page.getByRole("button",{name:"Ключи хоста",exact:true})).toBeDisabled();await expect(refresh(page)).toBeEnabled()
|
|
})
|
|
|
|
test("NetFlow: конфликт purge известен и не отправляет повтор",async({page})=>{await setup(page);let posts=0;await page.route("**/api/traffic/flow/purge",r=>{posts++;return r.fulfill({status:409,json:{error:"Сброс NetFlow уже выполняется"}})});await open(page);await page.getByRole("button",{name:"Сбросить",exact:true}).click();await page.getByRole("alertdialog").getByRole("button",{name:"Сбросить",exact:true}).click();await expect(page.getByText("Новый сброс не запущен: Сброс NetFlow уже выполняется. Проверьте журнал перед новым сбросом.",{exact:true})).toBeVisible();await expect(page.getByRole("alertdialog")).toHaveCount(0);await expect(page.getByRole("button",{name:"Сбросить",exact:true})).toBeDisabled();expect(posts).toBe(1)})
|
|
|
|
test("NetFlow: сетевой ввод проверяется до HTTP и port инструкция отражает receipt",async({page})=>{
|
|
await setup(page);const writes:object[]=[];await page.route("**/api/traffic/flow/settings",r=>{if(r.request().method()!=="PUT")return r.fallback();const patch=r.request().postDataJSON();writes.push(patch);return r.fulfill({json:{ok:true,settings:{...settings,...patch}}})});await open(page)
|
|
const collector=page.getByRole("textbox",{name:"Collector IP",exact:true});await collector.fill("not-an-ip");await save(page).click();await expect(page.getByText("Укажите IPv4-адрес коллектора",{exact:true})).toBeVisible();expect(writes).toEqual([])
|
|
await collector.fill("10.255.254.1");const endpoint=page.getByRole("textbox",{name:"Публичный endpoint хоста MM",exact:true});await endpoint.fill("https://host.test");await save(page).click();await expect(page.getByText("Endpoint: укажите IP или DNS-имя без порта и протокола",{exact:true})).toBeVisible();expect(writes).toEqual([])
|
|
await endpoint.fill("Collector.EXAMPLE.test.");await field(page).fill("4740");await save(page).click();await expect(page.getByText("Сохранение настроек NetFlow подтверждено",{exact:true})).toBeVisible();expect(writes).toEqual([{collectorIp:"10.255.254.1",flowListenPort:4740,publicEndpoint:"Collector.EXAMPLE.test."}]);await expect(page.getByText(/Firewall: разрешите UDP listen WireGuard. UDP 4740/)).toBeVisible();await expect(page.getByText(/ss -ulnp.*4740/)).toBeVisible()
|
|
})
|
|
|
|
test("NetFlow: порог включается с явной долей, без default 5",async({page})=>{await setup(page);const writes:object[]=[];await page.route("**/api/traffic/flow/settings",r=>{if(r.request().method()!=="PUT")return r.fallback();const patch=r.request().postDataJSON();writes.push(patch);return r.fulfill({json:{ok:true,settings:{...settings,...patch}}})});await open(page);await page.getByRole("switch",{name:"Порог доли на карте",exact:true}).click();const share=page.getByRole("textbox",{name:"Минимум % окна",exact:true});await expect(share).toHaveValue("0");await save(page).click();await expect(page.getByText("Порог карты: укажите долю больше 0 и не более 100%",{exact:true})).toBeVisible();expect(writes).toEqual([]);await share.fill("0.25");await save(page).click();await expect(page.getByText("Сохранение настроек NetFlow подтверждено",{exact:true})).toBeVisible();expect(writes).toEqual([{mapServiceMinSharePct:0.25}]);await expect(share).toHaveValue("0.25")})
|
|
|
|
test("NetFlow: поздний GET не отменяет подтверждённый PUT",async({page})=>{await setup(page);let pending=false,started=false,completed=false,release!:()=>void;const gate=new Promise<void>(resolve=>release=resolve);await page.route("**/api/traffic/flow/settings",async r=>{if(r.request().method()==="PUT")return r.fulfill({json:{ok:true,settings:{...settings,...r.request().postDataJSON()}}});if(!pending)return r.fallback();started=true;await gate;try{await r.fulfill({json:settings})}catch{}finally{completed=true}});try{await open(page);pending=true;await refresh(page).click();await expect.poll(()=>started).toBe(true);await field(page).fill("4740");await save(page).click();await expect(page.getByText("Сохранение настроек NetFlow подтверждено",{exact:true})).toBeVisible();release();await expect.poll(()=>completed).toBe(true);await expect(field(page)).toHaveValue("4740");await expect(save(page)).toBeDisabled()}finally{release()}})
|
|
|
|
test("NetFlow: Settings ведут к единому подтверждению сброса без POST перехода",async({page})=>{await setup(page);let posts=0;page.on("request",r=>{if(new URL(r.url()).pathname==="/api/traffic/flow/purge")posts++});await page.goto("/settings");const link=page.getByRole("link",{name:"Открыть NetFlow",exact:true});await expect(link).toHaveAttribute("href","/data-collection#netflow");await link.click();await expect(page).toHaveURL(/data-collection#netflow$/);await expect(page.getByRole("region",{name:"Настройки NetFlow",exact:true})).toBeVisible();await expect(field(page)).toHaveValue("4739");expect(posts).toBe(0);await expect(page.getByRole("alertdialog")).toHaveCount(0)})
|