Repository navigation
Harden tests: characterize JSON:API, sessions and keychain; replace double-only tests - #96
Merged
Merged
Conversation
…ouble-only tests - Characterization tests for JSONAPIDocument/Resource/Relationship, DataCacheUpdate.loadFrom, where RootPathService differs from Service, unreadable KeychainStore items, and SessionController sign-in/out, permissions, locale sync and privacy/terms confirmation. - Real ShopRepository and AccountPasswordRepository against TestHTTPServer replace the Demo repositories, whose tests only exercised the doubles. - ForgotPassword/ResendConfirmation tests drive the real ViewModels. - Enable the two ShopkeeperTest functions that lacked @test, and assert the spy fields the ViewModel tests set but never checked. - Make signingOutRemovesCachedResponses deterministic: SessionController takes the URLCache it empties (URLCache.shared by default) and the test uses its own in-memory one. - Test-target lint cleanup: redundant @suite, isEmpty, failable String(bytes:). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K5hZEFqXhBsUQwSWsgR6eH
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 4 (test hardening). This PR adds characterization tests for the code phase 5 will refactor, enables two dormant tests, replaces tests that only exercised test doubles, and fixes a flaky test. The only production change is a test seam:
SessionController.inittakes aurlCache:parameter that defaults to.shared.Coverage was measured on the iPhone (
-enableCodeCoverage YES,xcrun xccov). App target: 28.51% → 33.97% lines.Changes
1. Dormant tests and unchecked spies
ShopkeeperTest.shopkeeperDictionaryHasRequiredFieldsandadditionalEntriesInTheDictionaryAreIgnoredhad no@Test. Both pass as written, so they revealed no bug.PasswordEditViewModelTest:lastUpdatePassword. Every field is sent, and each one is trimmed (the confirmation now ends in\n).ForgotPassword/ResendConfirmationInstructionstests:lastSendResetPassword/lastSendConfirmation, with the email trimmed.ShopkeeperEditViewModelTest:updateCalled,lastSignUp(name, email, the selected time zone, trimming) anddestroyCalled.2. Flaky
signingOutRemovesCachedResponsesURLCache.sharedwrites stored responses to disk later, on its own queue. When that write lands afterremoveAllCachedResponses(), the response comes back, and the final#expect(... == nil)fails.-test-iterations 30, next to SessionControllerPermissionsTest and MainViewModelTest, failed 1 of 30 times at that line.URLCache.sharedit failed twice, always at the post-logout check. With an in-memoryURLCacheit never failed.#require.SessionControllertakesurlCache: URLCache = .shared.signOutEmptiesTheSharedCacheByDefaultchecks that the app's default is stillURLCache.shared.3. Characterization tests (current behavior, passing on main)
JSONAPIDocumentTest(new):dataas an object or an array, in order;includedas an array or an object.parent.""(not the initial"0") and has noEntityIdentity.EntityTypename (shop, shopkeeper, shopkeeper_sign_in, item_tag); other spellings have none.typeis the relationship name.DataCacheUpdateTest(new). No production code callsDataCacheUpdate.loadFrom. Every Request adaptsdoc.dataitself.EntityTypeare dropped.merged(with:)keeps the order.RootPathServicesTest, whereRootPathServicediffers fromService:error_messagehas a nil message.KeychainStoreTest:archiveFailure.archiveFailure(badData).unarchivedObjectreturns nil, and thebadDatathrown inside thedoblock is wrapped by its owncatch.LoggedInShopkeeperarchive missingtokenisarchiveFailure. The test first checks that the complete archive reads back. It uses its own keychain item, not the app's session.SessionControllerTest/SessionControllerPermissionsTest:didExpireSession; a failure stays signed out and rethrows.logout: success, and a failure while signed in, which still clears the session and pops to root.updateShopkeeperwith nil and with a new shopkeeper.sessionDidExpirewith a nil token or while signed out.shouldUpdateAppisInt(appBuild) < ios_app_version(build + 1 → true, equal → false)./shopkeeper_authwith{"locale":"en"}saves the new locale. A rejected sync (devise's 401) keeps the session, because it goes throughRootPathService.MeRequest.swift, and a failure keeps the flags.4. Real code instead of test doubles
NativeAppTemplateTests/Demo/(3 Demo repositories and their 3 tests, 542 lines). Nothing else used them.isValidEmailcases that only the old Forgot/Resend tests had moved toStringExtensionsTest.ShopRepositoryTest(as in paid #116) runsShopRepository,ShopsServiceand the shop requests againstTestHTTPServer:reloadloads the shops and the limits; a missing meta field fails it.fetchDetailupserts.createanddestroyleaveshopsalone.updatereplaces the shop; a failed update keeps it.AccountPasswordRepositoryTest: PATCH/{account}/api/v1/shopkeeper/account/passwordwithUpdatePassword.toJson's body; the API error reaches the caller.ForgotPasswordViewModelTestandResendConfirmationInstructionsViewModelTestcopied the validation logic inline. They now drive the real ViewModels: validation, trimming, the success message and dismissal, and the fixed error message.TestLoginRepositorygainsloginError.5. Test-target lint cleanup
@Suiteattributes (SwiftFormatredundantSwiftTestingSuite, 31 files).== ""andcount == 0withisEmpty.TestHTTPServeruses the failableString(bytes:encoding:).#requires that the compiler warned about.Red checks
I mutated production code in two batches and ran the affected suites on the iPhone. Every new, enabled or strengthened test failed, and the code was then restored.
included; reversedata; drop meta, parent and relationship names; add extra links; default id"0"; renameitem_tag; read an array body.""for a missing message.notFound.completed_item_tags_count.makeClientuses the wrong token or account.didExpireSessionon sign-in; don't resetuserStateafter a failed sign-in.shouldPopToRootViewon logout; keep the shopkeeper after a failed logout.updateShopkeeperpops to root and skips saving.isLoggedIn/nil-token guards insessionDidExpire.fetchPermissionsIfNeededguard; use<=forshouldUpdateApp.URLCache.rejectedLocaleSyncKeepsTheSessionfails.badData→notFound. Only the another-class test fails.merged.data.isEmailInvalid.isValidEmail: a regex that allows spaces and digits-less local parts, makes@optional and has no dotted domains.Local checks
make lint: 0 violations, 0 files to format.WaitUntilTest).urlCache:parameter. It defaults toURLCache.shared, whichsignOutEmptiesTheSharedCacheByDefaultchecks, so there is no runtime behavior to exercise. The whole suite ran hosted in the app on the iPhone.Notes for phase 5 (not changed here)
DataCacheUpdate.loadFromhas no production caller in Free. Free could remove the whole layer, beyond what paid #117 did:relationships, which is collected twice and passed to adapters that never read it.EntityIdentity/EntityRelationship, theEntityAdapterprotocol,cacheUpdate:.JSONAPIRelationship, links, the resource meta andparent.RootPathServiceandServiceduplicate the request and error handling. They differ on the 401 expiry notice, query items and thesourceheader.KeychainStore.retrieveusestry?onkeychain.data(for:), which turns any read error intonotFound. ItsbadDatacomes out asarchiveFailure(badData).SessionController.logoutrepeats the same three lines in both branches.AccountPasswordRepository.updatelogs a failure asFailure.destroyinstead of.update. I left it alone because it is a bug-fix item.🤖 Generated with Claude Code
https://claude.ai/code/session_01K5hZEFqXhBsUQwSWsgR6eH