Skip to content

Enable offline unlock in File Provider#456

Merged
tobihagemann merged 1 commit intodevelopfrom
feature/fix-offline-unlock-in-file-provider
Apr 24, 2026
Merged

Enable offline unlock in File Provider#456
tobihagemann merged 1 commit intodevelopfrom
feature/fix-offline-unlock-in-file-provider

Conversation

@tobihagemann
Copy link
Copy Markdown
Member

Completes the offline-operation story PR #446 started. #446 added the offline folder-enumeration fallback and the upload auto-retry, but the piece users hit first (the unlock screen in Files.app) still failed offline, so the fallback was unreachable in practice. This PR closes that gap: unlock now succeeds from the cached masterkey when connectivity drops, and the #446 fallback finally does what it was built to do.

What was in the way: the recover blocks in UnlockVaultViewModel.refreshVaultCache() and FileProviderCoordinator.showManualLogin() matched only CloudProviderError.noInternetConnection, but those errors go through LocalizedCloudProviderDecorator which converts them to LocalizedCloudProviderError.noInternetConnection. Offline unlock always fell into default: throw error and surfaced as "Server Unreachable" even with a fully cached masterkey. The latent type-mismatch predates #446 (commit a4ce8c33e, PR #161).

Both recover blocks now use error.isTransientConnectivityError, the same predicate UploadTaskExecutor.handleUploadError uses, so the full connectivity-error surface (including raw NSURLError variants) is covered consistently. CloudProviderError.itemNotFound joins LocalizedCloudProviderError.itemNotFound in the swallow branch. FileProviderCoordinator's unauthorized rethrow stays untouched.

isNoInternetConnectionError and isTransientConnectivityError moved from CryptomatorFileProvider/Middleware/ErrorMapper.swift into a new public extension in CryptomatorCommonCore/ErrorExtensions.swift so FileProviderExtensionUI can reach them. Their tests moved to CryptomatorCommonCoreTests/ErrorExtensionsTests.swift; ErrorMapperTests.swift keeps its toPresentableError() coverage.

New CryptomatorTests/UnlockVaultViewModelTests.swift (plus a minimal VaultUnlockingMock) covers the recover block across the swallow/reject matrix. UnlockVaultViewModel.swift picks up a secondary target membership in CryptomatorTests so the test can reference the type directly, avoiding the appex-linkage puzzle a dedicated FileProviderExtensionUITests bundle would have.

To test:

  1. Build and run Cryptomator on a simulator
  2. Add a local File Provider Storage vault and unlock it once online
  3. Close the app, enable Airplane Mode
  4. In the Files app, navigate to the vault and unlock (password or biometric)
  5. Unlock succeeds and the previously-enumerated folder contents appear
  6. Tap a previously-cached file and verify it opens offline

@tobihagemann tobihagemann added this to the 3.1.0 milestone Apr 21, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

Walkthrough

This pull request refactors error-checking logic for network and connectivity errors by moving two computed properties (isNoInternetConnectionError and isTransientConnectivityError) from the file provider module to a shared CryptomatorCommonCore library. The changes include creating new error extension definitions in the common library with comprehensive test coverage, removing the duplicate definitions from the file provider module, adding necessary imports to dependent files, and updating error-handling logic in FileProviderCoordinator and UnlockVaultViewModel to use the relocated properties. Additionally, new test utilities and test cases for UnlockVaultViewModel are introduced, and project file references are updated to include the new sources.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Enable offline unlock in File Provider' accurately describes the main change: enabling vault unlock functionality when offline by using cached masterkeys in the File Provider context.
Description check ✅ Passed The description comprehensively explains the problem (type-mismatch preventing offline unlock), the solution (using error.isTransientConnectivityError predicate), and implementation details including file movements and test coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/fix-offline-unlock-in-file-provider

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
CryptomatorCommon/Tests/CryptomatorCommonCoreTests/ErrorExtensionsTests.swift (1)

37-40: Coverage gap: LocalizedCloudProviderError.noInternetConnection not covered for isTransientConnectivityError.

The umbrella test asserts transitivity for CloudProviderError.noInternetConnection and NSFileProviderError(.serverUnreachable), but skips the LocalizedCloudProviderError.noInternetConnection branch — which is the exact variant that motivated this PR (errors wrapped by LocalizedCloudProviderDecorator). Worth pinning so a future refactor of isNoInternetConnectionError cannot silently drop that branch without a test failing.

