mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
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:
parent
e924b068ac
commit
1c41228a7f
2 changed files with 610 additions and 0 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
509
e2e/tests/sync/supersync-encryption-enable-disable.spec.ts
Normal file
509
e2e/tests/sync/supersync-encryption-enable-disable.spec.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue