wordpress-mobile / wordpress-mobile/WordPress-iOS

Encode the keychain "not found" contract in `KeychainAccessible` (test doubles have drifted)

Open
#25,659 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

[Type] Tech Debt
Dominant language
Swift
Stars
3.9k
Forks
1.2k
Avg merge
23h 51m
Merged PRs (30d)
58

Description

Problem

KeychainAccessible types the signature but not the contract:

func getPassword(for username: String, serviceName: String) throws -> String

A missing item can only be signaled by throwing, but nothing says which throw means "not found" vs. a real failure. The real implementations (AppKeychain, SharedKeychain, KeychainUtils) throw a bridged SFHFKeychainUtils NSError on a miss, and the not-found-vs-failure distinction is recovered after the fact by a free function — isRealKeychainFailure — that sniffs the NSError domain/code. The contract is implicit and stringly-typed.

Because nothing pins it down, the test doubles have each guessed differently:

Double not-found behavior
Tests/KeystoneTests/Tests/Jetpack/KeychainUtilsMock.swift returns "" (no throw)
Tests/KeystoneTests/Helpers/TestKeychain.swift throws TestKeychainErrors.keychainItemNotFound
Modules/Tests/WordPressDataTests/Helpers/MockKeychainService.swift throws MockKeychainError.notFound (and keys storage by username only, ignoring serviceName)
Modules/Tests/WordPressSharedTests/KeychainStub.swift throws StubError.notFound

Three throw (three different error types); one returns an empty string; they also disagree on the storage/key model.

Why it bites

SharedDataIssueSolver.migrateAuthKey has no test coverage, and the ""-returning KeychainUtilsMock (injected by the migrator tests) makes it hard to add: the "already migrated" guard if let _ = try? appKeychain.getPassword(…, .jetpack) always succeeds against that mock, so the migration write path can never be exercised. More broadly, a test written against the wrong double can pass while the real keychain behaves differently on a miss.

Proposed fix

Move the contract into the type:

enum KeychainError: Error { case notFound; case failure(OSStatus) }
func getPassword(for username: String, serviceName: String) throws(KeychainError) -> String
  • Every conformer — real and test — must produce .notFound for a miss; the compiler enforces it, and "" is no longer a way to signal absence.
  • The classification happens once, at the boundary (in AppKeychain/SharedKeychain, mapping the SFHFKeychainUtils NSError), and isRealKeychainFailure goes away — it only exists because the current contract can't express the distinction.
  • Then consolidate the four doubles into a single faithful in-memory implementation.

A lighter version is throws -> String? (nil = not found), which beats today's contract but doesn't carry the failure distinction.

Provenance

Pre-existing. The protocol and the "" mock are both on trunk (the mock since at least #24462, Apr 2025; trunk's migrateAuthKey has the identical untested try? pattern). Surfaced while reviewing #25654 (the keychain access-group split); #25658 is a companion fix for an unrelated logout bug. This is not a blocker for either — filing separately so the redesign isn't loaded onto the access-group work.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading KeychainAccessible and the real implementations AppKeychain, SharedKeychain, and KeychainUtils, then inspect the four named test doubles and SharedDataIssueSolver.migrateAuthKey. Done means the chosen not-found contract is enforced across conformers, the doubles share faithful username/serviceName behavior, migration coverage can exercise the write path, and isRealKeychainFailure is removed.

Written by the indexing model from the issue text.

Assessment

Tech stack
ios, swift
Domain
authentication, mobile, security
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.