💚 Proposed addition
 func testIsTransientConnectivityErrorIncludesNoInternetConnectionErrors() {
 	XCTAssertTrue(CloudProviderError.noInternetConnection.isTransientConnectivityError)
+	XCTAssertTrue(LocalizedCloudProviderError.noInternetConnection.isTransientConnectivityError)
 	XCTAssertTrue(NSFileProviderError(.serverUnreachable).isTransientConnectivityError)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@CryptomatorCommon/Tests/CryptomatorCommonCoreTests/ErrorExtensionsTests.swift`
around lines 37 - 40, Add coverage for the LocalizedCloudProviderError wrapper
by asserting that
LocalizedCloudProviderError.noInternetConnection.isTransientConnectivityError
returns true; update the existing test function
testIsTransientConnectivityErrorIncludesNoInternetConnectionErrors (or add a
small new test) to include an XCTAssertTrue call against
LocalizedCloudProviderError.noInternetConnection so the
isTransientConnectivityError logic for the LocalizedCloudProviderDecorator
variant is exercised.
CryptomatorTests/UnlockVaultViewModelTests.swift (1)

140-170: Consider using a Sourcery-generated VaultAccountManagerMock instead of a hand-written stub.

Most other test doubles here (VaultCacheMock, VaultPasswordManagerMock, FileProviderConnectorMock, VaultUnlockingMock) come from the mock-generation pipeline, which keeps them in lockstep with the protocol. The hand-rolled VaultAccountManagerStub will silently go stale if VaultAccountManager gains/renames members. If a generated mock already exists (or the protocol is in a mockable target), prefer it; otherwise the stub is fine as-is.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CryptomatorTests/UnlockVaultViewModelTests.swift` around lines 140 - 170,
Replace the hand-written VaultAccountManagerStub with the Sourcery-generated
VaultAccountManagerMock to keep the test double in sync with the
VaultAccountManager protocol: locate the VaultAccountManagerStub class and swap
usages to VaultAccountManagerMock (or import/use the generated mock target) so
tests use the generated mock implementation instead of the manual
getAccount/throwing-method stubs; if a generated mock does not yet exist, mark
the protocol as mockable for the generator or add the protocol to the mock
target so a VaultAccountManagerMock is produced and then update tests to
instantiate and configure that mock.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@CryptomatorCommon/Tests/CryptomatorCommonCoreTests/ErrorExtensionsTests.swift`:
- Around line 37-40: Add coverage for the LocalizedCloudProviderError wrapper by
asserting that
LocalizedCloudProviderError.noInternetConnection.isTransientConnectivityError
returns true; update the existing test function
testIsTransientConnectivityErrorIncludesNoInternetConnectionErrors (or add a
small new test) to include an XCTAssertTrue call against
LocalizedCloudProviderError.noInternetConnection so the
isTransientConnectivityError logic for the LocalizedCloudProviderDecorator
variant is exercised.

In `@CryptomatorTests/UnlockVaultViewModelTests.swift`:
- Around line 140-170: Replace the hand-written VaultAccountManagerStub with the
Sourcery-generated VaultAccountManagerMock to keep the test double in sync with
the VaultAccountManager protocol: locate the VaultAccountManagerStub class and
swap usages to VaultAccountManagerMock (or import/use the generated mock target)
so tests use the generated mock implementation instead of the manual
getAccount/throwing-method stubs; if a generated mock does not yet exist, mark
the protocol as mockable for the generator or add the protocol to the mock
target so a VaultAccountManagerMock is produced and then update tests to
instantiate and configure that mock.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aba3f61f-5f50-4e4b-bd1b-faca46abd400

📥 Commits

Reviewing files that changed from the base of the PR and between e7f1aec and 8d2d7fb.

📒 Files selected for processing (11)
  • Cryptomator.xcodeproj/project.pbxproj
  • CryptomatorCommon/Sources/CryptomatorCommonCore/ErrorExtensions.swift
  • CryptomatorCommon/Tests/CryptomatorCommonCoreTests/ErrorExtensionsTests.swift
  • CryptomatorFileProvider/Middleware/ErrorMapper.swift
  • CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift
  • CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift
  • CryptomatorFileProviderTests/Middleware/ErrorMapperTests.swift
  • CryptomatorTests/UnlockVaultViewModelTests.swift
  • CryptomatorTests/VaultUnlockingMock.swift
  • FileProviderExtensionUI/FileProviderCoordinator.swift
  • FileProviderExtensionUI/UnlockVaultViewModel.swift
💤 Files with no reviewable changes (2)
  • CryptomatorFileProvider/Middleware/ErrorMapper.swift
  • CryptomatorFileProviderTests/Middleware/ErrorMapperTests.swift

@tobihagemann tobihagemann merged commit 0ab07fb into develop Apr 24, 2026
6 checks passed
@tobihagemann tobihagemann deleted the feature/fix-offline-unlock-in-file-provider branch April 24, 2026 12:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant