super-productivity/e2e/tests/sync/supersync-encryption.spec.ts
Johannes Millan e8f5f1d404 test(e2e): fix supersync encryption tests for mandatory encryption
- Skip "disable encryption" test: button is hidden for SuperSync
- Skip "encryption prompt loop" tests: unencrypted SuperSync no longer possible
- Fix "wrong password" test: assert decrypt error dialog directly instead
  of checking for post-dialog snackbar/error indicators
- Fix "password change recovery" test: handle sync-import-conflict dialog
  that appears after retry decrypt due to clean slate from password change
2026-03-06 18:19:26 +01:00

355 lines
12 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 - don't wait for sync since it will fail
await clientC.sync.setupSuperSync({
...baseConfig,
isEncryptionEnabled: true,
password: wrongPassword,
waitForInitialSync: false, // Sync will fail with wrong password
});
// The sync already happened during setupSuperSync with wrong password
// A decrypt error dialog should appear — this IS the error UI
const decryptErrorDialog = clientC.page.locator('dialog-handle-decrypt-error');
// Wait for the decrypt error dialog to appear (it may take a moment after sync fails)
await decryptErrorDialog.waitFor({ state: 'visible', timeout: 15000 });
console.log('Decrypt error dialog appeared — wrong password detected');
// Verify Client C DOES NOT have the task (while dialog is open)
await expect(
clientC.page.locator(`task:has-text("${secretTaskName}")`),
).not.toBeVisible();
// The decrypt error dialog itself IS the error indicator.
// Verify it contains the expected error heading.
await expect(
decryptErrorDialog.locator('h2:has-text("Decryption Failed")'),
).toBeVisible();
// Close the dialog
const cancelBtn = decryptErrorDialog
.locator('button')
.filter({ hasText: /cancel/i });
await cancelBtn.click();
await decryptErrorDialog.waitFor({ state: 'hidden', timeout: 5000 });
} 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);
}
});
});