test(supersync): add comprehensive encryption enable/disable tests

Add E2E tests for scenarios C and D:
C) Password removal (encryption disabled) - verifies other clients adapt
D) Password addition (encryption enabled) - verifies other clients require password

Tests cover:
- Disabling encryption triggers clean slate and clients adapt automatically
- Enabling encryption triggers clean slate and clients detect encrypted data
- Multiple encryption state changes work correctly
- Concurrent changes are overwritten by encryption state change (expected behavior)

Also adds helper methods to SuperSyncPage:
- openEncryptionDialog() - Opens sync settings dialog
- enableEncryption(password) - Enables encryption via UI
- disableEncryption() - Disables encryption via UI
This commit is contained in:
Johannes Millan 2026-01-24 16:31:55 +01:00
parent e924b068ac
commit 1c41228a7f
2 changed files with 610 additions and 0 deletions

View file

@ -672,4 +672,105 @@ export class SuperSyncPage extends BasePage {
await dialogContainer.waitFor({ state: 'detached', timeout: 5000 });
}
}
/**
* Open the encryption/sync settings dialog.
* This opens the main sync configuration dialog where encryption can be managed.
*/
async openEncryptionDialog(): Promise<void> {
// Open sync settings via right-click on sync button
await this.syncBtn.click({ button: 'right' });
await this.providerSelect.waitFor({ state: 'visible', timeout: 10000 });
// Expand "Advanced settings" collapsible to access encryption fields
const advancedCollapsible = this.page.locator(
'.collapsible-header:has-text("Advanced")',
);
await advancedCollapsible.waitFor({ state: 'visible', timeout: 5000 });
// Check if already expanded
const isExpanded = await this.encryptionCheckbox.isVisible().catch(() => false);
if (!isExpanded) {
await advancedCollapsible.click();
await this.encryptionCheckbox.waitFor({ state: 'visible', timeout: 3000 });
}
}
/**
* Enable encryption by reconfiguring the SuperSync provider.
* This will trigger a clean slate operation (server wipe + fresh encrypted upload).
*
* Prerequisites: SuperSync must already be configured (call setupSuperSync first)
*
* @param password - The encryption password to use
*/
async enableEncryption(password: string): Promise<void> {
// Enable encryption
const checkboxLabel = this.page.locator('.e2e-isEncryptionEnabled label');
await this.encryptionCheckbox.waitFor({ state: 'attached', timeout: 5000 });
await this.page.waitForTimeout(200);
// Check if already enabled
const isChecked = await this.encryptionCheckbox.isChecked();
if (!isChecked) {
await checkboxLabel.click();
await this.page.waitForTimeout(100);
await expect(this.encryptionCheckbox).toBeChecked({ timeout: 3000 });
}
// Fill in password
await this.encryptionPasswordInput.waitFor({ state: 'visible' });
await this.encryptionPasswordInput.fill(password);
await this.encryptionPasswordInput.blur();
await this.page.waitForTimeout(200);
// Save the configuration
await this.saveBtn.waitFor({ state: 'visible', timeout: 5000 });
await this.page.waitForTimeout(100);
await Promise.race([
this.saveBtn.click({ timeout: 5000 }),
this.page
.locator('mat-dialog-container')
.waitFor({ state: 'detached', timeout: 5000 }),
]);
// Wait for clean slate operation to complete
await this.page.waitForTimeout(1000);
}
/**
* Disable encryption by reconfiguring the SuperSync provider.
* This will trigger a clean slate operation (server wipe + fresh unencrypted upload).
*
* Prerequisites: SuperSync must already be configured with encryption enabled
*/
async disableEncryption(): Promise<void> {
// Uncheck encryption
const checkboxLabel = this.page.locator('.e2e-isEncryptionEnabled label');
await this.encryptionCheckbox.waitFor({ state: 'attached', timeout: 5000 });
await this.page.waitForTimeout(200);
// Check if already disabled
const isChecked = await this.encryptionCheckbox.isChecked();
if (isChecked) {
await checkboxLabel.click();
await this.page.waitForTimeout(100);
await expect(this.encryptionCheckbox).not.toBeChecked({ timeout: 3000 });
}
// Save the configuration
await this.saveBtn.waitFor({ state: 'visible', timeout: 5000 });
await this.page.waitForTimeout(100);
await Promise.race([
this.saveBtn.click({ timeout: 5000 }),
this.page
.locator('mat-dialog-container')
.waitFor({ state: 'detached', timeout: 5000 }),
]);
// Wait for clean slate operation to complete
await this.page.waitForTimeout(1000);
}
}

