mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-25 08:53:50 +00:00
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
365 lines
13 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|