Skip to content

Nc.swift.review parity - #125

Open
ncooke3 wants to merge 10 commits into
pb-swiftfrom
nc.swift.review-parity
Open

ncooke3 wants to merge 10 commits into
pb-swiftfrom
nc.swift.review-parity

Conversation

@ncooke3

@ncooke3 ncooke3 commented Sep 22, 2026

Copy link
Copy Markdown
Member

No description provided.

…ation

`AppCheckCoreAppAttestKeyIDStorage.getAppAttestKeyID()` reports "no key
stored" by throwing `appAttestKeyIDNotFound`, never by returning nil
(matching the rejected promise in the ObjC implementation). The rewritten
`attestationState()` used `try await`, so that error propagated out and
failed the entire token fetch, making the `guard let ... else { return
supportedInitialState }` fallback unreachable dead code.

Since a fresh install has no stored key ID, App Attest attestation failed
100% of the time on first launch.

In ObjC this worked because `FBLPromiseAwait` returned nil on rejection and
the out-error was deliberately ignored, letting the nil check fall through
to the initial state. Restore that behavior for both the key ID lookup and
the artifact lookup (a transient Keychain read failure now degrades to
re-attesting rather than failing the fetch).

The bug was hidden because `MockAppAttestKeyIDStorage` was scripted with
`.success(nil)`, a value the real storage never produces. The shared test
helper now throws `appAttestKeyIDNotFound` instead, so the whole suite
exercises the real storage contract; reverting the source fix now fails 5
tests.

Also fixes a merge artifact in MockRecaptchaSupport.swift that left a stray
`request:` argument and broke compilation of the Recaptcha test target.
…c level

Three parity regressions in the logger:

1. `#if !NDEBUG` never compiled anything out. `NDEBUG` is a C preprocessor
   macro and is not a Swift compilation condition, so the expression was
   unconditionally true and all logging shipped in Release builds. The ObjC
   `GACAppCheckLog` compiled logging out via the same-named C macro, which
   did work there. This leaked App Check debug tokens into Release logs.
   Verified: the release build of AppCheckCoreLogger.swift.o now contains 0
   occurrences of the log format string (debug contains 1).

2. `print` replaced `NSLog`, so output no longer reached the system log and
   was invisible in Console.app without a debugger attached.

3. `logLevel` was a plain static var. The ObjC class property was `atomic`,
   backed by a `volatile` static. Restored serialized access via a lock.

Tests: restore the default log level in tearDown so the global state cannot
leak between tests, and add a concurrent read/write test for `logLevel`.
…ests

Two divergences in `getToken(limitedUse:)` request coalescing.

1. The chaining path used `_ = try? await ongoingTask.value`, swallowing the
   in-flight operation's failure so every queued caller went on to start a
   brand new attestation sequence. ObjC chained with `.thenOn`, which runs
   only on success, so a chaining caller was rejected with that same error
   (GACAppAttestProvider.m:213-216). During an outage this turned N queued
   callers into N sequential full attestation attempts instead of N fast
   failures, and handed callers a different error than the one that actually
   occurred.

2. The ongoing-operation slot was cleared in the originating caller's
   `defer`, not inside the operation. ObjC cleared it via `.thenOn`/
   `.recoverOn` chained onto the operation itself (L228-236), guaranteeing
   the slot was nil before any chained waiter resumed. With the clear in the
   caller, a waiter could resume while the completed task was still parked in
   the slot and spin through repeated `.retry` recursions until the
   originator happened to get scheduled.

Also removes the errSecDuplicateItem swallow in AppCheckCoreStorage (commit
1b743a7, added to quiet a test flake). It resumed the continuation with
success on a failed keychain write, so the in-memory token silently diverged
from what was actually persisted. ObjC propagated all keychain errors. The
flake is fixed properly in the tests instead, by scoping each test's token
key with a per-instance UUID and cleaning up in tearDown.

Adds `GACAppCheckErrors.errorDomain` so Objective-C can still reach the error
domain; Swift global constants are not bridged, so v11's
`GACAppCheckErrorDomain` global had disappeared from the ObjC API entirely.

Tests: adds the first coverage of the documented concurrent-request contract
(chained-caller error propagation, and standard-request coalescing), plus an
ObjC test pinning the error domain string. Verified both new coalescing tests
fail against the pre-fix implementation.
In v11 every public completion handler was invoked from a bare FBLPromise
`.then` / `.catch`. Those dispatch onto `FBLPromise.defaultDispatchQueue`,
which `+[FBLPromise initialize]` sets to `dispatch_get_main_queue()` and
which nothing in this library or its known consumers ever reassigns. The
effective contract was therefore: completion handlers always arrive on the
main queue, and always asynchronously (FBLPromise uses an unconditional
`dispatch_group_async` with no same-queue fast path).