View file

@ -0,0 +1,509 @@
import { test, expect } from '../../fixtures/supersync.fixture';
import {
createTestUser,
getSuperSyncConfig,
createSimulatedClient,
closeClient,
waitForTask,
type SimulatedE2EClient,
} from '../../utils/supersync-helpers';
/**
* SuperSync Encryption Enable/Disable E2E Tests
*
* These tests verify that enabling/disabling encryption on existing synced data
* properly handles the clean slate mechanism:
* - Server data is wiped (encrypted ops can't mix with unencrypted)
* - A fresh snapshot is uploaded with correct encryption settings
* - Other clients adapt to the new encryption state
*
* Scenarios covered:
* C) Encryption disabled (password removed) - other clients recognize and disable locally
* D) Encryption enabled (password added) - other clients recognize and prompt for password
*
* Run with: npm run e2e:supersync:file e2e/tests/sync/supersync-encryption-enable-disable.spec.ts
*/
test.describe('@supersync @encryption Encryption Enable/Disable', () => {
/**
* Scenario C: Remove encryption password (disable encryption)
*
* Setup:
* - Client A has encryption enabled with synced encrypted data
* - Client B also has encryption enabled and synced
*
* Actions:
* 1. Client A disables encryption (removes password)
* 2. Clean slate is triggered (server wiped, fresh unencrypted snapshot uploaded)
* 3. Client B syncs and should receive unencrypted data
* 4. Client B should automatically disable encryption settings locally
*
* Verify:
* - Client A's data is now unencrypted on server
* - Client B successfully syncs WITHOUT encryption
* - Both clients have same data
* - No encryption errors occur
*/
test('Disabling encryption triggers clean slate and other clients adapt', async ({
browser,
baseURL,
testRunId,
}) => {
const uniqueId = Date.now();
let clientA: SimulatedE2EClient | null = null;
let clientB: SimulatedE2EClient | null = null;
try {
const user = await createTestUser(testRunId);
const baseConfig = getSuperSyncConfig(user);
const encryptionPassword = `pass-${testRunId}`;
// ============ PHASE 1: Setup both clients with encryption ============
console.log('[DisableEncryption] Phase 1: Setting up clients with encryption');
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
await clientA.sync.setupSuperSync({
...baseConfig,
isEncryptionEnabled: true,
password: encryptionPassword,
});
// Create and sync encrypted task
const encryptedTask = `EncryptedTask-${uniqueId}`;
await clientA.workView.addTask(encryptedTask);
await clientA.sync.syncAndWait();
console.log(`[DisableEncryption] Client A created: ${encryptedTask}`);
// Setup Client B with same encryption
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
await clientB.sync.setupSuperSync({
...baseConfig,
isEncryptionEnabled: true,
password: encryptionPassword,
});
await clientB.sync.syncAndWait();
// Verify both clients have the encrypted task
await waitForTask(clientA.page, encryptedTask);
await waitForTask(clientB.page, encryptedTask);
console.log('[DisableEncryption] Both clients synced with encryption');
// ============ PHASE 2: Client A disables encryption ============
console.log('[DisableEncryption] Phase 2: Client A disabling encryption');
// Open encryption settings dialog
await clientA.sync.openEncryptionDialog();
// Disable encryption (remove password)
await clientA.sync.disableEncryption();
console.log('[DisableEncryption] Client A disabled encryption');
// Verify clean slate was triggered by checking task still exists
await waitForTask(clientA.page, encryptedTask);
console.log('[DisableEncryption] Client A data preserved after clean slate');
// ============ PHASE 3: Client A creates new unencrypted task ============
const unencryptedTask = `UnencryptedTask-${uniqueId}`;
await clientA.workView.addTask(unencryptedTask);
await clientA.sync.syncAndWait();
console.log(`[DisableEncryption] Client A created unencrypted: ${unencryptedTask}`);
// ============ PHASE 4: Client B syncs and should receive unencrypted data ============
console.log(
'[DisableEncryption] Phase 4: Client B syncing after encryption disabled',
);
// Client B should sync successfully without encryption errors
await clientB.sync.syncAndWait();
console.log('[DisableEncryption] Client B synced successfully');
// Client B should now have both tasks (clean slate replaced all data)
await waitForTask(clientB.page, encryptedTask);
await waitForTask(clientB.page, unencryptedTask);
// ============ PHASE 5: Verify encryption is disabled for both clients ============
console.log('[DisableEncryption] Phase 5: Verifying encryption is disabled');
// Verify no sync errors on either client
const hasErrorA = await clientA.sync.hasSyncError();
const hasErrorB = await clientB.sync.hasSyncError();
expect(hasErrorA).toBe(false);
expect(hasErrorB).toBe(false);
// Verify both clients can create and sync new tasks without encryption
const finalTask = `FinalTask-${uniqueId}`;
await clientB.workView.addTask(finalTask);
await clientB.sync.syncAndWait();
await clientA.sync.syncAndWait();
await waitForTask(clientA.page, finalTask);
console.log(
'[DisableEncryption] ✓ Encryption disabled successfully on both clients!',
);
} finally {
if (clientA) await closeClient(clientA);
if (clientB) await closeClient(clientB);
}
});
/**
* Scenario D: Add encryption password (enable encryption)
*
* Setup:
* - Client A has unencrypted sync active with data
* - Client B also has unencrypted sync
*
* Actions:
* 1. Client A enables encryption (adds password)
* 2. Clean slate is triggered (server wiped, fresh encrypted snapshot uploaded)
* 3. Client B syncs and receives encrypted data
* 4. Client B should detect encryption and prompt for password
* 5. Client B provides correct password and syncs successfully
*
* Verify:
* - Client A's data is now encrypted on server
* - Client B cannot sync without password
* - Client B can sync after providing correct password
* - Both clients have same encrypted data
*/
test('Enabling encryption triggers clean slate and other clients require password', async ({
browser,
baseURL,
testRunId,
}) => {
const uniqueId = Date.now();
let clientA: SimulatedE2EClient | null = null;
let clientB: SimulatedE2EClient | null = null;
try {
const user = await createTestUser(testRunId);
const baseConfig = getSuperSyncConfig(user);
const encryptionPassword = `newpass-${testRunId}`;
// ============ PHASE 1: Setup both clients WITHOUT encryption ============
console.log('[EnableEncryption] Phase 1: Setting up clients without encryption');
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
await clientA.sync.setupSuperSync({
...baseConfig,
isEncryptionEnabled: false,
});
// Create and sync unencrypted task
const unencryptedTask = `UnencryptedTask-${uniqueId}`;
await clientA.workView.addTask(unencryptedTask);
await clientA.sync.syncAndWait();
console.log(`[EnableEncryption] Client A created: ${unencryptedTask}`);
// Setup Client B without encryption
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
await clientB.sync.setupSuperSync({
...baseConfig,
isEncryptionEnabled: false,
});
await clientB.sync.syncAndWait();
// Verify both clients have the unencrypted task
await waitForTask(clientA.page, unencryptedTask);
await waitForTask(clientB.page, unencryptedTask);
console.log('[EnableEncryption] Both clients synced without encryption');
// ============ PHASE 2: Client A enables encryption ============
console.log('[EnableEncryption] Phase 2: Client A enabling encryption');
// Open encryption settings dialog
await clientA.sync.openEncryptionDialog();
// Enable encryption with new password
await clientA.sync.enableEncryption(encryptionPassword);
console.log('[EnableEncryption] Client A enabled encryption');
// Verify clean slate was triggered by checking task still exists
await waitForTask(clientA.page, unencryptedTask);
console.log('[EnableEncryption] Client A data preserved after clean slate');
// ============ PHASE 3: Client A creates new encrypted task ============
const encryptedTask = `EncryptedTask-${uniqueId}`;
await clientA.workView.addTask(encryptedTask);
await clientA.sync.syncAndWait();
console.log(`[EnableEncryption] Client A created encrypted: ${encryptedTask}`);
// ============ PHASE 4: Client B syncs WITHOUT password (should fail) ============
console.log('[EnableEncryption] Phase 4: Client B syncing without password');
// Client B tries to sync - should detect encryption and fail
try {
await clientB.sync.syncAndWait();
// If we get here, the test should fail because sync should have failed
throw new Error('Client B should not sync without encryption password');
} catch (error) {
// Expected: sync should fail because data is encrypted
console.log(
'[EnableEncryption] Client B sync failed as expected (needs password)',
);
console.log(`[EnableEncryption] Error: ${error}`);
}
// Verify Client B has sync error related to encryption
const hasError = await clientB.sync.hasSyncError();
expect(hasError).toBe(true);
console.log('[EnableEncryption] ✓ Client B detected encryption requirement');
// ============ PHASE 5: Client B provides password and syncs successfully ============
console.log('[EnableEncryption] Phase 5: Client B providing password');
// Reconfigure Client B with encryption password
await clientB.sync.setupSuperSync({
...baseConfig,
isEncryptionEnabled: true,
password: encryptionPassword,
});
console.log('[EnableEncryption] Client B configured with encryption password');
// Now sync should work
await clientB.sync.syncAndWait();
console.log('[EnableEncryption] Client B synced successfully with password');
// Client B should now have both tasks
await waitForTask(clientB.page, unencryptedTask);
await waitForTask(clientB.page, encryptedTask);
// ============ PHASE 6: Verify bidirectional encrypted sync works ============
console.log('[EnableEncryption] Phase 6: Verifying bidirectional encrypted sync');
// Client B creates a task
const finalTask = `FinalTask-${uniqueId}`;
await clientB.workView.addTask(finalTask);
await clientB.sync.syncAndWait();
// Client A syncs and should receive it
await clientA.sync.syncAndWait();
await waitForTask(clientA.page, finalTask);
// Verify no sync errors
const hasErrorA = await clientA.sync.hasSyncError();
const hasErrorB = await clientB.sync.hasSyncError();
expect(hasErrorA).toBe(false);
expect(hasErrorB).toBe(false);
console.log(
'[EnableEncryption] ✓ Encryption enabled successfully on both clients!',
);
} finally {
if (clientA) await closeClient(clientA);
if (clientB) await closeClient(clientB);
}
});
/**
* Scenario: Multiple encryption state changes work correctly
*
* Tests that:
* - Client can enable disable enable encryption multiple times
* - Each state change triggers clean slate
* - Other clients adapt to each change
*/
test('Multiple encryption state changes work correctly', async ({
browser,
baseURL,
testRunId,
}) => {
const uniqueId = Date.now();
let clientA: SimulatedE2EClient | null = null;
let clientB: SimulatedE2EClient | null = null;
try {
const user = await createTestUser(testRunId);
const baseConfig = getSuperSyncConfig(user);
const password1 = `pass1-${testRunId}`;
const password2 = `pass2-${testRunId}`;
// ============ PHASE 1: Start unencrypted ============
console.log('[MultipleChanges] Phase 1: Starting unencrypted');
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
await clientA.sync.setupSuperSync({
...baseConfig,
isEncryptionEnabled: false,
});
const task1 = `Task1-${uniqueId}`;
await clientA.workView.addTask(task1);
await clientA.sync.syncAndWait();
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
await clientB.sync.setupSuperSync({
...baseConfig,
isEncryptionEnabled: false,
});
await clientB.sync.syncAndWait();
await waitForTask(clientB.page, task1);
// ============ PHASE 2: Enable encryption ============
console.log('[MultipleChanges] Phase 2: Enabling encryption');
await clientA.sync.openEncryptionDialog();
await clientA.sync.enableEncryption(password1);
const task2 = `Task2-${uniqueId}`;
await clientA.workView.addTask(task2);
await clientA.sync.syncAndWait();
// Client B should fail without password
try {
await clientB.sync.syncAndWait();
throw new Error('Should have failed');
} catch (error) {
console.log('[MultipleChanges] Client B failed as expected');
}
// Client B gets password
await clientB.sync.setupSuperSync({
...baseConfig,
isEncryptionEnabled: true,
password: password1,
});
await clientB.sync.syncAndWait();
await waitForTask(clientB.page, task2);
// ============ PHASE 3: Disable encryption ============
console.log('[MultipleChanges] Phase 3: Disabling encryption');
await clientA.sync.openEncryptionDialog();
await clientA.sync.disableEncryption();
const task3 = `Task3-${uniqueId}`;
await clientA.workView.addTask(task3);
await clientA.sync.syncAndWait();
// Client B should sync successfully (unencrypted now)
await clientB.sync.syncAndWait();
await waitForTask(clientB.page, task3);
// ============ PHASE 4: Re-enable with different password ============
console.log('[MultipleChanges] Phase 4: Re-enabling with different password');
await clientA.sync.openEncryptionDialog();
await clientA.sync.enableEncryption(password2);
const task4 = `Task4-${uniqueId}`;
await clientA.workView.addTask(task4);
await clientA.sync.syncAndWait();
// Client B needs NEW password
await clientB.sync.setupSuperSync({
...baseConfig,
isEncryptionEnabled: true,
password: password2,
});
await clientB.sync.syncAndWait();
await waitForTask(clientB.page, task4);
// ============ PHASE 5: Verify final state ============
console.log('[MultipleChanges] Phase 5: Verifying final state');
// All tasks should be present on both clients
await waitForTask(clientA.page, task1);
await waitForTask(clientA.page, task2);
await waitForTask(clientA.page, task3);
await waitForTask(clientA.page, task4);
await waitForTask(clientB.page, task1);
await waitForTask(clientB.page, task2);
await waitForTask(clientB.page, task3);
await waitForTask(clientB.page, task4);
const hasErrorA = await clientA.sync.hasSyncError();
const hasErrorB = await clientB.sync.hasSyncError();
expect(hasErrorA).toBe(false);
expect(hasErrorB).toBe(false);
console.log('[MultipleChanges] ✓ Multiple encryption state changes work!');
} finally {
if (clientA) await closeClient(clientA);
if (clientB) await closeClient(clientB);
}
});
/**
* Scenario: Concurrent changes during encryption state change
*
* Tests that:
* - If Client B makes changes while Client A is changing encryption state
* - Client B's changes are overwritten by clean slate (expected behavior)
* - This is correct: clean slate is an explicit user action to reset sync
*/
test('Concurrent changes are overwritten by encryption state change', async ({
browser,
baseURL,
testRunId,
}) => {
const uniqueId = Date.now();
let clientA: SimulatedE2EClient | null = null;
let clientB: SimulatedE2EClient | null = null;
try {
const user = await createTestUser(testRunId);
const baseConfig = getSuperSyncConfig(user);
const encryptionPassword = `pass-${testRunId}`;
// Setup both clients unencrypted
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
await clientA.sync.setupSuperSync({
...baseConfig,
isEncryptionEnabled: false,
});
const sharedTask = `SharedTask-${uniqueId}`;
await clientA.workView.addTask(sharedTask);
await clientA.sync.syncAndWait();
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
await clientB.sync.setupSuperSync({
...baseConfig,
isEncryptionEnabled: false,
});
await clientB.sync.syncAndWait();
await waitForTask(clientB.page, sharedTask);
// Client B creates a task (while A will enable encryption)
const concurrentTask = `ConcurrentTask-${uniqueId}`;
await clientB.workView.addTask(concurrentTask);
// Note: B does NOT sync yet
// Client A enables encryption (triggers clean slate)
await clientA.sync.openEncryptionDialog();
await clientA.sync.enableEncryption(encryptionPassword);
const taskAfterEncryption = `AfterEncryption-${uniqueId}`;
await clientA.workView.addTask(taskAfterEncryption);
await clientA.sync.syncAndWait();
// Now Client B syncs
await clientB.sync.setupSuperSync({
...baseConfig,
isEncryptionEnabled: true,
password: encryptionPassword,
});
await clientB.sync.syncAndWait();
// Client B should have tasks from clean slate (A's state)
await waitForTask(clientB.page, sharedTask);
await waitForTask(clientB.page, taskAfterEncryption);
// CRITICAL: Client B's concurrent task should be GONE (overwritten by clean slate)
const concurrentTaskLocator = clientB.page.locator(
`task:has-text("${concurrentTask}")`,
);
await expect(concurrentTaskLocator).not.toBeVisible({ timeout: 5000 });
console.log(
'[Concurrent] ✓ Concurrent changes correctly overwritten by clean slate!',
);
} finally {
if (clientA) await closeClient(clientA);
if (clientB) await closeClient(clientB);
}
});
});