Skip to content

Commit c85bda5

Browse files
committed
docs: design for JSON Patch side-effect regression tests
Spec for multi-entity integration + web-api coverage answering David's system-wide PATCH concern on PR #24489 / DHIS2-21852.
1 parent 707de08 commit c85bda5

1 file changed

Lines changed: 185 additions & 0 deletions

File tree

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Design: JSON Patch side-effect regression tests (PR #24489 / DHIS2-21852)
2+
3+
Date: 2026-07-23
4+
PR: https://github.com/dhis2/dhis2-core/pull/24489
5+
Branch: `fix-userrole-jsonpatch-lazy-members`
6+
Author: Morten (netroms)
7+
8+
## Problem
9+
10+
PR #24489 changes `JsonPatchManager` system-wide: during `apply`, non-owner collection properties that are not referenced by any patch path are omitted from serialization so lazy Hibernate collections (e.g. `UserRole.members`) are not initialized on scalar PATCH.
11+
12+
Review feedback:
13+
14+
1. **Jan (retracted in weekly meeting)** — filter mechanics complexity. Out of scope for this follow-up.
15+
2. **David** — worried about unknown side effects because the change applies to all PATCHes, not only `/userRoles`. Asked whether PUT has the same issue.
16+
17+
Existing PR tests are almost entirely UserRole-focused. That is not enough to affirm the generic rule.
18+
19+
## PUT conclusion (no code change)
20+
21+
PUT does **not** share this bug path.
22+
23+
| | PATCH `/{uid}` | PUT `/{uid}` |
24+
|---|---|---|
25+
| Entry | `AbstractCrudController.patchObject``doPatch` | `AbstractCrudController.putJsonObject` |
26+
| Core | `jsonPatchManager.apply(patch, persistedObject)` | `deserializeJsonEntity(request)` only |
27+
| Then | `importMetadata(UPDATE)` | `importMetadata(UPDATE)` |
28+
29+
Evidence: `AbstractCrudController` PUT ~393–422 never calls `JsonPatchManager`. `UserRoleController` does not override PUT/PATCH.
30+
31+
PUT can still be heavy if the client sends a large `users` array; that is payload size, not live lazy-collection hydration via `valueToTree` / `handleCollectionUpdates`.
32+
33+
No PUT regression tests are required for merge. Document the distinction in the PR reply.
34+
35+
## Fix rule under test (unchanged production behavior)
36+
37+
Skip a collection property only when all hold:
38+
39+
1. `property.isCollection()`
40+
2. `!property.isOwner()`
41+
3. patch paths contain neither `property.getName()` nor `property.getCollectionName()`
42+
43+
Owner collections must always serialize: metadata import UPDATE would clear them if omitted.
44+
45+
Note: `UserGroup.users` (`members`) is **owner** (hbm has no `inverse`). The PR description implying UserGroup members benefit from the skip is slightly wrong; owner collections are still fully walked. Tests must document that contrast.
46+
47+
## Goal
48+
49+
Add integration + focused web-api tests that lock the skip rule on multiple entity types so David's system-wide side-effect concern is answered with evidence.
50+
51+
## Non-goals
52+
53+
- Production code changes (unless a test exposes a real bug)
54+
- PUT tests as merge gates
55+
- Gatling / perf expansion
56+
- Reworking Jan's filter-mechanics feedback
57+
- Schema-wide brute-force PATCH of every type
58+
59+
## Invariants
60+
61+
| ID | Invariant | Layer |
62+
|---|---|---|
63+
| I1 | Unreferenced **non-owner** collection stays lazy (`!Hibernate.isInitialized`) after scalar `jsonPatchManager.apply` | integration |
64+
| I2 | **Owner** collections still present on the patched object after scalar apply (not wiped by omit) | integration |
65+
| I3 | Explicit patch path targeting a non-owner collection still applies without throw (path-aware skip, not a ban) | integration (existing UserRole `/users`) |
66+
| I4 | HTTP PATCH of a scalar leaves unreferenced membership/children/groups unchanged on GET | web-api |
67+
| I5 | HTTP PATCH `replace /users` on UserRole does not change DB membership (inverse + no setter) | web-api |
68+
69+
## Representative types
70+
71+
| Entity | Collection | Owner? | Role in matrix |
72+
|---|---|---|---|
73+
| UserRole | `users` / field `members` | no | primary bug; already covered |
74+
| UserRole | `authorities` | yes | owner preserve; already covered |
75+
| OrganisationUnit | `children`, `users` | no | second fat inverse type |
76+
| User | `userGroups` (field `groups`) | no | common PATCH target; inverse |
77+
| User | `userRoles` | yes | owner must remain on patched user |
78+
| UserGroup | `users` (field `members`) | yes | must **not** be skipped; round-trip |
79+
| UserGroup | `managedByGroups` | no | skip when unreferenced |
80+
| DataElementGroup | `dataElements` / members | yes | owner wipe guard outside user domain |
81+
82+
## Test plan
83+
84+
### A. `JsonPatchManagerTest` (integration)
85+
86+
File: `dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/jsonpatch/JsonPatchManagerTest.java`
87+
88+
Keep existing PR tests:
89+
90+
- `testUserRoleScalarPatchDoesNotInitializeMembers` (I1)
91+
- `testUserRoleOwnerAuthoritiesPreservedOnNamePatch` (I2)
92+
- `testUserRoleUsersPathPatchDoesNotThrow` (I3)
93+
94+
Add:
95+
96+
1. **`testOrganisationUnitScalarPatchDoesNotInitializeInverseCollections`**
97+
- Setup: parent OU, 2 children, 1 user assigned to parent; `clearSession`; reload parent
98+
- Assert children/users lazy before apply
99+
- Patch: `replace /name`
100+
- Assert name updated; `!Hibernate.isInitialized(children)`; `!Hibernate.isInitialized(users)`
101+
102+
2. **`testUserScalarPatchDoesNotInitializeUserGroups`**
103+
- Setup: user with a userRole and membership in a userGroup; `clearSession`; reload user
104+
- Assert `groups` lazy before apply
105+
- Patch: `replace /firstName`
106+
- Assert firstName updated; `!isInitialized(groups)`; patched object still has non-empty `userRoles` (I2 owner)
107+
108+
3. **`testUserGroupScalarPatchKeepsOwnerUsersAndSkipsManagedBy`**
109+
- Setup: group with 2 users; optional managedByGroups link; `clearSession`; reload
110+
- Patch: `replace /name`
111+
- Assert patched name; patched `users`/`members` size == 2 (owner not omitted)
112+
- Assert `!isInitialized(managedByGroups)` when that collection was left lazy
113+
114+
4. **`testDataElementGroupNamePatchPreservesOwnerMembers`**
115+
- Setup: DEG with 2 data elements (same spirit as existing collection test)
116+
- Patch: `replace /name`
117+
- Assert patched name; member count still 2
118+
119+
Setup pattern: save graph → `clearSession()` → reload → assert lazy where relevant → `jsonPatchManager.apply` → assert. Match style of existing UserRole lazy test (DisplayName, Hibernate.isInitialized).
120+
121+
### B. Web-api: new `JsonPatchSideEffectControllerTest`
122+
123+
File: `dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/JsonPatchSideEffectControllerTest.java`
124+
125+
Base: `H2ControllerIntegrationTestBase`, `@Transactional`, same HTTP helpers as other controller tests.
126+
127+
Author: Morten (netroms) on the new file.
128+
129+
Tests:
130+
131+
1. **`testPatchOrganisationUnitNameKeepsChildren`**
132+
- Create parent + child OUs
133+
- `PATCH /organisationUnits/{parent}` replace `/name`
134+
- GET parent with `fields=name,children[id]` → new name; children size unchanged
135+
136+
2. **`testPatchUserGroupNameKeepsUsers`**
137+
- Create group with a user member
138+
- `PATCH /userGroups/{id}` replace `/name`
139+
- GET `fields=name,users[id]` → new name; users size unchanged
140+
141+
3. **`testPatchUserFirstNameKeepsUserGroups`**
142+
- User in a group
143+
- `PATCH /users/{id}?importReportMode=ERRORS` replace `/firstName`
144+
- GET `fields=firstName,userGroups[id]` → new firstName; userGroups size unchanged
145+
146+
4. **`testPatchUserRoleUsersPathDoesNotChangeMembership`**
147+
- Role with one assigned user (via owner side / existing helpers)
148+
- `PATCH /userRoles/{id}` `replace /users` with `[]`
149+
- GET `fields=users[id]` → still 1 user (I5)
150+
- Accept 200 with possible ERRORS_NOT_OWNER reporting; membership must not drop
151+
152+
Keep existing `UserControllerTest.testPatchUserRoleDescriptionKeepsUsers` on the PR (I4 for UserRole).
153+
154+
### C. PR reply (after tests green)
155+
156+
Reply to David covering:
157+
158+
1. System-wide concern addressed by multi-entity integration + web tests (list invariants I1–I5).
159+
2. PUT: different path; no `JsonPatchManager`; not the same issue.
160+
3. Owner vs non-owner: UserGroup.users is owner and is not skipped; test proves members retained.
161+
4. Explicit `/users` on UserRole does not persist membership (owner is `User.userRoles` / membership sub-resource).
162+
163+
## Implementation notes
164+
165+
- Work on branch `fix-userrole-jsonpatch-lazy-members`.
166+
- Tests only unless a failure reveals a product bug.
167+
- Prefer entity factories already used in `JsonPatchManagerTest` / web-api bases (`createOrganisationUnit`, `makeUser`, `createUserRole`, `createDataElementGroup`, etc.).
168+
- For UserGroup managedBy relationship, use existing domain APIs if cheap; if setup is awkward, still assert owner `users` preservation and drop managedBy lazy assert rather than block the suite.
169+
- Do not use `git add -A`; stage specific paths.
170+
- Update root `CHANGELOG.md` if the project log is in use for this workstream; otherwise note in PR body only.
171+
172+
## Acceptance criteria
173+
174+
1. New integration tests cover OU, User, UserGroup, DataElementGroup as specified.
175+
2. New web-api class covers OU, UserGroup, User, UserRole `/users` no-op.
176+
3. All new tests pass locally (integration + web-api modules as applicable).
177+
4. Existing PR UserRole tests still pass.
178+
5. No production code change required for green tests (ideal).
179+
6. Design/plan docs committed; PR comment ready for David.
180+
181+
## Out of scope follow-ups
182+
183+
- Optional PUT `/userRoles` without `users` body keeps members (reply ammo only).
184+
- Fixing UserGroup owner lazy cost on PATCH (separate perf ticket if needed).
185+
- Simplifying Jackson filter mechanics (Jan; retracted for now).

0 commit comments

Comments
 (0)