Repository navigation
Harden the test safety net before refactoring - #76
Merged
Merged
Conversation
RepositoryRequestsTest runs every repository through NetModule's OkHttp client and Retrofit converters (with AuthInterceptor and AcceptLanguageInterceptor) against a local HTTP server, pinning each call's method, path, query and body, the session and Accept-Language headers, and how error bodies become ApiExceptions. ApiResponseExtensionsTest adds devise's full_messages body, JSON and empty bodies without a message, and the body code vs the HTTP status. AuthInterceptorTest adds the case-insensitive API host match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K5hZEFqXhBsUQwSWsgR6eH
- NativeAppTemplatePreferencesDataSource: the stored session, the shouldUpdateApp derivation from BuildConfig.VERSION_CODE, dark mode, the flags, and clearUserPreferences resetting everything (including dark mode and the shop tip). - NativeAppTemplateAppState over the real preferences: authDestination (null until loaded, then following sign-in and the permissions), shouldShowBottomBar, isOffline and the deletion flags. - Data.updateRelationships and its callers: "included" is merged into nothing today; the results equal the decoded data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K5hZEFqXhBsUQwSWsgR6eH
- SignInEmailAndPasswordViewModel: rejected credentials show the coded message and save nothing; a sign-in clears the previous preferences before saving the new session (TestLoginRepository gains failLogin). - MainActivityViewModel.updatePermissions: saves the permissions when signed in, fetches nothing when signed out. - ItemTagListViewModel: a page without pagination meta is the last one; a failed reload does not block loading more. - Expected values no longer come from the code under test: the item tags are compared with the fixtures instead of getDatumWithRelationships() of the same data, and the error messages with literals derived from ApiException instead of codedDescription. 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) before the refactoring phase: characterization tests of the current behavior of the code phase 5 is likely to touch. Test-only. No production code changes.
Coverage, measured locally (not committed)
JaCoCo via a temporary
enableUnitTestCoverage = true(plusincludeNoLocationClassesso the Robolectric tests count), reverted before committing. I didn't add a plugin because it would also need CI wiring. Line coverage of the Kotlin sources, excluding*View.ktcomposables anddesignsystem/:Still untested and not addressed here: navigation graphs (
*Navigation.kt,NativeAppTemplateNavHost),MainActivity,NativeAppTemplateApp,ConnectivityManagerNetworkMonitor, the Hilt modules, andUtilityintents. These are Compose/Android glue, which suits instrumented or UI tests better.What's added
Network and repositories (
data/RepositoryRequestsTest, new). Each repository runs through NetModule's realprovideOkHttpandprovideRetrofit(re-pointed withnewBuilder().baseUrl), withAuthInterceptorandAcceptLanguageInterceptor, against a JDKHttpServer. The tests pin down:?page=(and no query when the page is null), and the complete/idle routesnull, the update-password, locale-only and sign-up payloadsAccept-Languagelogoutclears the stored session on both success and failure{code, error_message}becomesApiError; a 401 devise body becomesisUnauthorizedApiException mapping (
ApiResponseExtensionsTest). Adds devise's{"errors":{"full_messages":[...]}}, JSON and empty bodies with no message ("HTTP <status>"), and the case where the body'scodediffers from the HTTP status.AuthInterceptor host check. The API host is matched case-insensitively (RFC 4343).
DataStore (
NativeAppTemplatePreferencesDataSourceTest). Covers the stored session fields,shouldUpdateAppagainstBuildConfig.VERSION_CODE(newer means an update is needed, equal means it isn't), the dark-theme round trip and its FOLLOW_SYSTEM default, the flags, andclearUserPreferencesresetting everything.Session bootstrap.
NativeAppTemplateAppStateTest(new) uses the real LoginRepositoryImpl and DataSource.authDestinationis null until collected, then ONBOARDING → SHOPS → ACCEPT_PRIVACY → NEED_APP_UPDATES → ONBOARDING. It also coversshouldShowBottomBar,isOfflineand the deletion flags.MainActivityViewModelTest:updatePermissionssaves the permissions when signed in and fetches nothing when signed out.SignInEmailAndPasswordViewModelTest: rejected credentials show the coded message and save nothing. A sign-in clears the previous preferences before saving the new session.TestLoginRepositorygainsfailLogin.ItemTagList pagination. A page with no
metacounts as the last page. A failed reload shows the error but does not setloadMoreFailed.Data.updateRelationships(model/DataRelationshipsTest, new). This pins down the reported no-op without fixing it. Withincluded, the result equals the original Data, and the shop reference keeps only{id, type}. Withoutincluded, it returns the same instance.ItemTags/Shops.getDatumWithRelationships()andItemTag/Shop.getData()all equal the decoded data.LoadOnceEffect / LoadOnceState. No new tests. The existing configuration-change and process-death tests already cover it (100% lines once Robolectric is instrumented).
Weak tests fixed
ItemTagListViewModelTest.stateShop_whenSuccess…compared the UI state withgetDatumWithRelationships()applied to the same data, so the transformation was on both sides. It now compares with the fixtures.SignInEmailAndPasswordViewModelTestandShopkeeperEditViewModelTestusederror.codedDescriptionas the expected message. They now use literals derived fromApiException.UnprocessableError("[NATIVEAPPTEMPLATE-2002] Processing error: $rawMessage"). The red check below shows the difference: changing that description now fails both tests, and before it would have gone unnoticed.I scanned the test sources for tests that assert nothing and found none. Every test the scan flagged either uses
@Configor asserts through the Compose test APIs.Red checks
I broke the code under test temporarily (never committed) and confirmed that each new or changed test failed:
@Query("page")→"p"addInterceptor(acceptLanguageInterceptor)from NetModulecompleteItemTag→idleItemTagdeleteShopemitting falseclearUserPreferencesin eachlogoutbranch"en"page ?: 1ItemTagBodyDetail.descriptionandShopBodyDetail.description(both now dropped byencodeDefaults = false)full_messagesbranchApiError.codetaken from the HTTP status"HTTP error"fallbackisUnauthorizedon 403ApiErrorpath disabledRequestHelperalways adding the sessionequals(apiHost)withoutignoreCase<→<=and>inshouldUpdateAppisShopDeletedalways trueauthDestinationstarting at ONBOARDINGauthDestinationreadingisEmailUpdatedin place ofshouldUpdateAppisOfflinewithout thenotisShopDeletedread from the wrong flowclearUserPreferencesfrom sign-in.catchShowingError→.catch {}if (isLoggedIn)→if (true)setPermissionstotalPages ?: 2loadMoreFailed = trueUnprocessableErrordescription text changedupdateRelationshipsreturningrelationships = nullor a copyFindings for later phases (not fixed here)
"{account_id}/api/v1/..."becomes"/api/v1/...", and Retrofit resolves that against the host root. So a request made while signed out goes to a route without the account segment instead of failing locally.RepositoryRequestsTest.aSignedOutRequest_carriesNoTokenAndNoAccountSegmentpins this down. Low impact, because screens that call these APIs are only reachable when signed in.Data.updateRelationshipsis a no-op, andincludedis never merged. Its callers (getDatumWithRelationships,Shop/ItemTag.getData) can be simplified or removed in phase 5.clearUserPreferences(on sign-out, and on every sign-in) also resets dark mode anddidShowTapShopBelowTip. That's a product decision, so I'm reporting it rather than changing it.NativeAppTemplateAppState.isLoggedIn,shouldUpdateApp,shouldUpdatePrivacyandshouldUpdateTermsare never read outside the class.Checks
spotlessCheck,lintDebug,:app:minifyReleaseWithR8,testand:app:assembleReleaseall pass (run with--no-daemon). Tests: 383 in:appand 15 in:model, 0 failures.🤖 Generated with Claude Code
https://claude.ai/code/session_01K5hZEFqXhBsUQwSWsgR6eH