super-productivity/e2e/tests/sync/supersync-encryption.spec.ts
Johannes Millan 9431f23a1a fix(e2e): stabilize flaky supersync tests with better waiting strategies
Replace fixed timeouts with condition-based waiting to improve test
reliability under load:
- USE_REMOTE test: wait for sync completion instead of fixed 2s delay
- Encryption test: add retry logic for error state checking
- Undo delete test: use waitForTask polling instead of fixed timeout
- Task ordering test: add UI settling time after sync operations
2026-01-13 18:26:45 +01:00

365 lines
13 KiB
TypeScript

import { test, expect } from '../../fixtures/supersync.fixture';
import {
createTestUser,
getSuperSyncConfig,
createSimulatedClient,
closeClient,
waitForTask,
type SimulatedE2EClient,
} from '../../utils/supersync-helpers';
/**
* SuperSync Encryption E2E Tests
*
* Verifies End-to-End Encryption (E2EE) logic:
* - Setting up encryption
* - Secure syncing between clients with same password
* - Denial of access for clients with wrong password
* - Multiple operations with encryption
* - Bidirectional sync with encryption
* - Update and delete operations
*
* Run with E2E_VERBOSE=1 to see browser console logs for debugging.
*/
test.describe('@supersync SuperSync Encryption', () => {
// Server health check is handled automatically by the supersync fixture
test('Encrypted data syncs correctly with valid password', async ({
browser,
baseURL,
testRunId,
serverHealthy,
}) => {
void serverHealthy; // Ensure fixture is evaluated for server health check
let clientA: SimulatedE2EClient | null = null;
let clientB: SimulatedE2EClient | null = null;
try {
const user = await createTestUser(testRunId);
const baseConfig = getSuperSyncConfig(user);
const encryptionPassword = `pass-${testRunId}`;
const syncConfig = {
...baseConfig,
isEncryptionEnabled: true,
password: encryptionPassword,
};
// --- Client A: Encrypt & Upload ---
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
await clientA.sync.setupSuperSync(syncConfig);
const secretTaskName = `SecretTask-${testRunId}`;
await clientA.workView.addTask(secretTaskName);
// Sync A (Encrypts and uploads)
await clientA.sync.syncAndWait();
// --- Client B: Download & Decrypt ---
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
// Use SAME config (same password)
await clientB.sync.setupSuperSync(syncConfig);
// Sync B (Downloads and decrypts)
await clientB.sync.syncAndWait();
// Verify B has the task
await waitForTask(clientB.page, secretTaskName);
await expect(
clientB.page.locator(`task:has-text("${secretTaskName}")`),
).toBeVisible();
} finally {
if (clientA) await closeClient(clientA);
if (clientB) await closeClient(clientB);
}
});
test('Encrypted data fails to sync with wrong password', async ({
browser,
baseURL,
testRunId,
serverHealthy,
}) => {
void serverHealthy; // Ensure fixture is evaluated for server health check
let clientA: SimulatedE2EClient | null = null;
let clientC: SimulatedE2EClient | null = null;
try {
const user = await createTestUser(testRunId);
const baseConfig = getSuperSyncConfig(user);
const correctPassword = `correct-${testRunId}`;
const wrongPassword = `wrong-${testRunId}`;
// --- Client A: Encrypt & Upload (Correct Password) ---
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
await clientA.sync.setupSuperSync({
...baseConfig,
isEncryptionEnabled: true,
password: correctPassword,
});
const secretTaskName = `SecretTask-${testRunId}`;
await clientA.workView.addTask(secretTaskName);
await clientA.sync.syncAndWait();
// --- Client C: Attempt Download (Wrong Password) ---
clientC = await createSimulatedClient(browser, baseURL!, 'C', testRunId);
// Setup with WRONG password
await clientC.sync.setupSuperSync({
...baseConfig,
isEncryptionEnabled: true,
password: wrongPassword,
});
// Try to sync - expectation is that it completes (download happens) but processing fails
// The SyncPage helper might throw if it detects an error icon, or we check for error manually
// We expect the sync to technically "fail" or show an error state because decryption failed
try {
await clientC.sync.triggerSync();
// It might not throw immediately if waitForSyncComplete handles the error state gracefully or we catch it
await clientC.sync.waitForSyncComplete();
} catch (e) {
// Expected error or timeout due to failure
console.log('Sync failed as expected:', e);
}
// Verify Client C DOES NOT have the task
await expect(
clientC.page.locator(`task:has-text("${secretTaskName}")`),
).not.toBeVisible();
// Verify Error UI - always assert error occurred
// Silent decryption failure is unacceptable - wrong password MUST produce visible error
// Use retry logic since error indicators may take a moment to appear under load
await expect(async () => {
const hasError = await clientC.sync.hasSyncError();
// App uses snack-custom component, not simple-snack-bar
const snackbar = clientC.page.locator(
'snack-custom:has-text("decrypt"), ' +
'snack-custom:has-text("password"), ' +
'.mat-mdc-snack-bar-container:has-text("decrypt"), ' +
'.mat-mdc-snack-bar-container:has-text("password")',
);
const snackbarVisible = await snackbar
.first()
.isVisible()
.catch(() => false);
expect(hasError || snackbarVisible).toBe(true);
}).toPass({ timeout: 10000, intervals: [500, 1000, 2000] });
} finally {
if (clientA) await closeClient(clientA);
if (clientC) await closeClient(clientC);
}
});
test('Multiple tasks sync correctly with encryption', async ({
browser,
baseURL,
testRunId,
serverHealthy,
}) => {
void serverHealthy; // Ensure fixture is evaluated for server health check
let clientA: SimulatedE2EClient | null = null;
let clientB: SimulatedE2EClient | null = null;
try {
const user = await createTestUser(testRunId);
const baseConfig = getSuperSyncConfig(user);
const encryptionPassword = `multi-${testRunId}`;
const syncConfig = {
...baseConfig,
isEncryptionEnabled: true,
password: encryptionPassword,
};
// --- Client A: Create multiple tasks ---
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
await clientA.sync.setupSuperSync(syncConfig);
const task1 = `Task1-${testRunId}`;
const task2 = `Task2-${testRunId}`;
const task3 = `Task3-${testRunId}`;
await clientA.workView.addTask(task1);
await clientA.page.waitForTimeout(100);
await clientA.workView.addTask(task2);
await clientA.page.waitForTimeout(100);
await clientA.workView.addTask(task3);
// Sync all tasks
await clientA.sync.syncAndWait();
// --- Client B: Verify all tasks arrive ---
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
await clientB.sync.setupSuperSync(syncConfig);
await clientB.sync.syncAndWait();
// Verify all 3 tasks exist
await waitForTask(clientB.page, task1);
await waitForTask(clientB.page, task2);
await waitForTask(clientB.page, task3);
await expect(clientB.page.locator(`task:has-text("${task1}")`)).toBeVisible();
await expect(clientB.page.locator(`task:has-text("${task2}")`)).toBeVisible();
await expect(clientB.page.locator(`task:has-text("${task3}")`)).toBeVisible();
} finally {
if (clientA) await closeClient(clientA);
if (clientB) await closeClient(clientB);
}
});
test('Bidirectional sync works with encryption', async ({
browser,
baseURL,
testRunId,
serverHealthy,
}) => {
void serverHealthy; // Ensure fixture is evaluated for server health check
let clientA: SimulatedE2EClient | null = null;
let clientB: SimulatedE2EClient | null = null;
try {
const user = await createTestUser(testRunId);
const baseConfig = getSuperSyncConfig(user);
const encryptionPassword = `bidi-${testRunId}`;
const syncConfig = {
...baseConfig,
isEncryptionEnabled: true,
password: encryptionPassword,
};
// --- Setup both clients ---
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
await clientA.sync.setupSuperSync(syncConfig);
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
await clientB.sync.setupSuperSync(syncConfig);
// --- Client A creates a task ---
const taskFromA = `FromA-${testRunId}`;
await clientA.workView.addTask(taskFromA);
await clientA.sync.syncAndWait();
// --- Client B syncs and creates a task ---
await clientB.sync.syncAndWait();
await waitForTask(clientB.page, taskFromA);
const taskFromB = `FromB-${testRunId}`;
await clientB.workView.addTask(taskFromB);
await clientB.sync.syncAndWait();
// --- Client A syncs again and should have both tasks ---
await clientA.sync.syncAndWait();
await waitForTask(clientA.page, taskFromB);
// Verify both clients have both tasks
await expect(clientA.page.locator(`task:has-text("${taskFromA}")`)).toBeVisible();
await expect(clientA.page.locator(`task:has-text("${taskFromB}")`)).toBeVisible();
await expect(clientB.page.locator(`task:has-text("${taskFromA}")`)).toBeVisible();
await expect(clientB.page.locator(`task:has-text("${taskFromB}")`)).toBeVisible();
} finally {
if (clientA) await closeClient(clientA);
if (clientB) await closeClient(clientB);
}
});
test('Task update syncs correctly with encryption', async ({
browser,
baseURL,
testRunId,
serverHealthy,
}) => {
void serverHealthy; // Ensure fixture is evaluated for server health check
let clientA: SimulatedE2EClient | null = null;
let clientB: SimulatedE2EClient | null = null;
try {
const user = await createTestUser(testRunId);
const baseConfig = getSuperSyncConfig(user);
const encryptionPassword = `update-${testRunId}`;
const syncConfig = {
...baseConfig,
isEncryptionEnabled: true,
password: encryptionPassword,
};
// --- Client A: Create a task ---
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
await clientA.sync.setupSuperSync(syncConfig);
const taskName = `UpdatableTask-${testRunId}`;
await clientA.workView.addTask(taskName);
await clientA.page.waitForTimeout(300);
await clientA.sync.syncAndWait();
// --- Client B: Sync and verify task exists ---
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
await clientB.sync.setupSuperSync(syncConfig);
await clientB.sync.syncAndWait();
await waitForTask(clientB.page, taskName);
await expect(clientB.page.locator(`task:has-text("${taskName}")`)).toBeVisible();
// --- Client B: Create another task ---
const task2Name = `UpdatedByB-${testRunId}`;
await clientB.workView.addTask(task2Name);
await clientB.page.waitForTimeout(300);
await clientB.sync.syncAndWait();
// --- Client A: Sync and verify both tasks exist ---
await clientA.sync.syncAndWait();
await waitForTask(clientA.page, task2Name);
await expect(clientA.page.locator(`task:has-text("${taskName}")`)).toBeVisible();
await expect(clientA.page.locator(`task:has-text("${task2Name}")`)).toBeVisible();
} finally {
if (clientA) await closeClient(clientA);
if (clientB) await closeClient(clientB);
}
});
test('Long encryption password works correctly', async ({
browser,
baseURL,
testRunId,
serverHealthy,
}) => {
void serverHealthy; // Ensure fixture is evaluated for server health check
let clientA: SimulatedE2EClient | null = null;
let clientB: SimulatedE2EClient | null = null;
try {
const user = await createTestUser(testRunId);
const baseConfig = getSuperSyncConfig(user);
// Use a very long password with special characters
const longPassword = `This-Is-A-Very-Long-Password-With-Special-Chars!@#$%^&*()-${testRunId}`;
const syncConfig = {
...baseConfig,
isEncryptionEnabled: true,
password: longPassword,
};
// --- Client A: Create task with long password ---
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
await clientA.sync.setupSuperSync(syncConfig);
const taskName = `LongPassTask-${testRunId}`;
await clientA.workView.addTask(taskName);
await clientA.sync.syncAndWait();
// --- Client B: Sync with same long password ---
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
await clientB.sync.setupSuperSync(syncConfig);
await clientB.sync.syncAndWait();
// Verify task synced correctly
await waitForTask(clientB.page, taskName);
await expect(clientB.page.locator(`task:has-text("${taskName}")`)).toBeVisible();
} finally {
if (clientA) await closeClient(clientA);
if (clientB) await closeClient(clientB);
}
});
});