From 301a8abdd79b77b7003490cb30a13f7014a09fd0 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Fri, 23 Jan 2026 14:01:10 +0100 Subject: [PATCH] fix(sync): improve Dropbox PKCE error handling and messaging Dropbox Sync Improvements: - Change PKCE alerts to thrown errors for better error handling - Add provider ID to sync auth error logs for debugging - Improve PKCE error message to guide users to HTTPS requirement - Add unit tests for PKCE code generation Testing Improvements: - Enhance wait-for-supersync.sh to check both health and test endpoints - Match E2E fixture behavior for more reliable test startup detection These changes improve error diagnostics and provide clearer guidance when WebCrypto APIs are unavailable (HTTP contexts). --- scripts/wait-for-supersync.sh | 57 ++++++++++++++-- src/app/imex/sync/sync-wrapper.service.ts | 2 +- .../dropbox/generate-pkce-codes.spec.ts | 68 +++++++++++++++++++ .../file-based/dropbox/generate-pkce-codes.ts | 6 +- src/assets/i18n/en.json | 2 +- 5 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 src/app/op-log/sync-providers/file-based/dropbox/generate-pkce-codes.spec.ts diff --git a/scripts/wait-for-supersync.sh b/scripts/wait-for-supersync.sh index 6a0ca73d71..b1030db6ed 100755 --- a/scripts/wait-for-supersync.sh +++ b/scripts/wait-for-supersync.sh @@ -1,20 +1,65 @@ #!/bin/bash # Wait for SuperSync server to be ready at http://localhost:1901 -# Retries for up to 90 seconds (accounts for PostgreSQL + SuperSync startup) +# Checks both health endpoint AND test mode endpoint to match fixture behavior echo "Waiting for SuperSync server on http://localhost:1901..." +echo "Checking both /health and /api/test/create-user endpoints..." MAX_WAIT=90 elapsed=0 -until curl -s http://localhost:1901/health > /dev/null 2>&1; do + +# Function to check if server is fully healthy (matches fixture logic) +check_server() { + # Check 1: Basic health endpoint + if ! curl -sf http://localhost:1901/health > /dev/null 2>&1; then + return 1 + fi + + # Check 2: Test mode endpoint (verify TEST_MODE is enabled) + # Create a dummy user to verify test endpoints are available + local test_email="health-check-$(date +%s)@test.local" + local response_code=$(curl -s -o /dev/null -w "%{http_code}" \ + -X POST http://localhost:1901/api/test/create-user \ + -H "Content-Type: application/json" \ + -d "{\"email\":\"$test_email\",\"password\":\"HealthCheck123!\"}" \ + 2>/dev/null) + + # Accept 201 (created), 409 (already exists), or 400 (validation error) + # Reject 404 (test mode disabled) or connection errors + if [ "$response_code" = "404" ] || [ -z "$response_code" ]; then + echo "WARNING: Health endpoint OK but test mode endpoint returned: $response_code" + return 1 + fi + + return 0 +} + +until check_server; do if [ $elapsed -ge $MAX_WAIT ]; then - echo "Timeout: SuperSync server did not start within ${MAX_WAIT}s" - echo "--- SuperSync logs ---" - docker compose -f docker-compose.yaml -f docker-compose.supersync.yaml logs supersync + echo "" + echo "ERROR: SuperSync server did not start within ${MAX_WAIT}s" + echo "" + echo "=== Diagnostics ===" + echo "1. Container status:" + docker ps -a | grep -E "(supersync|db|NAMES)" + echo "" + echo "2. Port mappings:" + docker ps | grep supersync | awk '{print $NF " " $(NF-1)}' + echo "" + echo "3. Database health:" + docker compose -f docker-compose.e2e.yaml -f docker-compose.yaml -f docker-compose.supersync.yaml exec -T db pg_isready -U supersync 2>&1 || echo "Database not ready" + echo "" + echo "4. SuperSync logs (last 50 lines):" + docker compose -f docker-compose.e2e.yaml -f docker-compose.yaml -f docker-compose.supersync.yaml logs --tail=50 supersync + echo "" exit 1 fi sleep 1 elapsed=$((elapsed + 1)) + # Show progress every 10 seconds + if [ $((elapsed % 10)) -eq 0 ]; then + echo "Still waiting... (${elapsed}s / ${MAX_WAIT}s)" + fi done -echo "SuperSync server is ready!" +echo "SuperSync server is ready! (Both health and test endpoints responding)" diff --git a/src/app/imex/sync/sync-wrapper.service.ts b/src/app/imex/sync/sync-wrapper.service.ts index ae23b53666..c67b0879c2 100644 --- a/src/app/imex/sync/sync-wrapper.service.ts +++ b/src/app/imex/sync/sync-wrapper.service.ts @@ -448,7 +448,7 @@ export class SyncWrapperService { } } } catch (error) { - SyncLog.err(error); + SyncLog.err(`Failed to configure auth for provider ${providerId}:`, error); this._snackService.open({ // TODO don't limit snack to dropbox msg: T.F.DROPBOX.S.UNABLE_TO_GENERATE_PKCE_CHALLENGE, diff --git a/src/app/op-log/sync-providers/file-based/dropbox/generate-pkce-codes.spec.ts b/src/app/op-log/sync-providers/file-based/dropbox/generate-pkce-codes.spec.ts new file mode 100644 index 0000000000..7507dd711d --- /dev/null +++ b/src/app/op-log/sync-providers/file-based/dropbox/generate-pkce-codes.spec.ts @@ -0,0 +1,68 @@ +import { generatePKCECodes } from './generate-pkce-codes'; + +describe('generatePKCECodes', () => { + let originalCrypto: Crypto; + + beforeEach(() => { + originalCrypto = window.crypto; + }); + + afterEach(() => { + // Restore original crypto + Object.defineProperty(window, 'crypto', { + value: originalCrypto, + writable: true, + configurable: true, + }); + }); + + it('should throw error when crypto.getRandomValues is unavailable', async () => { + // Mock crypto without getRandomValues + Object.defineProperty(window, 'crypto', { + value: {} as Crypto, + writable: true, + configurable: true, + }); + + await expectAsync(generatePKCECodes(128)).toBeRejectedWithError( + /WebCrypto API.*getRandomValues/i, + ); + }); + + it('should throw error when crypto.subtle is unavailable', async () => { + // Mock crypto with getRandomValues but without subtle + Object.defineProperty(window, 'crypto', { + value: { + getRandomValues: (array: Uint32Array) => { + // Fill with dummy values + for (let i = 0; i < array.length; i++) { + array[i] = Math.floor(Math.random() * 0xffffffff); + } + return array; + }, + subtle: undefined, + } as unknown as Crypto, + writable: true, + configurable: true, + }); + + await expectAsync(generatePKCECodes(128)).toBeRejectedWithError( + /WebCrypto API.*subtle/i, + ); + }); + + it('should successfully generate PKCE codes when WebCrypto is available', async () => { + // Use real WebCrypto API (already available in test environment) + const result = await generatePKCECodes(128); + + expect(result).toBeDefined(); + expect(result.codeVerifier).toBeDefined(); + expect(result.codeChallenge).toBeDefined(); + expect(typeof result.codeVerifier).toBe('string'); + expect(typeof result.codeChallenge).toBe('string'); + expect(result.codeVerifier.length).toBeGreaterThan(0); + expect(result.codeChallenge.length).toBeGreaterThan(0); + // Code challenge should be different from verifier (it's hashed) + expect(result.codeChallenge).not.toBe(result.codeVerifier); + }); +}); diff --git a/src/app/op-log/sync-providers/file-based/dropbox/generate-pkce-codes.ts b/src/app/op-log/sync-providers/file-based/dropbox/generate-pkce-codes.ts index 100dd6018b..7f80df171d 100644 --- a/src/app/op-log/sync-providers/file-based/dropbox/generate-pkce-codes.ts +++ b/src/app/op-log/sync-providers/file-based/dropbox/generate-pkce-codes.ts @@ -4,7 +4,7 @@ const generateRandomString = (length: number): string => { const array = new Uint32Array(length / 2); if (!window.crypto?.getRandomValues) { - alert( + throw new Error( 'WebCrypto API (getRandomValues) not supported in your browser. Please update to the latest version or use a different one', ); } @@ -21,7 +21,9 @@ const sha256 = (plain: string): Promise => { // NOTE: crypto.subtle is supposed to be undefined in insecure contexts // @see https://www.chromium.org/blink/webcrypto if (window.crypto?.subtle === undefined) { - alert('WebCrypto API (subtle.digest) is only supported in secure contexts.'); + throw new Error( + 'WebCrypto API (subtle.digest) is only supported in secure contexts.', + ); } return window.crypto.subtle.digest('SHA-256', data); diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index a48375d8e3..645ef61128 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -159,7 +159,7 @@ "S": { "OFFLINE": "Dropbox: Unable to sync, because offline", "SYNC_ERROR": "Dropbox: Error while syncing", - "UNABLE_TO_GENERATE_PKCE_CHALLENGE": "Dropbox: Unable to generate PKCE challenge." + "UNABLE_TO_GENERATE_PKCE_CHALLENGE": "Dropbox sync requires a secure connection (HTTPS). Please ensure you're using the app over HTTPS or try the stable version." } }, "D_RATE": {