fix: multiple bug fixes
All checks were successful
Queue Release Build / prepare (push) Successful in 15s
Deploy Web Apps / deploy (push) Successful in 6m54s
Queue Release Build / build-windows (push) Successful in 16m6s
Queue Release Build / build-linux (push) Successful in 30m58s
Queue Release Build / finalize (push) Successful in 44s
All checks were successful
Queue Release Build / prepare (push) Successful in 15s
Deploy Web Apps / deploy (push) Successful in 6m54s
Queue Release Build / build-windows (push) Successful in 16m6s
Queue Release Build / build-linux (push) Successful in 30m58s
Queue Release Build / finalize (push) Successful in 44s
isolated users, db backup, weird disconnect issues for long voice sessions,
This commit is contained in:
269
e2e/tests/auth/user-session-data-isolation.spec.ts
Normal file
269
e2e/tests/auth/user-session-data-isolation.spec.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
chromium,
|
||||
type BrowserContext,
|
||||
type Page
|
||||
} from '@playwright/test';
|
||||
import { test, expect } from '../../fixtures/multi-client';
|
||||
import { installTestServerEndpoint } from '../../helpers/seed-test-endpoint';
|
||||
import { ChatMessagesPage } from '../../pages/chat-messages.page';
|
||||
import { LoginPage } from '../../pages/login.page';
|
||||
import { RegisterPage } from '../../pages/register.page';
|
||||
import { ServerSearchPage } from '../../pages/server-search.page';
|
||||
|
||||
interface TestUser {
|
||||
username: string;
|
||||
displayName: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface PersistentClient {
|
||||
context: BrowserContext;
|
||||
page: Page;
|
||||
userDataDir: string;
|
||||
}
|
||||
|
||||
const CLIENT_LAUNCH_ARGS = [
|
||||
'--use-fake-device-for-media-stream',
|
||||
'--use-fake-ui-for-media-stream'
|
||||
];
|
||||
|
||||
test.describe('User session data isolation', () => {
|
||||
test.describe.configure({ timeout: 240_000 });
|
||||
|
||||
test('preserves a user saved rooms and local history across app restarts', async ({ testServer }) => {
|
||||
const suffix = uniqueName('persist');
|
||||
const userDataDir = await mkdtemp(join(tmpdir(), 'metoyou-auth-persist-'));
|
||||
const alice: TestUser = {
|
||||
username: `alice_${suffix}`,
|
||||
displayName: 'Alice',
|
||||
password: 'TestPass123!'
|
||||
};
|
||||
const aliceServerName = `Alice Session Server ${suffix}`;
|
||||
const aliceMessage = `Alice persisted message ${suffix}`;
|
||||
let client: PersistentClient | null = null;
|
||||
|
||||
try {
|
||||
client = await launchPersistentClient(userDataDir, testServer.port);
|
||||
|
||||
await test.step('Alice registers and creates local chat history', async () => {
|
||||
await registerUser(client.page, alice);
|
||||
await createServerAndSendMessage(client.page, aliceServerName, aliceMessage);
|
||||
});
|
||||
|
||||
await test.step('Alice sees the same saved room and message after a full restart', async () => {
|
||||
await restartPersistentClient(client, testServer.port);
|
||||
await openApp(client.page);
|
||||
await expect(client.page).not.toHaveURL(/\/login/, { timeout: 15_000 });
|
||||
await expectSavedRoomAndHistory(client.page, aliceServerName, aliceMessage);
|
||||
});
|
||||
} finally {
|
||||
await closePersistentClient(client);
|
||||
await rm(userDataDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('gives a new user a blank slate and restores only that user local data after account switches', async ({ testServer }) => {
|
||||
const suffix = uniqueName('isolation');
|
||||
const userDataDir = await mkdtemp(join(tmpdir(), 'metoyou-auth-isolation-'));
|
||||
const alice: TestUser = {
|
||||
username: `alice_${suffix}`,
|
||||
displayName: 'Alice',
|
||||
password: 'TestPass123!'
|
||||
};
|
||||
const bob: TestUser = {
|
||||
username: `bob_${suffix}`,
|
||||
displayName: 'Bob',
|
||||
password: 'TestPass123!'
|
||||
};
|
||||
const aliceServerName = `Alice Private Server ${suffix}`;
|
||||
const bobServerName = `Bob Private Server ${suffix}`;
|
||||
const aliceMessage = `Alice history ${suffix}`;
|
||||
const bobMessage = `Bob history ${suffix}`;
|
||||
let client: PersistentClient | null = null;
|
||||
|
||||
try {
|
||||
client = await launchPersistentClient(userDataDir, testServer.port);
|
||||
|
||||
await test.step('Alice creates persisted local data and verifies it survives a restart', async () => {
|
||||
await registerUser(client.page, alice);
|
||||
await createServerAndSendMessage(client.page, aliceServerName, aliceMessage);
|
||||
|
||||
await restartPersistentClient(client, testServer.port);
|
||||
await openApp(client.page);
|
||||
await expectSavedRoomAndHistory(client.page, aliceServerName, aliceMessage);
|
||||
});
|
||||
|
||||
await test.step('Bob starts from a blank slate in the same browser profile', async () => {
|
||||
await logoutUser(client.page);
|
||||
await registerUser(client.page, bob);
|
||||
await expectBlankSlate(client.page, [aliceServerName]);
|
||||
});
|
||||
|
||||
await test.step('Bob gets only his own saved room and history after a restart', async () => {
|
||||
await createServerAndSendMessage(client.page, bobServerName, bobMessage);
|
||||
|
||||
await restartPersistentClient(client, testServer.port);
|
||||
await openApp(client.page);
|
||||
await expectSavedRoomAndHistory(client.page, bobServerName, bobMessage);
|
||||
await expectSavedRoomHidden(client.page, aliceServerName);
|
||||
});
|
||||
|
||||
await test.step('When Alice logs back in she sees only Alice local data, not Bob data', async () => {
|
||||
await logoutUser(client.page);
|
||||
await restartPersistentClient(client, testServer.port);
|
||||
await loginUser(client.page, alice);
|
||||
|
||||
await expectSavedRoomVisible(client.page, aliceServerName);
|
||||
await expectSavedRoomHidden(client.page, bobServerName);
|
||||
await expectSavedRoomAndHistory(client.page, aliceServerName, aliceMessage);
|
||||
});
|
||||
} finally {
|
||||
await closePersistentClient(client);
|
||||
await rm(userDataDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function launchPersistentClient(userDataDir: string, testServerPort: number): Promise<PersistentClient> {
|
||||
const context = await chromium.launchPersistentContext(userDataDir, {
|
||||
args: CLIENT_LAUNCH_ARGS,
|
||||
baseURL: 'http://localhost:4200',
|
||||
permissions: ['microphone', 'camera']
|
||||
});
|
||||
|
||||
await installTestServerEndpoint(context, testServerPort);
|
||||
|
||||
const page = context.pages()[0] ?? await context.newPage();
|
||||
|
||||
return {
|
||||
context,
|
||||
page,
|
||||
userDataDir
|
||||
};
|
||||
}
|
||||
|
||||
async function restartPersistentClient(client: PersistentClient, testServerPort: number): Promise<void> {
|
||||
await client.context.close();
|
||||
|
||||
const restartedClient = await launchPersistentClient(client.userDataDir, testServerPort);
|
||||
|
||||
client.context = restartedClient.context;
|
||||
client.page = restartedClient.page;
|
||||
}
|
||||
|
||||
async function closePersistentClient(client: PersistentClient | null): Promise<void> {
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
|
||||
await client.context.close().catch(() => {});
|
||||
}
|
||||
|
||||
async function openApp(page: Page): Promise<void> {
|
||||
await retryTransientNavigation(() => page.goto('/', { waitUntil: 'domcontentloaded' }));
|
||||
}
|
||||
|
||||
async function registerUser(page: Page, user: TestUser): Promise<void> {
|
||||
const registerPage = new RegisterPage(page);
|
||||
|
||||
await retryTransientNavigation(() => registerPage.goto());
|
||||
await registerPage.register(user.username, user.displayName, user.password);
|
||||
await expect(page).toHaveURL(/\/search/, { timeout: 15_000 });
|
||||
}
|
||||
|
||||
async function loginUser(page: Page, user: TestUser): Promise<void> {
|
||||
const loginPage = new LoginPage(page);
|
||||
|
||||
await retryTransientNavigation(() => loginPage.goto());
|
||||
await loginPage.login(user.username, user.password);
|
||||
await expect(page).toHaveURL(/\/(search|room)(\/|$)/, { timeout: 15_000 });
|
||||
}
|
||||
|
||||
async function logoutUser(page: Page): Promise<void> {
|
||||
const menuButton = page.getByRole('button', { name: 'Menu' });
|
||||
const logoutButton = page.getByRole('button', { name: 'Logout' });
|
||||
const loginPage = new LoginPage(page);
|
||||
|
||||
await expect(menuButton).toBeVisible({ timeout: 10_000 });
|
||||
await menuButton.click();
|
||||
await expect(logoutButton).toBeVisible({ timeout: 10_000 });
|
||||
await logoutButton.click();
|
||||
await expect(page).toHaveURL(/\/login/, { timeout: 15_000 });
|
||||
await expect(loginPage.usernameInput).toBeVisible({ timeout: 10_000 });
|
||||
}
|
||||
|
||||
async function createServerAndSendMessage(page: Page, serverName: string, messageText: string): Promise<void> {
|
||||
const searchPage = new ServerSearchPage(page);
|
||||
const messagesPage = new ChatMessagesPage(page);
|
||||
|
||||
await searchPage.createServer(serverName, {
|
||||
description: `User session isolation coverage for ${serverName}`
|
||||
});
|
||||
await expect(page).toHaveURL(/\/room\//, { timeout: 15_000 });
|
||||
|
||||
await messagesPage.sendMessage(messageText);
|
||||
await expect(messagesPage.getMessageItemByText(messageText)).toBeVisible({ timeout: 20_000 });
|
||||
}
|
||||
|
||||
async function expectSavedRoomAndHistory(page: Page, roomName: string, messageText: string): Promise<void> {
|
||||
const roomButton = getSavedRoomButton(page, roomName);
|
||||
const messagesPage = new ChatMessagesPage(page);
|
||||
|
||||
await expect(roomButton).toBeVisible({ timeout: 20_000 });
|
||||
await roomButton.click();
|
||||
await expect(page).toHaveURL(/\/room\//, { timeout: 20_000 });
|
||||
await expect(messagesPage.getMessageItemByText(messageText)).toBeVisible({ timeout: 20_000 });
|
||||
}
|
||||
|
||||
async function expectBlankSlate(page: Page, hiddenRoomNames: string[]): Promise<void> {
|
||||
const searchPage = new ServerSearchPage(page);
|
||||
|
||||
await expect(page).toHaveURL(/\/search/, { timeout: 15_000 });
|
||||
await expect(searchPage.createServerButton).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
for (const roomName of hiddenRoomNames) {
|
||||
await expectSavedRoomHidden(page, roomName);
|
||||
}
|
||||
}
|
||||
|
||||
async function expectSavedRoomVisible(page: Page, roomName: string): Promise<void> {
|
||||
await expect(getSavedRoomButton(page, roomName)).toBeVisible({ timeout: 20_000 });
|
||||
}
|
||||
|
||||
async function expectSavedRoomHidden(page: Page, roomName: string): Promise<void> {
|
||||
await expect(getSavedRoomButton(page, roomName)).toHaveCount(0);
|
||||
}
|
||||
|
||||
function getSavedRoomButton(page: Page, roomName: string) {
|
||||
return page.locator(`button[title="${roomName}"]`).first();
|
||||
}
|
||||
|
||||
async function retryTransientNavigation<T>(navigate: () => Promise<T>, attempts = 4): Promise<T> {
|
||||
let lastError: unknown;
|
||||
|
||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||
try {
|
||||
return await navigate();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const isTransientNavigationError = message.includes('ERR_EMPTY_RESPONSE') || message.includes('ERR_CONNECTION_RESET');
|
||||
|
||||
if (!isTransientNavigationError || attempt === attempts) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError instanceof Error
|
||||
? lastError
|
||||
: new Error(`Navigation failed after ${attempts} attempts`);
|
||||
}
|
||||
|
||||
function uniqueName(prefix: string): string {
|
||||
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user