Skip to content

MTM-5: REST<->GraphQL bookmark parity audit + cross-surface equivalence/E2E tests#185

Merged
devin-ai-integration[bot] merged 1 commit into
feat/reading-listfrom
devin/1782445308-mtm-5-rest-graphql-parity
Jun 26, 2026
Merged

MTM-5: REST<->GraphQL bookmark parity audit + cross-surface equivalence/E2E tests#185
devin-ai-integration[bot] merged 1 commit into
feat/reading-listfrom
devin/1782445308-mtm-5-rest-graphql-parity

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 26, 2026

Copy link
Copy Markdown

Summary

Final wave of the Reading List epic (MTM-1). This is a verification/parity story, not a redesign — the bookmark feature already exists end-to-end on both surfaces (MTM-2/3/4/6). This PR:

  1. MTM-22 — Parity audit: confirms the bookmark feature is consistent across REST and GraphQL. No gaps found; no production code changed.
  2. MTM-23 — Cross-surface equivalence + E2E tests: one new test class, BookmarkCrossSurfaceTest, that drives the same application stack (real repositories + DB) through both REST (MockMvc) and GraphQL (DgsQueryExecutor) and asserts equivalent behavior.

Parity audit (MTM-22)

Concern REST GraphQL Consistent?
Add POST /articles/{slug}/bookmark (ArticleBookmarkApi) bookmarkArticle(slug) (ArticleMutation)
Remove DELETE /articles/{slug}/bookmark unbookmarkArticle(slug)
List GET /articles/bookmarked (offset/limit) → findUserBookmarks Query.bookmarkedArticles(first/after/last/before) (cursor) → findUserBookmarksWithCursor ✅ (same data/membership/order; see D4)
Per-article flag ArticleData.bookmarked (JSON bookmarked) Article.bookmarked: Boolean!
Ordering newest-bookmarked-first (article_bookmarks.created_at desc) newest-bookmarked-first (same column)
Auth (add/remove/list) anonymous → 401 (WebSecurityConfig requires auth on GET /articles/bookmarked; @AuthenticationPrincipal) anonymous → AuthenticationException (SecurityUtil.getCurrentUser().orElseThrow(...))
Privacy list/flag scoped to current user (ArticleBookmarksReadService filters by user_id) same read service / same scoping
Independence from favorites bookmark ops touch only article_bookmarks; never favorited/favoritesCount same

Both surfaces resolve the bookmarked flag and reading-list membership through the same ArticleQueryService / ArticleBookmarksReadService, so the underlying data, membership, ordering, and privacy rules are shared by construction.

D4 (intentional, accepted): REST uses offset/limit; GraphQL uses Relay cursor connections. "Consistent/equivalent" means same underlying data, membership, ordering, auth/privacy, and per-article flag — expressed via each surface's native paging idiom. This divergence is preserved, not "fixed".

Tests (MTM-23)

src/test/java/io/spring/BookmarkCrossSurfaceTest@SpringBootTest @AutoConfigureMockMvc @Transactional (real stack, rollback per test, matching the repo's transactional test convention). The article_bookmarks table is created in @BeforeEach because the test profile runs only Flyway V1 (flyway.target=1), mirroring the existing bookmark infra tests. Ordering is made deterministic by stamping controlled created_at values after each write.

7 cases:

  • E2E: bookmark via REST → visible on GraphQL (Article.bookmarked=true + in bookmarkedArticles); remove via REST → gone on GraphQL. And the reverse (bookmark via GraphQL mutation → visible via REST GET /articles/bookmarked and REST bookmarked=true; remove via GraphQL → gone on REST).
  • Equivalence: for the same user/state (writes mixed across surfaces), REST and GraphQL return the same set and the same newest-first order ([a3, a2, a1]).
  • Auth: anonymous rejected on both (REST 401 / GraphQL errors) for add and list.
  • Privacy: one user never sees another's bookmarks (list + per-article flag) on either surface.
  • Independence (regression): bookmarking leaves favorited/favoritesCount unchanged on both; favoriting leaves bookmarked false and the article absent from the reading list on both.
// shape of the equivalence assertion
restPost("/articles/{slug}/bookmark", token);          // REST write
graphqlMutation("bookmarkArticle", slug2, user);       // GraphQL write
setBookmarkTime(a1, "..01"); setBookmarkTime(a2, "..02"); setBookmarkTime(a3, "..03");
assertEquals([a3,a2,a1], restBookmarkedSlugs(token));        // offset/limit
assertEquals([a3,a2,a1], graphqlBookmarkedSlugs(user));      // cursor

Verification gate (Java 11) — green

./gradlew clean build -x test                              -> BUILD SUCCESSFUL
./gradlew clean test -x jacocoTestCoverageVerification     -> BUILD SUCCESSFUL  (133 tests, 0 failures)
./gradlew spotlessCheck                                    -> BUILD SUCCESSFUL

Link to Devin session: https://partner-workshops.devinenterprise.com/sessions/c8f2c351466f4736ad7c012a3dcda9f2


Open in Devin Review

@mbatchelor81 mbatchelor81 self-assigned this Jun 26, 2026
@devin-ai-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@devin-ai-integration devin-ai-integration Bot merged commit a9f175f into feat/reading-list Jun 26, 2026
3 checks passed
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