fix(user): replace Math.abs(hashCode()) in AppUser.computeColor (negative on Integer.MIN_VALUE) #471
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
SpotBugs (priority 1, CORRECTNESS) reports:
Math.abs(int)returns its argument unchanged when the argument isInteger.MIN_VALUE, because-Integer.MIN_VALUEoverflows back to itself.hashCode()can returnInteger.MIN_VALUEfor some UUIDs; with a 1.5K-user-base it's unlikely but not impossible, and the resulting color computation would either throw (modulo by zero on a sign-bit branch) or produce a negative hue.This is the only priority-1 SpotBugs finding in the entire codebase — worth fixing as a standalone issue since the rest of the SAST output is Lombok-EI noise to be suppressed (handled in the new CI security-gates issue).
Approach
Use
Math.floorModinstead, which always returns a non-negative residue:Math.floorModis Java-8+, has no overflow case, and handles negative dividends correctly. The output range becomes[0, 359]regardless of input sign — same intended behavior, no edge case.If
computeColorreturns hex (#RRGGBB) directly rather than HSL, the fix shape is the same — just substitute floorMod wherever the hashCode is reduced.Critical files
backend/src/main/java/org/raddatz/familienarchiv/user/AppUser.java(line ~28)backend/src/test/java/.../user/AppUserTest.java— add a targeted regression test usingInteger.MIN_VALUE-hashing inputVerification
hashCode()returnsInteger.MIN_VALUE(or mock the hash). AssertcomputeColorreturns a non-negative value in[0, 359]../mvnw test -Dtest=AppUserTeststill passes.Acceptance criteria
AppUser.computeColorusesMath.floorMod(or equivalent overflow-safe arithmetic).RV_ABSOLUTE_VALUE_OF_HASHCODEfor this method.Integer.MIN_VALUEcase.Effort
XS — 15 minutes.
Risk if not addressed
Cosmetic glitch (negative color value, exception thrown for an unlucky UUID). Real-world impact is one user with a broken avatar tile somewhere in the future; not load-bearing, but it's the kind of bug that's free to fix now and annoying to debug later.
Tracked in audit doc as F-34 (Medium) — new dynamic finding from SpotBugs SAST.
🏗️ Markus Keller — Application Architect
Observations
AppUser.computeColoris a pure deterministic function — given the same UUID it always returns the same palette entry. The bug is entirely self-contained in one static method and its single call site (deriveColor()). No domain boundary or architectural concern is implicated.Math.floorMod) is a one-line drop-in with identical semantics for all inputs exceptInteger.MIN_VALUE. There is no schema change, no migration, no module-boundary effect.colorfield is derived fromidvia a JPA lifecycle callback (@PrePersist,@PreUpdate,@PostLoad). This means the value is re-derived on every load if it was empty at persist time — which is the correct pattern for a computed field. No architectural concern here.AppUserTesttests determinism, stability, and distribution — but not theInteger.MIN_VALUEboundary. The test suite has a gap at exactly the edge case that matters.Recommendations
Integer.MIN_VALUEinAppUserTestas a plain JUnit unit test — no Spring context needed, no Testcontainers, no DB. The method isstatic.computeColorout ofAppUserinto a utility class — the coloring logic is the user domain's responsibility and it belongs in the entity. Moving it now would be over-engineering a one-liner fix.Open Decisions
👨💻 Felix Brandt — Fullstack Developer
Observations
The bug is on line 91 of
AppUser.java:Math.abs(Integer.MIN_VALUE)returnsInteger.MIN_VALUE(negative), so% PALETTE.length(8) produces a negative index, which throwsArrayIndexOutOfBoundsExceptionat runtime. The issue description is correct.The palette is 8 hex strings (
#7a4f9a,#5a8a6a, etc.), not an HSL hue range. The fix therefore operates on modulo-8, not modulo-360. The issue body mentions 360 as an example but that does not apply here — the concrete fix shape is:The existing
AppUserTesthas three tests: determinism, stability, and distribution. None of them construct a UUID whosehashCode()isInteger.MIN_VALUE. The regression test is missing.The
deriveColor()lifecycle method only setscolorif it's currently null or empty. Once a user's color is persisted, it's stable even if the palette changes — this is correct behaviour for avatar identity. The fix will not change already-persisted values.TDD sequence for this fix:
hashCode() == Integer.MIN_VALUE(or use a mock/spy — see below) and asserts the result is a valid palette color. Watch it throwArrayIndexOutOfBoundsException.Math.abs(id.hashCode()) % PALETTE.length→Math.floorMod(id.hashCode(), PALETTE.length).floorModis self-documenting).Getting a UUID whose
hashCode()is exactlyInteger.MIN_VALUEis non-trivial. The simplest approach: overridehashCode()via a test subclass isn't available onUUID(it's final). Instead, spy onid.hashCode()or simply verify the overflow-safe property directly:Alternatively, brute-force a UUID whose hashCode is
Integer.MIN_VALUE(doable but slow) — not worth it; the arithmetic test is sufficient.Recommendations
computeColor. This proves the test was meaningful.Math.floorMod(id.hashCode(), PALETTE.length)— notMath.abswith& 0x7fffffffbit-masking (that's an older Java idiom and less readable).AppUserTest— it's a unit test, no Spring context or DB needed.computeColor_returnsValidPaletteIndexForIntegerMinValueHash../mvnw test -Dtest=AppUserTestto confirm all four tests green before opening the PR.Open Decisions
🔒 Nora "NullX" Steiner — Security Engineer
Observations
computeColormethod is called fromderiveColor()which runs during@PrePersist,@PreUpdate, and@PostLoad. An uncaughtArrayIndexOutOfBoundsExceptionduring a JPA lifecycle callback can cause an entity save to fail with a 500 and, depending on exception handling, may leak a stack trace to the API response. TheGlobalExceptionHandlershould catch this, but it's better to not rely on that as a safety net for arithmetic errors.safeColorguard inContributorStack.svelte(/^#[0-9a-fA-F]{6}$/.test(color) ? color : '#8c9aa3') shows the frontend already has a defensive fallback for invalid color values. This suggests the team was aware the color field could be empty or invalid in some cases — the backend guard should match this defensive posture rather than relying on the frontend safety net.RV_ABSOLUTE_VALUE_OF_HASHCODEis a known rule for this exact pattern. It's good that SAST caught it. The fix should be accompanied by a SpotBugs suppression removal (i.e., don't suppress it — fix it) plus CI enforcement so this class of finding can't accumulate again.Recommendations
Math.floorMod— this is the correct, idiomatic Java solution and makes the intent explicit.floorModis used instead ofMath.abs:GlobalExceptionHandlermapsArrayIndexOutOfBoundsExceptionto a 500 without leaking the stack trace (it should, but worth confirming). This is a separate issue but this bug makes the handler's behaviour relevant.safeColorguard is good defensive programming — keep it regardless of this fix.Open Decisions
🧪 Sara Holt — QA Engineer
Observations
AppUserTestcovers three behaviours: determinism for a specific UUID, stability across calls, and colour distribution across random UUIDs. None of these three tests would fail today becauseInteger.MIN_VALUEis not produced by any of the randomly generated UUIDs in the distribution test (extremely unlikely withjava.util.UUID.randomUUID()). The test suite provides a false sense of safety for this exact edge case.AppUser.computeColoris a pure static method with no dependencies. This makes it maximally testable: no mocks, no Spring context, no DB, sub-millisecond execution. The regression test belongs at the unit layer.Math.floorMod(x, 8)returns the same value asMath.abs(x) % 8for all values exceptInteger.MIN_VALUE. Existing tests should remain green.staticmethod.Recommendations
Write two new tests in
AppUserTest:Test 1 — direct arithmetic boundary verification:
Test 2 — end-to-end via computeColor with a known-bad UUID (if obtainable) or a UUID whose hashCode is mocked:
Since
UUIDis final in Java, you cannot subclass or spy on it with Mockito. The cleanest approach is to assert the return value is in the palette for all integer hash values includingMIN_VALUEby testing the arithmetic directly (Test 1), plus a smoke test that the method never throws:This won't reliably hit
Integer.MIN_VALUEbut the direct arithmetic test (Test 1) closes that gap.Run
./mvnw test -Dtest=AppUserTestand confirm all tests pass before opening the PR.The acceptance criterion "SpotBugs no longer reports
RV_ABSOLUTE_VALUE_OF_HASHCODE" should be verified in CI — add that check to the PR description so the reviewer knows to look at the SpotBugs report.Open Decisions
📋 Elicit — Requirements Engineer
Observations
computeColor) and one file. There is no scope creep risk.Math.floorMod, the result is identical toMath.abs(x) % nfor all inputs exceptInteger.MIN_VALUE. The smoke check is appropriate but the wording "don't all change" is vague — a single unlucky UUID could have changed colour (if its hash wasInteger.MIN_VALUE, which is the bug case anyway). Better: "No visible colour change for any currently-active user account."ArrayIndexOutOfBoundsExceptionthrown during JPA lifecycle callback, (b) a negative index that somehow resolves to a wrong color. In Java, negative array indexing always throws — so failure mode (b) doesn't exist at runtime; it's always (a) the exception. The issue could be clearer about this.Recommendations
ArrayIndexOutOfBoundsException(not a silent wrong colour) — this helps whoever implements it write the right test assertion (doesNotThrowAnyExceptionvs. value assertion).PALETTE.length(currently 8) should be used in the fix, not a hardcoded360— the issue body uses 360 as a hypothetical HSL example, which does not match the actual code. A future implementer who reads only the "after" snippet in the issue body might use the wrong divisor.Open Decisions
🚀 Tobias Wendt — DevOps & Platform Engineer
Observations
./mvnw testcommand runs SpotBugs as part of the verify phase (if configured). Worth confirming that the SpotBugs Maven plugin is wired up and that re-running./mvnw verifyafter the fix actually produces a clean SpotBugs report — the acceptance criterion depends on this.colorfield is derived on load if empty; existing persisted values are unchanged; no Flyway migration required.Recommendations
./mvnw verify(not just./mvnw test) and confirm the SpotBugs report no longer containsRV_ABSOLUTE_VALUE_OF_HASHCODE. Include this in the PR checklist.Open Decisions
🎨 Leonie Voss — UI/UX & Accessibility
Observations
colorfield onAppUserfeeds three visible UI surfaces:ContributorStack.svelte(avatar circles in document activity feeds),DashboardFamilyPulse.svelte(contributor dots on the dashboard), andDashboardResumeStrip.svelte(collaborator avatars on resume cards).ContributorStack.sveltealready has a defensivesafeColorguard:return /^#[0-9a-fA-F]{6}$/.test(color) ? color : '#8c9aa3'. This means if the current bug caused an exception and thecolorfield was never persisted (left as empty string""), the frontend silently shows a neutral grey (#8c9aa3). Users may already be experiencing grey avatars for the unlucky user — they just don't know why.#7a4f9a,#5a8a6a,#3060b0,#a0522d,#c0446e,#c17a00,#0e7490,#1d4ed8) are used on small circles (22×22px) as background with white initials text. The contrast ratios for white text on these backgrounds should all pass WCAG AA (3:1 for non-text, 4.5:1 for text at this size). Worth spot-checking#c17a00(amber) — it's the borderline case for white text contrast.Integer.MIN_VALUE. For the rare affected user, their avatar will show a correct palette color instead of a 500 error / grey fallback. This is purely a visual fix — no layout, interaction, or accessibility implications.Recommendations
/admin/users) to confirm all user avatar circles show a palette color (not grey). If any appear grey after the fix, that suggests a persisted emptycolorvalue that wasn't re-derived — the@PostLoadcallback should handle this, but worth verifying.#c17a00(the amber palette entry) against white text:#c17a00on white is approximately 2.8:1 — this fails WCAG AA for normal text. However, the initials are displayed as white text on the#c17a00background at 10px bold in a 22px circle. At 10px, this is below the "large text" threshold (14pt bold / 18px), so the 4.5:1 ratio applies —#c17a00with white text fails at this size. Consider replacing#c17a00with a darker amber (e.g.,#a06800) in a follow-up issue. This is a separate accessibility concern from the bug fix.Open Decisions
#c17a00with white initials at 10px bold fails WCAG AA. Should this be fixed in the same PR (one-line palette swap) or tracked as a separate accessibility issue? Recommend a separate issue to keep this PR atomic, but flag it now so it's not forgotten.Decision Queue
One genuine tradeoff was surfaced across the 7 persona reviews. Everything else had a clear winner.
Accessibility — Avatar palette contrast
Raised by: Leonie Voss (@leonievoss)
#c17a00(amber) inAppUser.PALETTEis used as the background of 22×22px avatar circles with white initials at 10px bold. White on#c17a00is approximately 2.8:1, which fails WCAG AA for normal text (requires 4.5:1 at sizes below 14pt bold / 18px).Option A — Fix in this PR (one-line palette swap):
Replace
#c17a00with a darker amber (e.g.,#a06800, ~4.6:1 with white) in the samePALETTEarray change. Keeps the PR atomic relative to the accessibility fix. Downside: any user currently persisted with amber will get a different color on next load (sincederiveColorre-derives when empty — but persisted colors won't change unless reset).Option B — Track as a separate issue:
Keep this PR purely the
Math.floorModcorrectness fix. Open a newa11yissue for the amber contrast problem. Cleaner commit history, easier revert if needed.Recommended default: Option B. The scope of this issue is the SpotBugs P1 correctness finding. The contrast problem is real but orthogonal. A dedicated accessibility issue for the avatar palette ensures it gets triaged on its own merits and doesn't delay this XS fix.
Owner to decide: Marcel