diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S3577.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S3577.json index ec7fce02570..26e2fe03d03 100644 --- a/its/autoscan/src/test/resources/autoscan/diffs/diff_S3577.json +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S3577.json @@ -1,6 +1,6 @@ { "ruleKey": "S3577", "hasTruePositives": true, - "falseNegatives": 46, + "falseNegatives": 47, "falsePositives": 0 } diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S8692.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8692.json new file mode 100644 index 00000000000..f7cae943320 --- /dev/null +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8692.json @@ -0,0 +1,6 @@ +{ + "ruleKey": "S8692", + "hasTruePositives": true, + "falseNegatives": 0, + "falsePositives": 0 +} \ No newline at end of file diff --git a/its/ruling/src/test/resources/commons-beanutils/java-S8692.json b/its/ruling/src/test/resources/commons-beanutils/java-S8692.json new file mode 100644 index 00000000000..b7a6f5f2447 --- /dev/null +++ b/its/ruling/src/test/resources/commons-beanutils/java-S8692.json @@ -0,0 +1,71 @@ +{ +"commons-beanutils:commons-beanutils:src/test/java/org/apache/commons/beanutils2/BeanUtilsBenchCase.java": [ +186, +190, +198, +202, +219, +223, +231, +235, +252, +256, +264, +268, +285, +289, +297, +301, +318, +322, +330, +334, +352, +356, +364, +368 +], +"commons-beanutils:commons-beanutils:src/test/java/org/apache/commons/beanutils2/BeanUtilsTestCase.java": [ +149 +], +"commons-beanutils:commons-beanutils:src/test/java/org/apache/commons/beanutils2/ConvertUtilsTestCase.java": [ +643 +], +"commons-beanutils:commons-beanutils:src/test/java/org/apache/commons/beanutils2/DynaBeanMapDecoratorTestCase.java": [ +50 +], +"commons-beanutils:commons-beanutils:src/test/java/org/apache/commons/beanutils2/DynaRowSetTestCase.java": [ +403 +], +"commons-beanutils:commons-beanutils:src/test/java/org/apache/commons/beanutils2/PropertyUtilsBenchCase.java": [ +175, +179, +187, +191, +208, +212, +220, +224, +241, +245, +253, +257 +], +"commons-beanutils:commons-beanutils:src/test/java/org/apache/commons/beanutils2/TestResultSet.java": [ +66 +], +"commons-beanutils:commons-beanutils:src/test/java/org/apache/commons/beanutils2/converters/DateConverterTestBase.java": [ +105, +379 +], +"commons-beanutils:commons-beanutils:src/test/java/org/apache/commons/beanutils2/converters/NumberConverterTestBase.java": [ +321, +344 +], +"commons-beanutils:commons-beanutils:src/test/java/org/apache/commons/beanutils2/converters/SqlTimestampConverterTestCase.java": [ +59 +], +"commons-beanutils:commons-beanutils:src/test/java/org/apache/commons/beanutils2/locale/converters/DateLocaleConverterTestCase.java": [ +443 +] +} diff --git a/its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S8692.json b/its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S8692.json new file mode 100644 index 00000000000..de997f41178 --- /dev/null +++ b/its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S8692.json @@ -0,0 +1,17 @@ +{ +"org.eclipse.jetty:jetty-project:jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java": [ +139 +], +"org.eclipse.jetty:jetty-project:jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java": [ +693, +744, +801, +835, +1008, +1010, +1296, +1298, +1715, +1743 +] +} diff --git a/its/ruling/src/test/resources/eclipse-jetty/java-S8692.json b/its/ruling/src/test/resources/eclipse-jetty/java-S8692.json new file mode 100644 index 00000000000..f0ccce9797b --- /dev/null +++ b/its/ruling/src/test/resources/eclipse-jetty/java-S8692.json @@ -0,0 +1,40 @@ +{ +"org.eclipse.jetty:jetty-project:jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java": [ +139 +], +"org.eclipse.jetty:jetty-project:jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java": [ +693, +744, +801, +835, +1008, +1010, +1296, +1298, +1715, +1743 +], +"org.eclipse.jetty:jetty-project:jetty-util/src/test/java/org/eclipse/jetty/util/DateCacheTest.java": [ +78, +79, +80, +82, +83, +85, +86, +87, +89, +90, +91 +], +"org.eclipse.jetty:jetty-project:jetty-util/src/test/java/org/eclipse/jetty/util/thread/SchedulerTest.java": [ +84, +90, +106, +112, +122, +128, +149, +170 +] +} diff --git a/its/ruling/src/test/resources/sonar-server/java-S8692.json b/its/ruling/src/test/resources/sonar-server/java-S8692.json new file mode 100644 index 00000000000..26c5a91e6ad --- /dev/null +++ b/its/ruling/src/test/resources/sonar-server/java-S8692.json @@ -0,0 +1,162 @@ +{ +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/authentication/JwtSerializerTest.java": [ +74, +88, +115, +254 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/batch/ProjectDataLoaderMediumTest.java": [ +609, +620, +621 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/component/ComponentCleanerServiceTest.java": [ +137 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/component/ws/TreeActionTest.java": [ +397 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IssueCounterTest.java": [ +332 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IssueLifecycleTest.java": [ +47 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/EvaluationResultTextConverterTest.java": [ +135, +158 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/step/ComputeQProfileMeasureStepTest.java": [ +150 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/step/QualityProfileEventsStepTest.java": [ +270 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/step/UpdateQualityProfilesLastUsedDateStepTest.java": [ +160 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java": [ +137 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/issue/AssignActionTest.java": [ +59 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/issue/IssueFieldsSetterTest.java": [ +50 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/issue/IssueQueryTest.java": [ +49, +50, +51 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/issue/IssueUpdaterTest.java": [ +92, +107, +132, +155, +179 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/issue/IssuesFinderSortTest.java": [ +111, +133, +155 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/issue/ServerIssueStorageTest.java": [ +118 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/issue/TransitionActionTest.java": [ +71 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/issue/TransitionServiceTest.java": [ +95 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java": [ +76 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/issue/index/IssueIndexTest.java": [ +107, +1254 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/issue/index/IssueIndexerTest.java": [ +244 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest.java": [ +63 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationTest.java": [ +68 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/issue/workflow/IssueWorkflowTest.java": [ +142, +160, +178, +196, +215, +231, +249 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/measure/custom/ws/SearchActionTest.java": [ +183 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java": [ +1384, +1393, +1402 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java": [ +293 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/measure/ws/ComponentTreeActionTest.java": [ +144, +146 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/organization/TestDefaultOrganizationProvider.java": [ +39 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/permission/index/PermissionIndexerTester.java": [ +41, +48, +55 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/platform/ws/DbMigrationStatusActionTest.java": [ +72 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/platform/ws/IndexActionTest.java": [ +64, +77 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/platform/ws/MigrateDbActionTest.java": [ +63 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/projecttag/ws/SearchActionTest.java": [ +116 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/qualityprofile/ws/InheritanceActionTest.java": [ +257, +270 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/rule/RuleCreatorTest.java": [ +471, +472, +489, +490, +506, +507 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/rule/ws/ShowActionMediumTest.java": [ +270, +271, +289, +290 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/search/BaseDocTest.java": [ +109, +135 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/test/index/TestIndexerTest.java": [ +120 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/ui/ws/ComponentActionTest.java": [ +560 +] +} diff --git a/java-checks-test-sources/default/src/test/java/checks/tests/SystemClockCheckSample.java b/java-checks-test-sources/default/src/test/java/checks/tests/SystemClockCheckSample.java new file mode 100644 index 00000000000..ba729fbe6c2 --- /dev/null +++ b/java-checks-test-sources/default/src/test/java/checks/tests/SystemClockCheckSample.java @@ -0,0 +1,105 @@ +package checks.tests; + +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +class SystemClockCheckSample { + + @Mock + private Clock clock; + + static class SecurityService { + private final Clock clock; + + public SecurityService(Clock clock) { + this.clock = clock; + } + + public boolean isTokenValid(Instant issuedAt) { + return issuedAt.isAfter(Instant.now(clock).minus(1, ChronoUnit.HOURS)); + } + } + + @Test + void testSystemClockInstants() { + Instant now = Instant.now(); // Noncompliant {{Do not use the system clock in tests.}} +// ^^^^^^^^^^^^^ + Instant now2 = Instant.now(Clock.system(ZoneId.systemDefault())); // Noncompliant {{Do not use the system clock in tests.}} +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + assertTrue(now.isBefore(now2)); + } + + @Test + void testLocalDateTimeTypes() { + LocalDateTime dateTime1 = LocalDateTime.now(Clock.systemUTC()); // Noncompliant {{Do not use the system clock in tests.}} +// ^^^^^^^^^^^^^^^^^ + LocalDateTime dateTime2 = LocalDateTime.now(); // Noncompliant {{Do not use the system clock in tests.}} +// ^^^^^^^^^^^^^^^^^^^ + assertTrue(dateTime1.isBefore(dateTime2)); + } + + @Test + void testInjectSystemClock() { + SecurityService securityService = new SecurityService(Clock.systemDefaultZone()); // Noncompliant {{Do not use the system clock in tests.}} +// ^^^^^^^^^^^^^^^^^^^^^^^^^ + assertTrue(securityService.isTokenValid(Instant.now(Clock.system(ZoneId.systemDefault())).minusSeconds(60))); // Noncompliant {{Do not use the system clock in tests.}} +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + } + + @Test + void testSystemMethods() { + long currentTimeMillis = System.currentTimeMillis(); // Noncompliant {{Do not use the system clock in tests.}} +// ^^^^^^^^^^^^^^^^^^^^^^^^^^ + long currentTimeNanos = System.nanoTime(); // Compliant: nanoTime is typically used to measure elapsed time in tests + assertTrue(currentTimeMillis < currentTimeNanos); + } + + @Test + void testDateAndCalendar() { + Date date = new Date(); // Noncompliant {{Do not use the system clock in tests.}} +// ^^^^^^^^^^ + Date date2 = new Date(100000); // Compliant + assertEquals(date, date2); + Calendar calendar = Calendar.getInstance(); // Noncompliant {{Do not use the system clock in tests.}} +// ^^^^^^^^^^^^^^^^^^^^^^ + Calendar calendar2 = Calendar.getInstance(TimeZone.getDefault()); // Noncompliant {{Do not use the system clock in tests.}} +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + Calendar calendar3 = Calendar.getInstance(Locale.getDefault()); // Noncompliant {{Do not use the system clock in tests.}} +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + assertEquals(calendar.getTime(), calendar2.getTime()); + assertEquals(calendar2.getTime(), calendar3.getTime()); + } + + @Test + void testFixedClock() { + Instant start = Instant.now(Clock.fixed(Instant.parse("2026-05-07T10:00:00Z"), ZoneOffset.UTC)); // Compliant + Instant later = start.plus(1, ChronoUnit.MINUTES); + assertTrue(start.isBefore(later)); + } + + @Test + void testInjectFixedClock() { + Instant fixedPoint = Instant.parse("2026-05-07T10:00:00Z"); // Compliant + when(clock.instant()).thenReturn(fixedPoint); + when(clock.getZone()).thenReturn(ZoneOffset.UTC); + + SecurityService service = new SecurityService(clock); + Instant issuedAt = Instant.parse("2026-05-07T09:30:00Z"); // Compliant + assertTrue(service.isTokenValid(issuedAt)); + } + +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/tests/SystemClockCheck.java b/java-checks/src/main/java/org/sonar/java/checks/tests/SystemClockCheck.java new file mode 100644 index 00000000000..6f9fe61f651 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/tests/SystemClockCheck.java @@ -0,0 +1,83 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.tests; + +import org.sonar.check.Rule; +import org.sonar.java.checks.methods.AbstractMethodDetection; +import org.sonar.plugins.java.api.semantic.MethodMatchers; +import org.sonar.plugins.java.api.tree.MethodInvocationTree; +import org.sonar.plugins.java.api.tree.NewClassTree; + +@Rule(key = "S8692") +public class SystemClockCheck extends AbstractMethodDetection { + + private static final String MESSAGE = "Do not use the system clock in tests."; + + private static final MethodMatchers SYSTEM_CLOCK_MATCHERS = MethodMatchers.or( + MethodMatchers.create() + .ofTypes("java.time.LocalDate", + "java.time.LocalTime", + "java.time.LocalDateTime", + "java.time.MonthDay", + "java.time.Year", + "java.time.YearMonth", + "java.time.ZonedDateTime", + "java.time.OffsetDateTime", + "java.time.OffsetTime", + "java.time.Instant") + .names("now") + .addWithoutParametersMatcher() + .addParametersMatcher("java.time.ZoneId") + .build(), + MethodMatchers.create() + .ofTypes("java.time.Clock") + .names("systemUTC", "systemDefaultZone", "system") + .withAnyParameters() + .build(), + MethodMatchers.create() + .ofTypes("java.lang.System") + .names("currentTimeMillis") + .addWithoutParametersMatcher() + .build(), + MethodMatchers.create() + .ofTypes("java.util.Calendar") + .names("getInstance") + .withAnyParameters() + .build(), + MethodMatchers.create() + .ofTypes("java.util.Date") + .constructor() + .addWithoutParametersMatcher() + .build() + ); + + @Override + protected MethodMatchers getMethodInvocationMatchers() { + return SYSTEM_CLOCK_MATCHERS; + } + + @Override + protected void onConstructorFound(NewClassTree newClass) { + reportIssue(newClass, MESSAGE); + } + + @Override + protected void onMethodInvocationFound(MethodInvocationTree mit) { + reportIssue(mit, MESSAGE); + } + +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/tests/SystemClockCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/tests/SystemClockCheckTest.java new file mode 100644 index 00000000000..10ae2c97a75 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/tests/SystemClockCheckTest.java @@ -0,0 +1,34 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.tests; + +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.CheckVerifier; + +import static org.sonar.java.checks.verifier.TestUtils.testCodeSourcesPath; + +class SystemClockCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(testCodeSourcesPath("checks/tests/SystemClockCheckSample.java")) + .withCheck(new SystemClockCheck()) + .verifyIssues(); + } + +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8692.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8692.html new file mode 100644 index 00000000000..661637fc938 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8692.html @@ -0,0 +1,103 @@ +

Reading the system clock directly within unit tests introduces non-determinism. Tests that rely on the current wall-clock time are "flaky": they +may pass today but fail tomorrow, or behave differently depending on the time zone and local time of the CI/CD runner.

+

To ensure that they are repeatable and predictable, .now() methods like LocalDate.now(), +LocalDateTime.now(), or Instant.now() should only be called with a fixed clock in unit tests. Legacy APIs such as +System.currentTimeMillis(), new Date(), and Calendar.getInstance() have the same problem and should be replaced +with clock injection. Other methods that explicitly call the system clock, such as Clock.system(ZoneId zone), +Clock.systemDefaultZone() and Clock.systemUTC() should also be avoided in tests.

+

Why is this an issue?

+

When a test uses the system clock, it relies on an external, uncontrollable state. This makes it impossible to:

+ +

How to fix it

+

The best practice is to inject a java.time.Clock instance into the class or method being tested. In production code, +Clock.systemDefaultZone() can be used, whereas in test code, Clock.fixed(Instant fixedInstant, ZoneId zone) should be +called, or a mock Clock be injected. This ensures that tests remain deterministic regardless of when or where they are run.

+

Code examples

+

Noncompliant code example

+

The test below is flaky because assumes that the second call to Instant.now() returns an instant that is later than the first, which +might not be true depending on the environment in which the test is run.

+
+@Test
+void testTimeDifference() {
+    Instant instant1 = Instant.now();
+    Instant instant2 = Instant.now();
+    assertTrue(instant1.isBefore(instant2)); // Noncompliant
+}
+
+

In the second example below, the service uses a static call to Instant.now(). This makes it impossible to mock the time without using +advanced (and often discouraged) static mocking techniques.

+
+public class SecurityService {
+    public boolean isTokenValid(Instant issuedAt) {
+        return issuedAt.isAfter(Instant.now().minus(1, ChronoUnit.HOURS));
+    }
+}
+
+public class SecurityServiceTest {
+    @Test
+    void testTokenValidation() {
+        SecurityService service = new SecurityService();
+        assertTrue(service.isTokenValid(Instant.now().minusSeconds(60))); // Noncompliant: the test is non-deterministic and depends on the execution time.
+    }
+}
+
+

Compliant solution

+

Here, the first instant is guaranteed to be before the second, because it is fixed, and the second is explicitly set to a later moment.

+
+@Test
+void testTimeDifference() {
+    Instant instant1 = Instant.now(Clock.fixed(Instant.parse("2026-05-07T10:00:00Z"), ZoneOffset.UTC)); // Compliant
+    Instant instant2 = instant1.plus(1, ChronoUnit.MINUTES);
+    assertTrue(instant1.isBefore(instant2));
+}
+
+

In the solution below, the Clock is a field in the class (often injected by a framework like Spring). In the test, we use @Mock to control exactly +what time the service "thinks" it is.

+
+public class SecurityService {
+    private final Clock clock;
+
+    public SecurityService(Clock clock) {
+        this.clock = clock;
+    }
+
+    public boolean isTokenValid(Instant issuedAt) {
+        return issuedAt.isAfter(Instant.now(clock).minus(1, ChronoUnit.HOURS));
+    }
+}
+
+@ExtendWith(MockitoExtension.class)
+public class SecurityServiceTest {
+    @Mock
+    private Clock clock;
+
+    @Test
+    void testTokenValidationWithMock() {
+        Instant fixedPoint = Instant.parse("2026-05-07T10:00:00Z");
+
+        // Stub the mock clock to behave like a fixed clock.
+        when(clock.instant()).thenReturn(fixedPoint);
+        when(clock.getZone()).thenReturn(ZoneOffset.UTC);
+
+        SecurityService service = new SecurityService(clock);
+
+        Instant issuedAt = Instant.parse("2026-05-07T09:30:00Z");
+        assertTrue(service.isTokenValid(issuedAt)); // Compliant: this is now deterministic, as 09:30 is within 1 hour of 10:00.
+    }
+}
+
+

Resources

+

Documentation

+ +

Articles & blog posts

+ + diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8692.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8692.json new file mode 100644 index 00000000000..9918d6aec7d --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8692.json @@ -0,0 +1,26 @@ +{ + "title": "The system clock should not be used in unit tests", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "1h" + }, + "tags": [ + "java8", + "datetime", + "tests" + ], + "defaultSeverity": "Critical", + "ruleSpecification": "RSPEC-8692", + "sqKey": "S8692", + "scope": "Tests", + "quickfix": "infeasible", + "code": { + "impacts": { + "MAINTAINABILITY": "HIGH", + "RELIABILITY": "MEDIUM" + }, + "attribute": "TRUSTWORTHY" + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json index 1525266545a..5c9ffc92c7e 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json @@ -530,6 +530,7 @@ "S8465", "S8469", "S8491", + "S8692", "S8694" ] }