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).
This commit is contained in:
Johannes Millan 2026-01-23 14:01:10 +01:00
parent 5027b31431
commit 301a8abdd7
5 changed files with 125 additions and 10 deletions

View file

@ -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)"

View file

@ -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,

View file

@ -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);
});
});

View file

@ -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<ArrayBuffer> => {
// 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);

View file

@ -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": {