wip: doctor/cli/docs/api to vector db consolidation; api hardening for descriptions, tenant, and scopes; migrations and conversions of all DALs to EF v10
This commit is contained in:
173
src/Web/StellaOps.Web/tests/e2e/tenant-switch-matrix.spec.ts
Normal file
173
src/Web/StellaOps.Web/tests/e2e/tenant-switch-matrix.spec.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
|
||||
import { type CapturedApiRequest, installMultiTenantSessionFixture } from './support/multi-tenant-session.fixture';
|
||||
import { tenantSwitchPageMatrix } from './support/tenant-switch-page-matrix';
|
||||
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test.describe('Multi-tenant switch matrix', () => {
|
||||
test('switches tenant from header and persists across primary sections (desktop)', async ({ page }) => {
|
||||
const fixture = await installMultiTenantSessionFixture(page);
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
|
||||
await go(page, '/mission-control/board');
|
||||
await expectTenantLabelContains(page, 'alpha');
|
||||
|
||||
await switchTenant(page, 'Tenant Bravo');
|
||||
await expectTenantLabelContains(page, 'bravo');
|
||||
|
||||
fixture.clearCapturedApiRequests();
|
||||
|
||||
for (const entry of tenantSwitchPageMatrix) {
|
||||
await navigateInApp(page, entry.route);
|
||||
await expect(page.locator('main')).toBeVisible();
|
||||
await expectTenantLabelContains(page, 'bravo');
|
||||
}
|
||||
|
||||
const tenantScopedRequests = fixture.capturedApiRequests.filter((request) => {
|
||||
const url = request.url.toLowerCase();
|
||||
return !url.includes('/api/auth/') &&
|
||||
!url.includes('/health') &&
|
||||
!url.includes('/ready') &&
|
||||
!url.includes('/metrics');
|
||||
});
|
||||
|
||||
expect(tenantScopedRequests.length).toBeGreaterThan(0);
|
||||
for (const request of tenantScopedRequests) {
|
||||
expect(request.tenantId).toBe('tenant-bravo');
|
||||
}
|
||||
});
|
||||
|
||||
test('keeps tenant selector usable and persistent on mobile viewport', async ({ page }) => {
|
||||
await installMultiTenantSessionFixture(page);
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
|
||||
await go(page, '/mission-control/board');
|
||||
await expect(page.locator('.topbar__tenant-btn')).toBeVisible();
|
||||
await switchTenant(page, 'Tenant Bravo');
|
||||
await expectTenantLabelContains(page, 'bravo');
|
||||
|
||||
await navigateInApp(page, '/setup/identity-access');
|
||||
await expectTenantLabelContains(page, 'bravo');
|
||||
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => null);
|
||||
await expectTenantLabelContains(page, 'bravo');
|
||||
});
|
||||
|
||||
for (const entry of tenantSwitchPageMatrix) {
|
||||
test(`applies selected tenant on ${entry.section} route`, async ({ page }) => {
|
||||
const fixture = await installMultiTenantSessionFixture(page);
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
|
||||
await go(page, '/mission-control/board');
|
||||
await switchTenant(page, 'Tenant Bravo');
|
||||
await expectTenantLabelContains(page, 'bravo');
|
||||
|
||||
fixture.clearCapturedApiRequests();
|
||||
|
||||
await navigateInApp(page, entry.route);
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe(entry.route);
|
||||
const routeHints = buildRouteHints(entry.section, entry.expectedBreadcrumb);
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const mainText = (await page.locator('main').innerText()).toLowerCase();
|
||||
return routeHints.some((hint) => mainText.includes(hint.toLowerCase()));
|
||||
})
|
||||
.toBe(true);
|
||||
await expectTenantLabelContains(page, 'bravo');
|
||||
|
||||
const tenantScopedRequests = toTenantScopedRequests(fixture.capturedApiRequests);
|
||||
const crossTenantRequests = tenantScopedRequests.filter((request) => request.tenantId && request.tenantId !== 'tenant-bravo');
|
||||
expect(crossTenantRequests.length).toBe(0);
|
||||
for (const request of tenantScopedRequests) {
|
||||
expect(request.tenantId).toBe('tenant-bravo');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('switches tenant in both directions without stale request headers', async ({ page }) => {
|
||||
const fixture = await installMultiTenantSessionFixture(page);
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
|
||||
await go(page, '/mission-control/board');
|
||||
|
||||
await switchTenant(page, 'Tenant Bravo');
|
||||
await expectTenantLabelContains(page, 'bravo');
|
||||
|
||||
fixture.clearCapturedApiRequests();
|
||||
await navigateInApp(page, '/setup/topology/overview');
|
||||
const bravoRequests = toTenantScopedRequests(fixture.capturedApiRequests);
|
||||
const crossTenantBravoRequests = bravoRequests.filter((request) => request.tenantId && request.tenantId !== 'tenant-bravo');
|
||||
expect(crossTenantBravoRequests.length).toBe(0);
|
||||
for (const request of bravoRequests) {
|
||||
expect(request.tenantId).toBe('tenant-bravo');
|
||||
}
|
||||
|
||||
await switchTenant(page, 'Tenant Alpha');
|
||||
await expectTenantLabelContains(page, 'alpha');
|
||||
|
||||
fixture.clearCapturedApiRequests();
|
||||
await navigateInApp(page, '/security/posture');
|
||||
const alphaRequests = toTenantScopedRequests(fixture.capturedApiRequests);
|
||||
const crossTenantAlphaRequests = alphaRequests.filter((request) => request.tenantId && request.tenantId !== 'tenant-alpha');
|
||||
expect(crossTenantAlphaRequests.length).toBe(0);
|
||||
for (const request of alphaRequests) {
|
||||
expect(request.tenantId).toBe('tenant-alpha');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function toTenantScopedRequests(capturedApiRequests: readonly CapturedApiRequest[]): CapturedApiRequest[] {
|
||||
return capturedApiRequests.filter((request) => {
|
||||
const url = request.url.toLowerCase();
|
||||
return !url.includes('/api/auth/') &&
|
||||
!url.includes('/health') &&
|
||||
!url.includes('/ready') &&
|
||||
!url.includes('/metrics');
|
||||
});
|
||||
}
|
||||
|
||||
function buildRouteHints(section: string, expectedBreadcrumb: string): readonly string[] {
|
||||
const hints = new Set<string>();
|
||||
const sectionHint = section.trim();
|
||||
const singularSectionHint = sectionHint.endsWith('s') ? sectionHint.slice(0, -1) : sectionHint;
|
||||
hints.add(expectedBreadcrumb.trim());
|
||||
hints.add(sectionHint);
|
||||
hints.add(singularSectionHint);
|
||||
return [...hints].filter((hint) => hint.length > 0);
|
||||
}
|
||||
|
||||
async function switchTenant(page: Page, displayName: string): Promise<void> {
|
||||
const trigger = page.locator('.topbar__tenant-btn');
|
||||
await trigger.click();
|
||||
|
||||
const option = page.locator('.topbar__tenant-option-name', {
|
||||
hasText: displayName,
|
||||
});
|
||||
await expect(option).toBeVisible();
|
||||
await option.click();
|
||||
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => null);
|
||||
}
|
||||
|
||||
async function go(page: Page, path: string): Promise<void> {
|
||||
await page.goto(path, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => null);
|
||||
}
|
||||
|
||||
async function navigateInApp(page: Page, path: string): Promise<void> {
|
||||
await page.evaluate((nextPath) => {
|
||||
window.history.pushState({}, '', nextPath);
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
}, path);
|
||||
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe(path);
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => null);
|
||||
}
|
||||
|
||||
async function expectTenantLabelContains(page: Page, tenantHint: string): Promise<void> {
|
||||
await expect
|
||||
.poll(async () => (await page.locator('.topbar__tenant-btn').innerText()).toLowerCase())
|
||||
.toContain(tenantHint.toLowerCase());
|
||||
}
|
||||
Reference in New Issue
Block a user