test(test): serve CDA locales, a root mount, and skip paging from mocks [NT-3946] - #450
Conversation
…ks [NT-3946] The mock CDA only ever had to satisfy contentful.js. The Android and iOS SDKs read the CDA with their own platform clients, and the iOS reference app is moving onto contentful.swift, which reaches the mock in ways the handlers did not support. Three gaps, and why each one blocks or degrades a native client: Locales. contentful.swift resolves locale fallback chains client-side. Client.fetchLocalesIfNecessary requests /locales before any other endpoint, and entries cannot be decoded until it succeeds, so a stock Contentful.Client failed on its first entry query against the mock. The new route is built from the locale set already in the space fixture and honors the limit of 1000 the SDK sends. contentful.js sends locale to the API instead and never requests the endpoint, which is why the gap stayed invisible while only web consumed the mock. Root mount. Contentful.Client builds every request as scheme://host[:port]/spaces/… and ClientConfiguration exposes no base-path hook, so it cannot address the /contentful/ namespace the mock server used. The same handlers are now also mounted at the host root. contentful.js has basePath and the Android app builds its URLs from AppConfig.contentfulBaseUrl, so both keep using the prefixed mount unchanged; the iOS app can now differ from a production integration only by host and secure = false, instead of installing a URLProtocol shim. Skip. The content-type query parsed skip and then ignored it while echoing a skip of zero back, so any page after the first returned the first page again and a paging consumer accumulated duplicates. The clients that page this way are the native ones: fetchAllEntries in the iOS SDK and the URL built by the Android PreviewContentfulClient both walk skip, and contentful.swift pages the same way. With 45 fixture entries and a 100-item batch nothing pages today, so this is a latent fidelity gap rather than a live failure. Both routes are additive and no existing consumer changes mounts. The first consumer of the new behavior is the iOS reference app's move to contentful.swift, which lands on top of this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Code Review Agent Run #4b43d3Actionable Suggestions - 0Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
|
| Source | Requirement / Code Area | Status | Notes |
|---|---|---|---|
| NT-3946 | Add /locales endpoint to mock CDA to support iOS SDK's Client.fetchLocalesIfNecessary method | ✅ Met | The /locales endpoint is implemented in lib/mocks/src/contentful-handlers.ts via handleLocalesQuery function (lines 77-103). The endpoint reads locale data from the space fixture at lib/mocks/src/contentful/data/space/ctfl-space-data.json and honors the 1000-item limit via LOCALES_LIMIT constant. The handler route is registered at `spaces/:spaceId/environments/:environmentId/locales` and returns properly paginated responses with CORS headers. |
| NT-3946 | Mount mock CDA handlers at host root in addition to /contentful/ namespace | ✅ Met | The mock CDA handlers are mounted at host root in lib/mocks/src/server.ts via `...getContentfulHandlers('*/')` (line 133). This enables iOS SDK clients (contentful.swift) to address the mock server without URLProtocol shims, since their Client builds requests as `scheme://host[:port]/spaces/...` with no base-path hook. The existing namespace mount at `/contentful/` is preserved for contentful.js and Android clients. |
| NT-3946 | Fix skip parameter handling in content-type query to properly respect non-zero skip values | ✅ Met | The skip parameter handling is fixed in lib/mocks/src/contentful-handlers.ts within handleContentTypeQuery (lines 48-52, 61). The code now properly parses and validates the skip parameter, applies it via `filtered.slice(skip, skip + limit)` for correct pagination, and echoes back the actual skip value in the response instead of always returning 0. This prevents SDK paging consumers from silently looping over the same first page. |
Impact Analysis by BitoCross-Repository Impact Analysis
Code Paths AnalyzedImpact: Flow: Direct Changes (Diff Files): Repository Impact: Cross-Repository Dependencies: Database/Caching Impact: API Contract Violations: Infrastructure Dependencies: Additional Insights: Testing RecommendationsFrontend Impact: Service Integration: Data Serialization: Privacy Compliance: Backward Compatibility: OAuth Functionality: Cross-Service Communication: Reliability Testing: Additional Insights: Analysis based on known dependency patterns and edges. Actual impact may vary. |
✅ Review Settings OverriddenStatus: Guidelines:
Note: Extra guidelines beyond 3 general purpose guidelines and 1 language specific guideline per language are not processed. Guidelines are fetched from the source branch. |
…roid's in-app synthesis The mock CDA gained a real `/locales` route in #450 for iOS's contentful.swift migration. Android's `LocalesInterceptor` still synthesized both `/locales` and `/content_types` in-app because neither existed on the mock when it was written. `/locales` is now redundant and removed outright: a real request succeeds. `/content_types` is NOT redundant on its own — contentful.java's `ResourceFactory.array()` unconditionally calls `RichTextFactory.resolveRichTextField` on every array response, which unconditionally calls `ResourceUtils.ensureContentType` for every entry, regardless of whether that entry has a Rich Text field, and throws `CDAContentTypeNotFoundException` on a 404. The reference app's fixtures do have Rich Text fields (RichText.kt is exercised by ContentEntryView/NestedContentEntryView), so this path is genuinely load-bearing. Rather than keep the in-app fixture shim, add a `/content_types` route to the mock server that serves `ctfl-space-data.json`'s `contentTypes` array, mirroring the `/locales` route's shape and #450's precedent of fixing the mock rather than shimming around it in a file customers copy from. This lets Android's bundled `content_types.json`/`ContentTypesFixture` be deleted entirely. With both interceptor-synthesized routes gone, MockContentfulClient.kt no longer earns its own file: consolidate the shared CDAClient and the fetch-by-ids loop into ContentfulFetcher.kt, mirroring iOS's single ContentfulClient.swift shape. Validated: `pnpm --filter mocks typecheck`, `pnpm lint`, `pnpm format:check` clean; both APKs assemble; full Maestro suite passes 70/70 flows across Compose and Views.
…ntentful.java CDA SDK (NT-3947) (#439) * feat(implementations): migrate Android reference implementation to contentful.java CDA SDK (NT-3947) Replace hand-rolled HTTP/JSON Contentful fetching in the Android reference implementation with Contentful's official com.contentful.java:java-sdk CDAClient, mirroring the iOS (NT-3946) migration. - MockContentfulClient builds the shared CDAClient against the mock server and synthesizes the /locales and /content_types responses CDAClient requires (mock server only serves /entries), bundling real content-type definitions so Rich Text field resolution can find them. - ContentfulFetcher now fetches by-ID entries through CDAClient (single locale, include=10) and decodes them with the SDK's typed entry APIs instead of hand-rolled JSON parsing/link resolution. - MockPreviewContentfulClient wraps the same CDAClient for the preview panel's audience/experience fetch, fixing two regressions found while validating against the full Maestro suite: - Concurrent nt_audience/nt_experience fetches against the same shared CDAClient could interleave and drop entries (CDAClient mutates shared instance state while a call is in flight); calls are now serialized with a Mutex. - The mock's content-type-filtered entries endpoint never returns an includes section, so contentful.java silently drops any Link field it can't resolve rather than keeping it as a stub, breaking the preview panel's audience/experience grouping; raw link stubs are now restored from CDAEntry.rawFields() when contentful.java drops them. Verified with the full Compose and Views Maestro suites (35/35 passing on both), including all preview-panel-overrides scenarios. * refactor(implementations): favor typed CTEntry over Map in Android reference implementation Rework the app-owned entry-rendering components (ContentEntryView, NestedContentEntryView, ContentEntryViewBinder, NestedContentEntryViewBinder, MainScreen, MainActivity) to accept and pass CTEntry instead of raw Map<String, Any>, mirroring the SDK's own typed OptimizedEntry(entry: CDAEntry) entry point. Also drops redundant nullable type arguments on getField calls. Key CTEntry, having no structural equals/hashCode, are compared by reference: remember/LaunchedEffect keys in ContentEntryView and NestedContentEntryView now key on entry.toMap() (structurally comparable) instead of the entry instance or its id, restoring correct re-resolution of merge-tag text after identify() calls. * fix(implementations): serve CDA content types from the mock, drop Android's in-app synthesis The mock CDA gained a real `/locales` route in #450 for iOS's contentful.swift migration. Android's `LocalesInterceptor` still synthesized both `/locales` and `/content_types` in-app because neither existed on the mock when it was written. `/locales` is now redundant and removed outright: a real request succeeds. `/content_types` is NOT redundant on its own — contentful.java's `ResourceFactory.array()` unconditionally calls `RichTextFactory.resolveRichTextField` on every array response, which unconditionally calls `ResourceUtils.ensureContentType` for every entry, regardless of whether that entry has a Rich Text field, and throws `CDAContentTypeNotFoundException` on a 404. The reference app's fixtures do have Rich Text fields (RichText.kt is exercised by ContentEntryView/NestedContentEntryView), so this path is genuinely load-bearing. Rather than keep the in-app fixture shim, add a `/content_types` route to the mock server that serves `ctfl-space-data.json`'s `contentTypes` array, mirroring the `/locales` route's shape and #450's precedent of fixing the mock rather than shimming around it in a file customers copy from. This lets Android's bundled `content_types.json`/`ContentTypesFixture` be deleted entirely. With both interceptor-synthesized routes gone, MockContentfulClient.kt no longer earns its own file: consolidate the shared CDAClient and the fetch-by-ids loop into ContentfulFetcher.kt, mirroring iOS's single ContentfulClient.swift shape. Validated: `pnpm --filter mocks typecheck`, `pnpm lint`, `pnpm format:check` clean; both APKs assemble; full Maestro suite passes 70/70 flows across Compose and Views.
Extracted from #429 so it can merge first: #429's iOS work depends on these mock routes at runtime.
The mock CDA only ever had to satisfy
contentful.js. The Android and iOS SDKs read the CDA with their own platform clients, and the iOS reference app is moving ontocontentful.swift, which reaches the mock in ways the handlers did not support. Three gaps, and why each one blocks or degrades a native client.Locales
contentful.swiftresolves locale fallback chains client-side.Client.fetchLocalesIfNecessaryrequests/localesbefore any other endpoint, and entries cannot be decoded until it succeeds, so a stockContentful.Clientfailed on its first entry query against the mock. The new route is built from the locale set already in the space fixture and honors the limit of 1000 the SDK sends.contentful.jssendslocaleto the API instead and never requests the endpoint, which is why the gap stayed invisible while only web consumed the mock.Root mount
Contentful.Clientbuilds every request asscheme://host[:port]/spaces/…, andClientConfigurationexposes no base-path hook, so it cannot address the/contentful/namespace the mock server used. The same handlers are now also mounted at the host root.contentful.jshasbasePathand the Android app builds its URLs fromAppConfig.contentfulBaseUrl, so both keep using the prefixed mount unchanged. The iOS app can now differ from a production integration only byhostandsecure = false, instead of installing aURLProtocolshim.Skip
The content-type query parsed
skipand then ignored it while echoing a skip of zero back, so any page after the first returned the first page again and a paging consumer accumulated duplicates. The clients that page this way are the native ones:fetchAllEntriesin the iOS SDK and the URL built by the AndroidPreviewContentfulClientboth walkskip, andcontentful.swiftpages the same way.With 45 fixture entries and a 100-item batch nothing pages today, so this is a latent fidelity gap rather than a live failure.
Scope and compatibility
Both routes are additive and no existing consumer changes mounts. The first consumer of the new behavior is the iOS reference app's move to
contentful.swiftin #429, which is stacked on this branch.Per
lib/mocks/AGENTS.md, the/localesroute and root mount have no in-tree consumer until #429 lands. They are included here so #429 does not carry mock infrastructure changes.Validation
pnpm --filter mocks typecheck— clean. Perlib/mocks/AGENTS.mdthis is the only direct validation for mocks.pnpm lint— clean.pnpm format:check— clean./localesorskip > 0today. Theskipchange is a no-op forskip=0callers, and web consumers keep the prefixed mount.🤖 Generated with Claude Code