fix(alerts): подтверждать запись правил и сохранять формы при сбое
This commit is contained in:
1 parent
875819c577
commit
9c1d14635a
9 files changed
+198
-154
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import {test,expect,type Page} from "@playwright/test"
|
||||
import {emptyAlerts} from "./fixtures/alerts"
|
||||
import {emptyAlerts,alertsWithRule} from "./fixtures/alerts"
|
||||
const headers={"access-control-allow-origin":"*","access-control-allow-methods":"GET,OPTIONS","access-control-allow-headers":"Content-Type,Authorization"}
|
||||
async function source(page:Page,value:string){const input=page.getByRole("textbox",{name:"URL бекенда",exact:true});await input.fill(value);await input.press("Enter");await expect(input).toHaveValue(value);await expect(page.getByText("Доступен",{exact:true})).toBeVisible()}
|
||||
for(const failed of [false,true])test("Alerts source: ABA discards old primary and target outcomes/"+failed,async({page})=>{
|
||||
@@ -23,3 +23,11 @@ for(const kind of ["save","test"] as const)for(const failed of [false,true])test
|
||||
await page.goto("/settings");await source(page,"http://localhost:8101");await page.goto("/alerts");await expect(form().getByRole("button",{name:"Отправить тестовое сообщение",exact:true})).toBeEnabled();await page.goto("/settings");await source(page,"http://localhost:8000");await page.goto("/alerts");await expect(form()).toContainText("Результат запроса неизвестен");release();await expect.poll(()=>completed).toBe(true);await expect(form().getByRole("button",{name:"Отправить тестовое сообщение",exact:true})).toBeDisabled();await expect(form()).not.toContainText("Параметры Telegram сохранены в БД");await expect(form()).not.toContainText("API подтвердил приём");await expect(form()).not.toContainText("OLD TELEGRAM FAILED");await expect(page.getByRole("textbox",{name:"Chat ID по умолчанию",exact:true})).toHaveValue(configuredTelegram.chatId);expect(calls).toBe(1)
|
||||
}finally{release()}
|
||||
})
|
||||
|
||||
for(const failed of [false,true])test("Rules source: ABA ignores old save and retains scoped unknown checkpoint/"+failed,async({page})=>{
|
||||
await page.route("**/health",r=>r.fulfill({headers,json:{status:"ok"}}));await page.route("**/api/**",r=>{const path=new URL(r.request().url()).pathname;return r.fulfill({headers,json:path==="/api/auth/config"?{required:false}:path==="/api/alerts"?alertsWithRule:path==="/api/filters/gre-tunnels"?{tunnels:[],failures:[]}:[]})})
|
||||
let started=false,completed=false,calls=0,release!:()=>void;const gate=new Promise<void>(resolve=>release=resolve)
|
||||
await page.route("http://localhost:8000/api/alerts/rules",async r=>{calls++;started=true;const body=r.request().postDataJSON();await gate;try{await r.fulfill(failed?{headers,status:503,json:{error:"OLD RULES FAILED"}}:{headers,json:{ok:true,groups:body.groups,rules:body.rules.map((row:object)=>({...row,lastFiredAt:null}))}})}catch{}finally{completed=true}})
|
||||
const toggle=()=>page.getByRole("switch",{name:"Включить правило Fixture rule",exact:true})
|
||||
try{await page.goto("/alerts");await expect(toggle()).toBeEnabled();await toggle().click();await expect.poll(()=>started).toBe(true);await page.goto("/settings");await source(page,"http://localhost:8101");await page.goto("/alerts");await expect(toggle()).toBeEnabled();await page.goto("/settings");await source(page,"http://localhost:8000");await page.goto("/alerts");await expect(page.getByText(/Результат сохранения правил неизвестен/)).toBeVisible();release();await expect.poll(()=>completed).toBe(true);await expect(toggle()).toBeChecked();await expect(toggle()).toBeDisabled();await expect(page.getByText("Правила и группы сохранены в БД",{exact:true})).toHaveCount(0);await expect(page.getByText("OLD RULES FAILED",{exact:true})).toHaveCount(0);expect(calls).toBe(1)}finally{release()}
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
import {test,expect,type Page,type Route} from "@playwright/test"
|
||||
import {alertsWithRule} from "./fixtures/alerts"
|
||||
async function setup(page:Page,write:(route:Route)=>Promise<unknown>,required=false){
|
||||
await page.route("**/health",r=>r.fulfill({json:{status:"ok"}}));await page.route("**/api/**",r=>{const path=new URL(r.request().url()).pathname;if(path==="/api/auth/config")return r.fulfill({json:{required}});if(path==="/api/alerts")return r.fulfill({json:alertsWithRule});if(path==="/api/alerts/rules")return write(r);return r.fulfill({json:path==="/api/filters/gre-tunnels"?{tunnels:[],failures:[]}:[]})});await page.goto("/alerts");await expect(page.getByText("Всего: 1",{exact:true})).toBeVisible()
|
||||
}
|
||||
function receipt(r:Route){const body=r.request().postDataJSON();return {ok:true,groups:body.groups,rules:body.rules.map((rule:object)=>({...rule,lastFiredAt:null}))}}
|
||||
const toggle=(page:Page)=>page.getByRole("switch",{name:"Включить правило Fixture rule",exact:true})
|
||||
test("Rules: pending toggle keeps confirmed state, excludes writes and commits only strict receipt",async({page})=>{
|
||||
let calls=0,release!:()=>void;const gate=new Promise<void>(resolve=>release=resolve)
|
||||
try{await setup(page,async r=>{calls++;await gate;return r.fulfill({json:receipt(r)})});await toggle(page).click();await expect.poll(()=>calls).toBe(1);await expect(toggle(page)).toBeChecked();await expect(toggle(page)).toBeDisabled();await expect(page.getByRole("button",{name:"Обновить",exact:true})).toBeDisabled();for(const button of await page.getByRole("button",{name:"Добавить правило",exact:true}).all())await expect(button).toBeDisabled();release();await expect(toggle(page)).not.toBeChecked();await expect(page.getByText("Правила и группы сохранены в БД",{exact:true})).toBeVisible();expect(calls).toBe(1)}finally{release()}
|
||||
})
|
||||
test("Rules: failed edit keeps form and recovery settings for explicit retry",async({page})=>{
|
||||
let calls=0,payload:unknown,reject=true;await setup(page,r=>{calls++;payload=r.request().postDataJSON();return r.fulfill(reject?{status:409,json:{error:"Fixture conflict"}}:{json:receipt(r)})});await page.getByRole("button",{name:"Изменить правило Fixture rule",exact:true}).click();const dialog=page.getByRole("dialog"),name=dialog.getByRole("textbox",{name:"Название правила",exact:true});await name.fill("Changed rule");await dialog.getByRole("button",{name:"Сохранить изменения",exact:true}).click();await expect(dialog.getByRole("alert")).toHaveText("Fixture conflict");await expect(name).toHaveValue("Changed rule");await expect(page.getByText("Fixture rule",{exact:true})).toBeVisible();expect((payload as {rules:{recoveryMode:string;recoveryStabilitySec:number}[]}).rules[0]).toMatchObject({recoveryMode:"conditional",recoveryStabilitySec:17});reject=false;await dialog.getByRole("button",{name:"Сохранить изменения",exact:true}).click();await expect(dialog).not.toBeVisible();await expect(page.getByText("Changed rule",{exact:true})).toBeVisible();expect(calls).toBe(2)
|
||||
})
|
||||
test("Rules: malformed delete preserves row and unknown guard across refresh/reload",async({page})=>{
|
||||
let calls=0;await setup(page,r=>{calls++;return r.fulfill({json:{ok:false}})});await page.getByRole("button",{name:"Удалить правило Fixture rule",exact:true}).click();await page.getByRole("alertdialog").getByRole("button",{name:"Удалить",exact:true}).click();await expect(page.locator("#main-content").getByRole("alert")).toContainText("не подтверждено");await expect(page.getByText("Fixture rule",{exact:true})).toBeVisible();await expect(toggle(page)).toBeDisabled();await page.getByRole("button",{name:"Обновить",exact:true}).click();await expect(toggle(page)).toBeDisabled();await page.reload();await expect(page.getByText(/Результат сохранения правил неизвестен/)).toBeVisible();await expect(toggle(page)).toBeDisabled();expect(calls).toBe(1)
|
||||
})
|
||||
test("Groups: typing is draft, failed save retains it and successful receipt updates group",async({page})=>{
|
||||
let calls=0,reject=true,payload:unknown;await setup(page,r=>{calls++;payload=r.request().postDataJSON();return r.fulfill(reject?{status:409,json:{error:"Fixture group conflict"}}:{json:receipt(r)})});await page.getByRole("button",{name:/Пресеты и группы/}).click();const group=page.getByRole("region",{name:"Группа Fixture group",exact:true});await group.getByRole("textbox",{name:"Имя группы",exact:true}).fill("New draft group");expect(calls).toBe(0);await group.getByRole("combobox",{name:"Режим группы",exact:true}).selectOption("all");await group.getByRole("button",{name:"Сохранить группу",exact:true}).click();await expect(page.locator("#main-content").getByRole("alert")).toContainText("Fixture group conflict");await expect(group.getByRole("textbox")).toHaveValue("New draft group");expect((payload as {groups:object[]}).groups[0]).toMatchObject({name:"New draft group",combineMode:"all"});reject=false;await group.getByRole("button",{name:"Сохранить группу",exact:true}).click();await expect(page.getByRole("region",{name:"Группа New draft group",exact:true})).toBeVisible();expect(calls).toBe(2)
|
||||
})
|
||||
test("Rules: read permission keeps all mutation entry points disabled",async({page})=>{
|
||||
const token="header."+Buffer.from(JSON.stringify({sub:"fixture",apps:["mm"],permissions:["mm:alerts:read"],exp:Math.floor(Date.now()/1000)+3600})).toString("base64url")+".fixture";await page.addInitScript(value=>localStorage.setItem("mmapp_token",value),token);let calls=0;await setup(page,r=>{calls++;return r.fulfill({json:receipt(r)})},true);await expect(toggle(page)).toBeDisabled();await expect(page.getByRole("button",{name:"Изменить правило Fixture rule",exact:true})).toBeDisabled();await expect(page.getByRole("button",{name:"Удалить правило Fixture rule",exact:true})).toBeDisabled();for(const button of await page.getByRole("button",{name:"Добавить правило",exact:true}).all())await expect(button).toBeDisabled();await page.getByRole("button",{name:/Пресеты и группы/}).click();await expect(page.getByRole("button",{name:"Группа",exact:true})).toBeDisabled();await expect(page.getByRole("button",{name:"Добавить к списку",exact:true}).first()).toBeDisabled();await expect(page.getByRole("region",{name:"Группа Fixture group",exact:true}).getByRole("textbox")).toBeDisabled();expect(calls).toBe(0)
|
||||
})
|
||||
|
||||
test("Rules: unknown edit keeps draft through explicit review and confirmed retry",async({page})=>{let calls=0;await setup(page,r=>{calls++;return r.fulfill({json:calls===1?{ok:false}:receipt(r)})});await page.getByRole("button",{name:"Изменить правило Fixture rule",exact:true}).click();const dialog=page.getByRole("dialog"),input=dialog.getByRole("textbox",{name:"Название правила",exact:true});await input.fill("Changed unknown draft");await dialog.getByRole("button",{name:"Сохранить изменения",exact:true}).click();await expect(dialog.getByRole("alert")).toContainText("не подтверждено");await expect(input).toHaveValue("Changed unknown draft");await expect(dialog.getByRole("button",{name:"Сохранить изменения",exact:true})).toBeDisabled();await dialog.getByRole("button",{name:"Я проверил правила — разрешить новый запрос",exact:true}).click();await dialog.getByRole("button",{name:"Сохранить изменения",exact:true}).click();await expect(dialog).not.toBeVisible();await expect(page.getByText("Changed unknown draft",{exact:true})).toBeVisible();expect(calls).toBe(2)})
|
||||
test("Rules presets: no confirmed RTT targets cannot clear existing rules",async({page})=>{let calls=0;await setup(page,r=>{calls++;return r.fulfill({json:receipt(r)})});await page.getByRole("button",{name:/Пресеты и группы/}).click();await page.getByRole("button",{name:"Заменить все",exact:true}).nth(1).click();await page.getByRole("alertdialog").getByRole("button",{name:"Заменить",exact:true}).click();await expect(page.locator("#main-content").getByRole("alert")).toHaveText("Для пресета нет подтверждённых объектов. Список правил сохранён.");await expect(page.getByText("Fixture rule",{exact:true})).toBeVisible();expect(calls).toBe(0)})
|
||||
test("Groups: confirmed delete removes group while retaining its rules without membership",async({page})=>{let payload:{groups:unknown[];rules:{groupId:string|null}[]}|undefined;await setup(page,r=>{payload=r.request().postDataJSON();return r.fulfill({json:receipt(r)})});await page.getByRole("button",{name:/Пресеты и группы/}).click();await page.getByRole("region",{name:"Группа Fixture group",exact:true}).getByRole("button",{name:"Удалить группу",exact:true}).click();await page.getByRole("alertdialog").getByRole("button",{name:"Удалить",exact:true}).click();await expect(page.getByRole("region",{name:"Группа Fixture group",exact:true})).toHaveCount(0);await expect(page.getByText("Fixture rule",{exact:true})).toBeVisible();expect(payload?.groups).toEqual([]);expect(payload?.rules).toHaveLength(1);expect(payload?.rules[0]?.groupId).toBeNull()})
|
||||
@@ -1 +1,4 @@
|
||||
export const emptyAlerts={telegram:{chatId:"",tokenConfigured:false,connected:false,messageThreadId:null},groups:[],rules:[],history:[],meta:{servers:[],greClients:[],probeTargets:[],rttLossTargets:[],trafficServers:[],serversDetail:[]}}
|
||||
export const alertsRule={id:"rule-fixture",name:"Fixture rule",type:"server",target:"router-fixture",targets:["router-fixture"],groupId:"group-fixture",condition:"перешёл в offline",conditions:["перешёл в offline"],severity:"critical",enabled:true,cooldown:"5м",confirmStabilitySec:5,recoveryMode:"conditional",recoveryStabilitySec:17,chatId:"",lastFiredAt:null}
|
||||
export const alertsGroup={id:"group-fixture",name:"Fixture group",combineMode:"any",enabled:true,cooldownOverride:null}
|
||||
export const alertsWithRule={...emptyAlerts,rules:[alertsRule],groups:[alertsGroup],meta:{...emptyAlerts.meta,servers:["router-fixture"],serversDetail:[{name:"router-fixture",host:"192.0.2.1",site:"",country:"",type:""}]}}
|
||||
Reference in new issue
Block a user