Is your feature request related to a problem? Please describe.
Fido2UserMetrics (jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2UserMetrics.java) is the per-user ORM entity backing every FIDO2 user-adoption metric stored under ou=fido2-user-metrics,o=jans. Unlike the earlier DTOs, this class carries real computed logic that downstream services and the REST analytics endpoints depend on:
incrementRegistrations(boolean), incrementAuthentications(boolean), incrementFallbackEvents() — counter mutations with side effects on lastActivityDate/lastUpdated.
getRegistrationSuccessRate(), getAuthenticationSuccessRate(), getOverallSuccessRate(), getFallbackRate() — null-safe ratio computations that must protect against divide-by-zero (otherwise Double.NaN / Double.POSITIVE_INFINITY leak into stored aggregates and JSON responses).
updateEngagementLevel() — threshold-based bucketing into HIGH (≥50 ops), MEDIUM (≥10), LOW (otherwise). Off-by-one mistakes at the boundaries silently re-bucket every user.
updateAdoptionStage() — threshold-based bucketing into NEW (0 regs), LEARNING (1), ADOPTED (2–5), EXPERT (>5).
isNewUser(), isActiveUser() — 30-day window logic anchored to System.currentTimeMillis(), with null-date guards.
- Custom
equals(Object) / hashCode() based on the (userId, username, firstRegistrationDate) triple — used by collections/dedup in Fido2UserMetricsService.
- Two constructors with non-trivial initialization: the no-arg constructor zeroes counters, sets
isActive=true, and stamps lastUpdated; the 2-arg constructor additionally assigns a random UUID to id.
None of this is tested today. This is the first issue in the rollout that exercises real computation rather than getter/setter wiring, and the per-method consequences of a silent regression are large (corrupted stored aggregates, mis-bucketed engagement reports, NaN leakage through the JSON API).
This is step 5 of the FIDO2 metrics test rollout (step 1 added Fido2MetricTypeTest; step 2 added Fido2MetricsConstantsTest; step 3 added Fido2MetricsDataTest; step 4 added UserMetricsUpdateRequestTest).
Describe the solution you'd like
Add a new JUnit 5 test class:
- File:
jans-fido2/model/src/test/java/io/jans/fido2/model/metric/Fido2UserMetricsTest.java
The test should cover:
- Default constructor initial state: all six counters (
totalRegistrations, totalAuthentications, successfulRegistrations, successfulAuthentications, failedRegistrations, failedAuthentications, fallbackEvents) are 0; isActive is true; lastUpdated is non-null and within ±1s of System.currentTimeMillis() at construction.
- 2-arg constructor:
new Fido2UserMetrics("u-1", "alice") sets userId/username correctly, generates a non-null id parseable as a UUID, and inherits the default-constructor initialization (counters zeroed, isActive=true).
incrementRegistrations(true): totalRegistrations goes 0→1, successfulRegistrations goes 0→1, failedRegistrations stays at 0, lastActivityDate and lastUpdated are both non-null and ~now.
incrementRegistrations(false): mirrors the above but increments failedRegistrations, not successfulRegistrations.
incrementAuthentications(true/false): symmetric to the registration tests, on the authentication counters.
incrementFallbackEvents(): fallbackEvents 0→1, plus timestamp side effects.
getRegistrationSuccessRate() happy path: with totalRegistrations=10, successfulRegistrations=7, returns 0.7 (delta 1e-9).
getRegistrationSuccessRate() zero-and-null guards: returns 0.0 (not NaN) when totalRegistrations is null, and also when it is 0.
getAuthenticationSuccessRate() happy path + guards: parallel to the registration cases.
getOverallSuccessRate() cross-counter math: with totalRegistrations=4, successfulRegistrations=2, totalAuthentications=6, successfulAuthentications=3, returns 5/10 = 0.5. With both totals null/zero, returns 0.0. With one side null, treats it as 0 and computes from the other side.
getFallbackRate(): happy path with mixed counters; 0.0 when both totals are zero/null; null fallbackEvents is treated as 0 (not NPE).
updateEngagementLevel() boundary check at 50: totals 49 → MEDIUM, totals 50 → HIGH. Catches > vs >= regressions on the high threshold.
updateEngagementLevel() boundary check at 10: totals 9 → LOW, totals 10 → MEDIUM.
updateEngagementLevel() null totals: returns LOW (the null-safe branch must treat null as 0).
updateAdoptionStage() boundaries: 0 regs → NEW, 1 → LEARNING, 5 → ADOPTED, 6 → EXPERT; null → NEW.
isNewUser(): true when firstRegistrationDate is 29 days ago, false when 31 days ago, false when null. Use a fixed Date(System.currentTimeMillis() - Nd) rather than wallclock comparisons inside the assertion.
isActiveUser(): mirrors isNewUser() against lastActivityDate.
equals() / hashCode() contract: two instances with identical (userId, username, firstRegistrationDate) are equal and share a hash; differing in any of the three breaks equality; differing in unrelated fields (e.g. counters, engagementLevel) preserves equality (the equality is identity-tuple-only by design). Includes reflexive (x.equals(x)) and null/wrong-type (x.equals(null), x.equals("string")) cases.
Conventions to follow (same as Fido2MetricTypeTest, Fido2MetricsConstantsTest, Fido2MetricsDataTest, UserMetricsUpdateRequestTest):
- JUnit 5 (
org.junit.jupiter.api).
- No Mockito needed — pure entity behavior.
- No additional dependencies —
junit-jupiter-api and junit-jupiter-engine test-scope deps are already declared in jans-fido2/model/pom.xml; jans-orm-model (for the Entry superclass) is already on the model module's compile classpath.
- Group related cases into one
@Test per behavior (e.g., testGetOverallSuccessRateHappyPath, testGetOverallSuccessRateZeroAndNullGuards, testUpdateEngagementLevelBoundary50, testEqualsContractForIdentityTuple).
- For timestamp side-effect assertions, use a small wall-clock window (e.g., capture
before = System.currentTimeMillis() before the call, after = System.currentTimeMillis() after, assert lastUpdated.getTime() falls in [before, after]). Avoid Thread.sleep.
Acceptance criteria:
Describe alternatives you've considered
- Splitting this into smaller sub-issues (one per method group: counters, rates, levels/stages, equals): rejected — all the logic lives in one class with overlapping setup (the counters drive the rates which drive the levels), and splitting would either duplicate the setup or force artificial test-file fragmentation. The class file is one cohesive unit and should be covered as one.
- Skipping boundary tests and only covering happy paths: rejected — the entire risk profile here is off-by-one in the threshold ladders and
NaN from unguarded division. Happy paths catch neither.
- Using a clock-injection abstraction to make
isNewUser() / isActiveUser() deterministic: rejected as out of scope — the method takes its time from System.currentTimeMillis() directly and refactoring that signature is a separate change. The proposed approach (fixed Date offsets relative to current time) is robust enough for a unit test.
- Asserting on exact
lastUpdated == new Date() equality: rejected — flaky; uses the small-window assertion described above instead.
Additional context
Roadmap position: step 5 of the FIDO2 metrics test rollout — and the first step that exercises computed behavior rather than value mapping. Next planned step is to strengthen the existing MetricServiceTest (jans-fido2/server/src/test/java/io/jans/fido2/service/shared/MetricServiceTest.java), replacing its assertDoesNotThrow smoke checks with real verify() / ArgumentCaptor assertions. After that, the rollout moves into the server-side service classes (Fido2MetricsService will be split across several issues — store, query, aggregate, analytics).
Same Maven module (jans-fido2/model) as issues #1–#4; with the surefire/JUnit alignment fix already in place, no further pom workarounds are required.
Suggested label: kind-test.
Is your feature request related to a problem? Please describe.
Fido2UserMetrics(jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2UserMetrics.java) is the per-user ORM entity backing every FIDO2 user-adoption metric stored underou=fido2-user-metrics,o=jans. Unlike the earlier DTOs, this class carries real computed logic that downstream services and the REST analytics endpoints depend on:incrementRegistrations(boolean),incrementAuthentications(boolean),incrementFallbackEvents()— counter mutations with side effects onlastActivityDate/lastUpdated.getRegistrationSuccessRate(),getAuthenticationSuccessRate(),getOverallSuccessRate(),getFallbackRate()— null-safe ratio computations that must protect against divide-by-zero (otherwiseDouble.NaN/Double.POSITIVE_INFINITYleak into stored aggregates and JSON responses).updateEngagementLevel()— threshold-based bucketing intoHIGH(≥50 ops),MEDIUM(≥10),LOW(otherwise). Off-by-one mistakes at the boundaries silently re-bucket every user.updateAdoptionStage()— threshold-based bucketing intoNEW(0 regs),LEARNING(1),ADOPTED(2–5),EXPERT(>5).isNewUser(),isActiveUser()— 30-day window logic anchored toSystem.currentTimeMillis(), with null-date guards.equals(Object)/hashCode()based on the (userId,username,firstRegistrationDate) triple — used by collections/dedup inFido2UserMetricsService.isActive=true, and stampslastUpdated; the 2-arg constructor additionally assigns a randomUUIDtoid.None of this is tested today. This is the first issue in the rollout that exercises real computation rather than getter/setter wiring, and the per-method consequences of a silent regression are large (corrupted stored aggregates, mis-bucketed engagement reports, NaN leakage through the JSON API).
This is step 5 of the FIDO2 metrics test rollout (step 1 added
Fido2MetricTypeTest; step 2 addedFido2MetricsConstantsTest; step 3 addedFido2MetricsDataTest; step 4 addedUserMetricsUpdateRequestTest).Describe the solution you'd like
Add a new JUnit 5 test class:
jans-fido2/model/src/test/java/io/jans/fido2/model/metric/Fido2UserMetricsTest.javaThe test should cover:
totalRegistrations,totalAuthentications,successfulRegistrations,successfulAuthentications,failedRegistrations,failedAuthentications,fallbackEvents) are0;isActiveistrue;lastUpdatedis non-null and within ±1s ofSystem.currentTimeMillis()at construction.new Fido2UserMetrics("u-1", "alice")setsuserId/usernamecorrectly, generates a non-nullidparseable as a UUID, and inherits the default-constructor initialization (counters zeroed,isActive=true).incrementRegistrations(true):totalRegistrationsgoes 0→1,successfulRegistrationsgoes 0→1,failedRegistrationsstays at 0,lastActivityDateandlastUpdatedare both non-null and ~now.incrementRegistrations(false): mirrors the above but incrementsfailedRegistrations, notsuccessfulRegistrations.incrementAuthentications(true/false): symmetric to the registration tests, on the authentication counters.incrementFallbackEvents():fallbackEvents0→1, plus timestamp side effects.getRegistrationSuccessRate()happy path: withtotalRegistrations=10,successfulRegistrations=7, returns0.7(delta1e-9).getRegistrationSuccessRate()zero-and-null guards: returns0.0(notNaN) whentotalRegistrationsisnull, and also when it is0.getAuthenticationSuccessRate()happy path + guards: parallel to the registration cases.getOverallSuccessRate()cross-counter math: withtotalRegistrations=4,successfulRegistrations=2,totalAuthentications=6,successfulAuthentications=3, returns5/10 = 0.5. With both totals null/zero, returns0.0. With one side null, treats it as0and computes from the other side.getFallbackRate(): happy path with mixed counters;0.0when both totals are zero/null; nullfallbackEventsis treated as0(not NPE).updateEngagementLevel()boundary check at 50: totals 49 →MEDIUM, totals 50 →HIGH. Catches>vs>=regressions on the high threshold.updateEngagementLevel()boundary check at 10: totals 9 →LOW, totals 10 →MEDIUM.updateEngagementLevel()null totals: returnsLOW(the null-safe branch must treat null as 0).updateAdoptionStage()boundaries: 0 regs →NEW, 1 →LEARNING, 5 →ADOPTED, 6 →EXPERT;null→NEW.isNewUser():truewhenfirstRegistrationDateis 29 days ago,falsewhen 31 days ago,falsewhennull. Use a fixedDate(System.currentTimeMillis() - Nd)rather than wallclock comparisons inside the assertion.isActiveUser(): mirrorsisNewUser()againstlastActivityDate.equals()/hashCode()contract: two instances with identical (userId,username,firstRegistrationDate) are equal and share a hash; differing in any of the three breaks equality; differing in unrelated fields (e.g. counters,engagementLevel) preserves equality (the equality is identity-tuple-only by design). Includes reflexive (x.equals(x)) and null/wrong-type (x.equals(null),x.equals("string")) cases.Conventions to follow (same as
Fido2MetricTypeTest,Fido2MetricsConstantsTest,Fido2MetricsDataTest,UserMetricsUpdateRequestTest):org.junit.jupiter.api).junit-jupiter-apiandjunit-jupiter-enginetest-scope deps are already declared injans-fido2/model/pom.xml;jans-orm-model(for theEntrysuperclass) is already on the model module's compile classpath.@Testper behavior (e.g.,testGetOverallSuccessRateHappyPath,testGetOverallSuccessRateZeroAndNullGuards,testUpdateEngagementLevelBoundary50,testEqualsContractForIdentityTuple).before = System.currentTimeMillis()before the call,after = System.currentTimeMillis()after, assertlastUpdated.getTime()falls in[before, after]). AvoidThread.sleep.Acceptance criteria:
mvn -pl model -am testfromjans-fido2/passes locally.Thread.sleep, no I/O).src/main.Describe alternatives you've considered
NaNfrom unguarded division. Happy paths catch neither.isNewUser()/isActiveUser()deterministic: rejected as out of scope — the method takes its time fromSystem.currentTimeMillis()directly and refactoring that signature is a separate change. The proposed approach (fixedDateoffsets relative to current time) is robust enough for a unit test.lastUpdated == new Date()equality: rejected — flaky; uses the small-window assertion described above instead.Additional context
Roadmap position: step 5 of the FIDO2 metrics test rollout — and the first step that exercises computed behavior rather than value mapping. Next planned step is to strengthen the existing
MetricServiceTest(jans-fido2/server/src/test/java/io/jans/fido2/service/shared/MetricServiceTest.java), replacing itsassertDoesNotThrowsmoke checks with realverify()/ArgumentCaptorassertions. After that, the rollout moves into the server-side service classes (Fido2MetricsServicewill be split across several issues — store, query, aggregate, analytics).Same Maven module (
jans-fido2/model) as issues #1–#4; with the surefire/JUnit alignment fix already in place, no further pom workarounds are required.Suggested label:
kind-test.