mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 08:56:41 +00:00
- 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
355 lines
12 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|