163 lines
5.8 KiB
TypeScript
163 lines
5.8 KiB
TypeScript
import { test, expect, Page } from '@playwright/test';
|
|||
|
|
import {
|
||
|
|
HS,
|
||
|
|
createRoom,
|
||
|
|
ensureUser,
|
||
|
|
hsReachable,
|
||
|
|
loginUI,
|
||
|
|
openRoom,
|
||
|
|
uniq,
|
||
|
|
TestUser,
|
||
|
|
} from './localHs';
|
||
|
|
|
||
|
|
// [Gitea #124] Status banner from the homeserver's Uptime Kuma page. Kuma is
|
||
|
|
// answered by fixtures here; nothing contacts a real Kuma.
|
||
|
|
const KUMA = 'https://isitup.lotusguild.org';
|
||
|
|
const PAGE_CFG = { url: KUMA, slug: 'matrix' };
|
||
|
|
const UP = 1;
|
||
|
|
const DOWN = 0;
|
||
|
|
|
||
|
|
type Fixture = { hs?: number; calls?: number; staleMinutes?: number; fail?: boolean };
|
||
|
|
|
||
|
|
const kumaTime = (ms: number) => new Date(ms).toISOString().replace('T', ' ').slice(0, 23);
|
||
|
|
|
||
|
|
/** Serve config.json with a status page for `serverName` and answer Kuma from `fx()`. */
|
||
|
|
async function mockKuma(page: Page, serverName: string | null, fx: () => Fixture) {
|
||
|
|
const kumaRequests: string[] = [];
|
||
|
|
await page.route(/\/config\.json(\?.*)?$/, async (route) => {
|
||
|
|
const res = await route.fetch();
|
||
|
|
const cfg = await res.json();
|
||
|
|
cfg.statusPages = serverName ? { [serverName]: PAGE_CFG } : {};
|
||
|
|
await route.fulfill({ response: res, json: cfg });
|
||
|
|
});
|
||
|
|
await page.route(/isitup\.lotusguild\.org\/api\/status-page\//, async (route) => {
|
||
|
|
const url = route.request().url();
|
||
|
|
kumaRequests.push(url);
|
||
|
|
const f = fx();
|
||
|
|
if (f.fail) {
|
||
|
|
await route.fulfill({ status: 500, body: 'oops' });
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const cors = { 'access-control-allow-origin': '*' };
|
||
|
|
if (url.includes('/heartbeat/')) {
|
||
|
|
// Two checks, the latest `staleMinutes` ago (0 = just now).
|
||
|
|
const last = Date.now() - (f.staleMinutes ?? 0) * 60_000;
|
||
|
|
const beats = (s: number) => [
|
||
|
|
{ status: s, time: kumaTime(last - 60_000) },
|
||
|
|
{ status: s, time: kumaTime(last) },
|
||
|
|
];
|
||
|
|
await route.fulfill({
|
||
|
|
headers: cors,
|
||
|
|
json: { heartbeatList: { 1: beats(f.hs ?? UP), 2: beats(f.calls ?? UP) }, uptimeList: {} },
|
||
|
|
});
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
await route.fulfill({
|
||
|
|
headers: cors,
|
||
|
|
json: {
|
||
|
|
config: { slug: 'matrix' },
|
||
|
|
incidents: [],
|
||
|
|
maintenanceList: [],
|
||
|
|
publicGroupList: [
|
||
|
|
{ name: 'Homeserver', monitorList: [{ id: 1 }] },
|
||
|
|
{ name: 'Voice calls', monitorList: [{ id: 2 }] },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
});
|
||
|
|
});
|
||
|
|
return kumaRequests;
|
||
|
|
}
|
||
|
|
|
||
|
|
const serverOf = (u: TestUser) => u.userId.split(':').slice(1).join(':');
|
||
|
|
const strip = (page: Page, text: RegExp) =>
|
||
|
|
page.locator('[role="status"], [role="alert"]').filter({ hasText: text });
|
||
|
|
const cutSync = (page: Page) =>
|
||
|
|
page.route(/\/_matrix\/client\/v3\/sync/, (route) => route.abort('connectionfailed'));
|
||
|
|
|
||
|
|
test.describe('server status banner (#124)', () => {
|
||
|
|
let alice: TestUser;
|
||
|
|
let room: string;
|
||
|
|
|
||
|
|
test.beforeAll(async () => {
|
||
|
|
test.skip(!(await hsReachable()), `no local homeserver at ${HS} (set E2E_LOCAL_HS)`);
|
||
|
|
alice = await ensureUser(uniq('e2e_status_'));
|
||
|
|
room = await createRoom(alice, 'Status Room');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('calls down: amber strip, dismiss sticks across a reload', async ({ page }) => {
|
||
|
|
await mockKuma(page, serverOf(alice), () => ({ calls: DOWN }));
|
||
|
|
await loginUI(page, alice);
|
||
|
|
await openRoom(page, room);
|
||
|
|
const calls = strip(page, /Voice calls are down right now\. Messages still work\./);
|
||
|
|
await expect(calls).toBeVisible();
|
||
|
|
await calls.getByRole('button', { name: 'Dismiss' }).click();
|
||
|
|
await expect(calls).toHaveCount(0);
|
||
|
|
await page.reload();
|
||
|
|
await openRoom(page, room);
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
await expect(calls).toHaveCount(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('other homeservers never contact Kuma', async ({ page }) => {
|
||
|
|
const requests = await mockKuma(page, 'some.other.server', () => ({ hs: DOWN }));
|
||
|
|
await loginUI(page, alice);
|
||
|
|
await openRoom(page, room);
|
||
|
|
await page.waitForTimeout(3000);
|
||
|
|
expect(requests).toEqual([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('Kuma failing shows nothing (fail quiet)', async ({ page }) => {
|
||
|
|
const requests = await mockKuma(page, serverOf(alice), () => ({ fail: true }));
|
||
|
|
await loginUI(page, alice);
|
||
|
|
await openRoom(page, room);
|
||
|
|
await expect.poll(() => requests.length).toBeGreaterThan(0);
|
||
|
|
await page.waitForTimeout(1500);
|
||
|
|
await expect(strip(page, /server|Voice calls|Maintenance/i)).toHaveCount(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('connection lost + Kuma says the server is down: critical strip, not "Connection Lost"', async ({
|
||
|
|
page,
|
||
|
|
}) => {
|
||
|
|
test.setTimeout(150_000);
|
||
|
|
await mockKuma(page, serverOf(alice), () => ({ hs: DOWN }));
|
||
|
|
await loginUI(page, alice);
|
||
|
|
await openRoom(page, room);
|
||
|
|
// Connected: the server is "having problems" for others, not for us.
|
||
|
|
await expect(strip(page, /server is having problems/)).toBeVisible();
|
||
|
|
await cutSync(page);
|
||
|
|
const down = page.getByRole('alert').filter({ hasText: /server is down\. We're on it/ });
|
||
|
|
await expect(down).toBeVisible({ timeout: 120_000 });
|
||
|
|
await expect(page.getByText('Connection Lost! Reconnecting...')).toHaveCount(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('connection lost + Kuma checked since and the server is up: "may be your connection"', async ({
|
||
|
|
page,
|
||
|
|
}) => {
|
||
|
|
test.setTimeout(150_000);
|
||
|
|
await mockKuma(page, serverOf(alice), () => ({ hs: UP }));
|
||
|
|
await loginUI(page, alice);
|
||
|
|
await openRoom(page, room);
|
||
|
|
await cutSync(page);
|
||
|
|
await expect(page.getByText(/server is up, so it may be your internet connection/)).toBeVisible(
|
||
|
|
{
|
||
|
|
timeout: 120_000,
|
||
|
|
},
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('connection lost + only a stale "up" from Kuma: plain "Connection Lost"', async ({
|
||
|
|
page,
|
||
|
|
}) => {
|
||
|
|
test.setTimeout(150_000);
|
||
|
|
await mockKuma(page, serverOf(alice), () => ({ hs: UP, staleMinutes: 10 }));
|
||
|
|
await loginUI(page, alice);
|
||
|
|
await openRoom(page, room);
|
||
|
|
await cutSync(page);
|
||
|
|
await expect(page.getByText('Connection Lost! Reconnecting...')).toBeVisible({
|
||
|
|
timeout: 120_000,
|
||
|
|
});
|
||
|
|
await page.waitForTimeout(3000);
|
||
|
|
await expect(page.getByText(/may be your internet connection/)).toHaveCount(0);
|
||
|
|
});
|
||
|
|
});
|