Nothing at the v11 call site mentions a queue, so the guarantee was invisible
and was dropped during the Swift port: `Task {}` at a nonisolated call site
resumes on the cooperative pool. UI-layer callers touching UIKit from the
handler would hit timing-dependent main-thread violations.

Restores main-queue delivery for:
  - token(forcingRefresh:completion:)
  - limitedUseToken(completion:)
  - periodicTokenRefresh(completion:)
  - tokenRefresher.updateWithRefreshResult + tokenDelegate.tokenDidUpdate,
    which v11 ran inside the same bare `.then` in -[GACAppCheck refreshToken]

The delegate notification is awaited rather than fire-and-forget because the
v11 promise resolved only after that block ran, so callers were guaranteed the
delegate had already been notified before receiving the token.

The `async` variants intentionally keep resuming on the cooperative pool;
async callers are expected to hop themselves, and forcing a hop would be a new
divergence rather than parity.

Adds AppCheckCoreMainThreadDeliveryTests (6 cases) covering success, error,
off-main callers, and the delegate. All 6 fail without the source change.
…erage

Restores 8 cases that existed in the v11 Objective-C suite but had no Swift
equivalent after the rewrite. All are paths where the server returns HTTP 200
with an unusable body, or where a storage key is constructed — the kind of
coverage whose absence is invisible because the happy path and the HTTP-error
path still pass without it.

GACAppAttestAPIServiceTests (7 cases):
  - getRandomChallenge with an empty response body
  - getRandomChallenge with a non-JSON response body
  - getRandomChallenge with valid JSON missing the `challenge` field
  - getAppCheckToken network error / unexpected response
  - attestKey network error / unexpected response

All 7 passed on first run, which is itself the parity evidence: the Swift
implementation already emits the same failure reasons as v11 ("Empty server
response body.", "JSON serialization error.", and naming the missing
`challenge` field) and propagates transport errors unwrapped rather than
translating them.

GACAppCheckDebugProviderTests (1 case):
  - registeredUserDefaultsKey sanitization: `/` becomes `_`, empty components
    fall back to "default"

This key namespaces the "debug token already registered" flag. A change to its
construction would silently orphan the flag for every existing install and
force a spurious re-registration, so it is worth pinning. The v11 test also
covered nil arguments; those are unrepresentable in Swift since both
parameters are non-optional String, so only the empty-string fallback is
reachable.

Full suite: 167 tests, 0 failures.
`pod lib lint` builds via xcodebuild against the podspec, which does not
inherit `Package.swift`'s `swiftLanguageModes: [.v5]`. It therefore surfaced
four `capture of non-Sendable type` warnings in the main-queue delivery added
in 5007f69, and escalated them to fatal. `swift test` could not see them.

Rebind the crossing values with `nonisolated(unsafe) let` rather than
declaring the types `Sendable`: the refresher, the token delegate and the
completion handler are all caller-supplied, so the library cannot promise
thread-safety on the caller's behalf. The boundary itself is unchanged and
matches the unsynchronized `dispatch_async` hand-off v11 performed.

Also record in agents.md that the Ruby 2.7.5 override is now harmful, and
that `swift build -Xswiftc -strict-concurrency=complete` reproduces the lint
failure in seconds.
The prior note claimed SwiftPM suppresses concurrency diagnostics because of
swiftLanguageModes: [.v5]. That is wrong. Verified empirically by reverting
the fix and rebuilding: plain swift build emits all four
#SendableClosureCaptures warnings at the same lines pod lib lint reported.

The real difference is escalation, not visibility - SwiftPM exits 0 and the
warnings scroll past, while pod lib lint treats them as fatal. Both build
systems are in Swift 5 language mode, so the podspec and Package.swift are
not actually divergent here.
SwiftPM emits Swift diagnostics but exits 0, so the four #SendableClosureCaptures
warnings that later failed pod lib lint scrolled past every local and CI SPM
build unnoticed. That left CocoaPods as the only gate able to reject them - a
~5 minute round-trip, and an accidental dependency rather than a designed one.

Add a dedicated job running swift build --build-tests -Xswiftc -warnings-as-errors.
Verified clean on a from-scratch build of both sources and tests (exit 0, zero
warnings), so this does not red the tree on merge.
firebase-ios-sdk dropped Xcode 16.x, so this repo follows. Firebase's
convention pairs macos-15 with Xcode_26.2, which both remaining legs now use.

- spm.yml: matrix Xcode_16.4 -> Xcode_26.2, and the macos-14 / Xcode_16.2
  include is removed entirely rather than repinned, since macos-14 does not
  carry Xcode 26.x.
- app_check_core.yml: the macos-15 lint leg moves off Xcode_16.4; macos-26 was
  already on 26.2.
- The new warnings-as-errors job is pinned to match.

This also closes a verification gap rather than opening one: every gate run
during this review (swift test, pod lib lint on four platforms, Catalyst,
warnings-as-errors) was executed locally on Xcode 26.2, whereas CI had been
on 16.4 - a toolchain nothing in this branch was ever checked against.
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