diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticatorTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticatorTest.java index 78d919ac8..e26cb0214 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticatorTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticatorTest.java @@ -37,6 +37,7 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.textsecuregcm.util.TestClock; class BaseAccountAuthenticatorTest { @@ -47,7 +48,7 @@ class BaseAccountAuthenticatorTest { private AccountsManager accountsManager; private BaseAccountAuthenticator baseAccountAuthenticator; - private Clock clock; + private TestClock clock; private Account acct1; private Account acct2; private Account oldAccount; @@ -55,7 +56,7 @@ class BaseAccountAuthenticatorTest { @BeforeEach void setup() { accountsManager = mock(AccountsManager.class); - clock = mock(Clock.class); + clock = TestClock.now(); baseAccountAuthenticator = new BaseAccountAuthenticator(accountsManager, clock); // We use static UUIDs here because the UUID affects the "date last seen" offset @@ -76,7 +77,7 @@ class BaseAccountAuthenticatorTest { @Test void testUpdateLastSeenMiddleOfDay() { - when(clock.instant()).thenReturn(Instant.ofEpochMilli(currentTime)); + clock.pin(Instant.ofEpochMilli(currentTime)); final Device device1 = acct1.getDevices().stream().findFirst().get(); final Device device2 = acct2.getDevices().stream().findFirst().get(); @@ -96,7 +97,7 @@ class BaseAccountAuthenticatorTest { @Test void testUpdateLastSeenStartOfDay() { - when(clock.instant()).thenReturn(Instant.ofEpochMilli(today)); + clock.pin(Instant.ofEpochMilli(today)); final Device device1 = acct1.getDevices().stream().findFirst().get(); final Device device2 = acct2.getDevices().stream().findFirst().get(); @@ -116,7 +117,7 @@ class BaseAccountAuthenticatorTest { @Test void testUpdateLastSeenEndOfDay() { - when(clock.instant()).thenReturn(Instant.ofEpochMilli(today + 86_400_000L - 1)); + clock.pin(Instant.ofEpochMilli(today + 86_400_000L - 1)); final Device device1 = acct1.getDevices().stream().findFirst().get(); final Device device2 = acct2.getDevices().stream().findFirst().get(); @@ -136,7 +137,7 @@ class BaseAccountAuthenticatorTest { @Test void testNeverWriteYesterday() { - when(clock.instant()).thenReturn(Instant.ofEpochMilli(today)); + clock.pin(Instant.ofEpochMilli(today)); final Device device = oldAccount.getDevices().stream().findFirst().get(); @@ -157,7 +158,7 @@ class BaseAccountAuthenticatorTest { final Device device = mock(Device.class); final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class); - when(clock.instant()).thenReturn(Instant.now()); + clock.unpin(); when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); when(account.getUuid()).thenReturn(uuid); when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); @@ -187,7 +188,7 @@ class BaseAccountAuthenticatorTest { final Device device = mock(Device.class); final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class); - when(clock.instant()).thenReturn(Instant.now()); + clock.unpin(); when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); when(account.getUuid()).thenReturn(uuid); when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); @@ -218,7 +219,7 @@ class BaseAccountAuthenticatorTest { final Device device = mock(Device.class); final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class); - when(clock.instant()).thenReturn(Instant.now()); + clock.unpin(); when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); when(account.getUuid()).thenReturn(uuid); when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); @@ -251,7 +252,7 @@ class BaseAccountAuthenticatorTest { final Device device = mock(Device.class); final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class); - when(clock.instant()).thenReturn(Instant.now()); + clock.unpin(); when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); when(account.getUuid()).thenReturn(uuid); when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); @@ -288,7 +289,7 @@ class BaseAccountAuthenticatorTest { final Device device = mock(Device.class); final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class); - when(clock.instant()).thenReturn(Instant.now()); + clock.unpin(); when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); when(account.getUuid()).thenReturn(uuid); when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); @@ -316,7 +317,7 @@ class BaseAccountAuthenticatorTest { final Device device = mock(Device.class); final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class); - when(clock.instant()).thenReturn(Instant.now()); + clock.unpin(); when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); when(account.getUuid()).thenReturn(uuid); when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverterTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverterTest.java index 152d24212..085f63044 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverterTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverterTest.java @@ -37,21 +37,19 @@ import org.whispersystems.textsecuregcm.entities.Badge; import org.whispersystems.textsecuregcm.entities.BadgeSvg; import org.whispersystems.textsecuregcm.entities.SelfBadge; import org.whispersystems.textsecuregcm.storage.AccountBadge; +import org.whispersystems.textsecuregcm.util.TestClock; public class ConfiguredProfileBadgeConverterTest { - private Clock clock; + private final Clock clock = TestClock.pinned(Instant.ofEpochSecond(42)); private ResourceBundleFactory resourceBundleFactory; private ResourceBundle resourceBundle; @BeforeEach private void beforeEach() { - clock = mock(Clock.class); resourceBundleFactory = mock(ResourceBundleFactory.class, (invocation) -> { throw new UnsupportedOperationException(); }); - - when(clock.instant()).thenReturn(Instant.ofEpochSecond(42)); } private static String idFor(int i) { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java index 36b39ba2e..22cf988ef 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java @@ -31,6 +31,7 @@ import java.security.SecureRandom; import java.time.Clock; import java.time.Duration; import java.time.Instant; +import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; @@ -105,6 +106,7 @@ import org.whispersystems.textsecuregcm.storage.VersionedProfile; import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; import org.whispersystems.textsecuregcm.tests.util.AuthHelper; import org.whispersystems.textsecuregcm.util.SystemMapper; +import org.whispersystems.textsecuregcm.util.TestClock; import org.whispersystems.textsecuregcm.util.Util; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; @@ -112,7 +114,7 @@ import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; @ExtendWith(DropwizardExtensionsSupport.class) class ProfileControllerTest { - private static final Clock clock = mock(Clock.class); + private static final Clock clock = TestClock.pinned(Instant.ofEpochSecond(42)); private static final AccountsManager accountsManager = mock(AccountsManager.class); private static final ProfilesManager profilesManager = mock(ProfilesManager.class); private static final RateLimiters rateLimiters = mock(RateLimiters.class); @@ -172,8 +174,6 @@ class ProfileControllerTest { void setup() { reset(s3client); - when(clock.instant()).thenReturn(Instant.ofEpochSecond(42)); - AccountsHelper.setupMockUpdate(accountsManager); dynamicPaymentsConfiguration = mock(DynamicPaymentsConfiguration.class); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/push/ApnPushNotificationSchedulerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/push/ApnPushNotificationSchedulerTest.java index 9a80fc4fa..1e1fc5e87 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/push/ApnPushNotificationSchedulerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/push/ApnPushNotificationSchedulerTest.java @@ -31,6 +31,7 @@ import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.textsecuregcm.util.TestClock; class ApnPushNotificationSchedulerTest { @@ -41,7 +42,7 @@ class ApnPushNotificationSchedulerTest { private Device device; private APNSender apnSender; - private Clock clock; + private TestClock clock; private ApnPushNotificationScheduler apnPushNotificationScheduler; @@ -70,7 +71,7 @@ class ApnPushNotificationSchedulerTest { when(accountsManager.getByAccountIdentifier(ACCOUNT_UUID)).thenReturn(Optional.of(account)); apnSender = mock(APNSender.class); - clock = mock(Clock.class); + clock = TestClock.now(); apnPushNotificationScheduler = new ApnPushNotificationScheduler(REDIS_CLUSTER_EXTENSION.getRedisCluster(), apnSender, accountsManager, clock); } @@ -83,10 +84,10 @@ class ApnPushNotificationSchedulerTest { assertTrue( apnPushNotificationScheduler.getPendingDestinationsForRecurringVoipNotifications(SlotHash.getSlot(endpoint), 1).isEmpty()); - when(clock.millis()).thenReturn(currentTimeMillis - 30_000); + clock.pin(Instant.ofEpochMilli(currentTimeMillis - 30_000)); apnPushNotificationScheduler.scheduleRecurringVoipNotification(account, device); - when(clock.millis()).thenReturn(currentTimeMillis); + clock.pin(Instant.ofEpochMilli(currentTimeMillis)); final List pendingDestinations = apnPushNotificationScheduler.getPendingDestinationsForRecurringVoipNotifications(SlotHash.getSlot(endpoint), 2); assertEquals(1, pendingDestinations.size()); @@ -106,10 +107,10 @@ class ApnPushNotificationSchedulerTest { final ApnPushNotificationScheduler.NotificationWorker worker = apnPushNotificationScheduler.new NotificationWorker(); final long currentTimeMillis = System.currentTimeMillis(); - when(clock.millis()).thenReturn(currentTimeMillis - 30_000); + clock.pin(Instant.ofEpochMilli(currentTimeMillis - 30_000)); apnPushNotificationScheduler.scheduleRecurringVoipNotification(account, device); - when(clock.millis()).thenReturn(currentTimeMillis); + clock.pin(Instant.ofEpochMilli(currentTimeMillis)); final int slot = SlotHash.getSlot(ApnPushNotificationScheduler.getEndpointKey(account, device)); @@ -130,7 +131,7 @@ class ApnPushNotificationSchedulerTest { @Test void testScheduleBackgroundNotificationWithNoRecentNotification() { final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS); - when(clock.millis()).thenReturn(now.toEpochMilli()); + clock.pin(now); assertEquals(Optional.empty(), apnPushNotificationScheduler.getLastBackgroundNotificationTimestamp(account, device)); @@ -151,10 +152,10 @@ class ApnPushNotificationSchedulerTest { now.minus(ApnPushNotificationScheduler.BACKGROUND_NOTIFICATION_PERIOD.dividedBy(2)); // Insert a timestamp for a recently-sent background push notification - when(clock.millis()).thenReturn(recentNotificationTimestamp.toEpochMilli()); + clock.pin(Instant.ofEpochMilli(recentNotificationTimestamp.toEpochMilli())); apnPushNotificationScheduler.sendBackgroundNotification(account, device); - when(clock.millis()).thenReturn(now.toEpochMilli()); + clock.pin(now); apnPushNotificationScheduler.scheduleBackgroundNotification(account, device); final Instant expectedScheduledTimestamp = @@ -170,16 +171,16 @@ class ApnPushNotificationSchedulerTest { final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS); - when(clock.millis()).thenReturn(now.toEpochMilli()); + clock.pin(Instant.ofEpochMilli(now.toEpochMilli())); apnPushNotificationScheduler.scheduleBackgroundNotification(account, device); final int slot = SlotHash.getSlot(ApnPushNotificationScheduler.getPendingBackgroundNotificationQueueKey(account, device)); - when(clock.millis()).thenReturn(now.minusMillis(1).toEpochMilli()); + clock.pin(Instant.ofEpochMilli(now.minusMillis(1).toEpochMilli())); assertEquals(0, worker.processScheduledBackgroundNotifications(slot)); - when(clock.millis()).thenReturn(now.toEpochMilli()); + clock.pin(now); assertEquals(1, worker.processScheduledBackgroundNotifications(slot)); final ArgumentCaptor notificationCaptor = ArgumentCaptor.forClass(PushNotification.class); @@ -203,7 +204,7 @@ class ApnPushNotificationSchedulerTest { final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS); - when(clock.millis()).thenReturn(now.toEpochMilli()); + clock.pin(now); apnPushNotificationScheduler.scheduleBackgroundNotification(account, device); apnPushNotificationScheduler.cancelScheduledNotifications(account, device); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsTest.java index 719789a63..d488b9d51 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsTest.java @@ -40,6 +40,7 @@ import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; import org.whispersystems.textsecuregcm.tests.util.DevicesHelper; import org.whispersystems.textsecuregcm.util.AttributeValues; import org.whispersystems.textsecuregcm.util.SystemMapper; +import org.whispersystems.textsecuregcm.util.TestClock; import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; @@ -78,7 +79,7 @@ class AccountsTest { .build()) .build(); - private Clock mockClock; + private TestClock clock = TestClock.pinned(Instant.EPOCH); private DynamicConfigurationManager mockDynamicConfigManager; private Accounts accounts; @@ -135,11 +136,8 @@ class AccountsTest { when(mockDynamicConfigManager.getConfiguration()) .thenReturn(new DynamicConfiguration()); - mockClock = mock(Clock.class); - when(mockClock.instant()).thenReturn(Instant.EPOCH); - this.accounts = new Accounts( - mockClock, + clock, mockDynamicConfigManager, dynamoDbExtension.getDynamoDbClient(), dynamoDbExtension.getDynamoDbAsyncClient(), @@ -810,12 +808,12 @@ class AccountsTest { Supplier take = () -> accounts.reserveUsername(account2, username, Duration.ofDays(2)); for (int i = 0; i <= 2; i++) { - when(mockClock.instant()).thenReturn(Instant.EPOCH.plus(Duration.ofDays(i))); + clock.pin(Instant.EPOCH.plus(Duration.ofDays(i))); assertThrows(ContestedOptimisticLockException.class, take::get); } // after 2 days, can take the name - when(mockClock.instant()).thenReturn(Instant.EPOCH.plus(Duration.ofDays(2)).plus(Duration.ofSeconds(1))); + clock.pin(Instant.EPOCH.plus(Duration.ofDays(2)).plus(Duration.ofSeconds(1))); final UUID token = take.get(); assertThrows(ContestedOptimisticLockException.class, @@ -840,12 +838,12 @@ class AccountsTest { Runnable take = () -> accounts.setUsername(account2, username); for (int i = 0; i <= 2; i++) { - when(mockClock.instant()).thenReturn(Instant.EPOCH.plus(Duration.ofDays(i))); + clock.pin(Instant.EPOCH.plus(Duration.ofDays(i))); assertThrows(ContestedOptimisticLockException.class, take::run); } // after 2 days, can take the name - when(mockClock.instant()).thenReturn(Instant.EPOCH.plus(Duration.ofDays(2)).plus(Duration.ofSeconds(1))); + clock.pin(Instant.EPOCH.plus(Duration.ofDays(2)).plus(Duration.ofSeconds(1))); take.run(); assertThrows(ContestedOptimisticLockException.class, diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DonationControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DonationControllerTest.java index e6033ab83..a5f80a6e7 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DonationControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DonationControllerTest.java @@ -61,6 +61,7 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.RedeemedReceiptsManager; import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.TestClock; class DonationControllerTest { @@ -99,7 +100,7 @@ class DonationControllerTest { Map.of(1L, "TEST1", 2L, "TEST2", 3L, "TEST3")); } - Clock clock; + Clock clock = TestClock.pinned(Instant.ofEpochSecond(nowEpochSeconds)); ServerZkReceiptOperations zkReceiptOperations; RedeemedReceiptsManager redeemedReceiptsManager; AccountsManager accountsManager; @@ -112,7 +113,6 @@ class DonationControllerTest { @BeforeEach void beforeEach() throws Throwable { - clock = mock(Clock.class); zkReceiptOperations = mock(ServerZkReceiptOperations.class); redeemedReceiptsManager = mock(RedeemedReceiptsManager.class); accountsManager = mock(AccountsManager.class); @@ -125,9 +125,6 @@ class DonationControllerTest { receiptCredentialPresentationFactory = mock(DonationController.ReceiptCredentialPresentationFactory.class); receiptCredentialPresentation = mock(ReceiptCredentialPresentation.class); - when(clock.millis()).thenReturn(nowEpochSeconds * 1000L); - when(clock.instant()).thenReturn(Instant.ofEpochSecond(nowEpochSeconds)); - try { when(receiptCredentialPresentationFactory.build(presentation)).thenReturn(receiptCredentialPresentation); } catch (InvalidInputException e) { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/AccountTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/AccountTest.java index ccfc50059..d2f1afa06 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/AccountTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/AccountTest.java @@ -30,6 +30,7 @@ import org.whispersystems.textsecuregcm.storage.AccountBadge; import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities; import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; +import org.whispersystems.textsecuregcm.util.TestClock; class AccountTest { @@ -442,8 +443,7 @@ class AccountTest { @Test void addAndRemoveBadges() { final Account account = AccountsHelper.generateTestAccount("+14151234567", UUID.randomUUID(), UUID.randomUUID(), List.of(createDevice(Device.MASTER_ID)), new byte[0]); - final Clock clock = mock(Clock.class); - when(clock.instant()).thenReturn(Instant.ofEpochSecond(40)); + final Clock clock = TestClock.pinned(Instant.ofEpochSecond(40)); account.addBadge(clock, new AccountBadge("foo", Instant.ofEpochSecond(42), false)); account.addBadge(clock, new AccountBadge("bar", Instant.ofEpochSecond(44), true)); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/RedeemedReceiptsManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/RedeemedReceiptsManagerTest.java index c9732f7db..3d737f5dc 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/RedeemedReceiptsManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/RedeemedReceiptsManagerTest.java @@ -23,6 +23,7 @@ import org.signal.libsignal.zkgroup.receipts.ReceiptSerial; import org.whispersystems.textsecuregcm.storage.DynamoDbExtension; import org.whispersystems.textsecuregcm.storage.RedeemedReceiptsManager; import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.TestClock; import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; @@ -42,15 +43,12 @@ class RedeemedReceiptsManagerTest { .build()) .build(); - Clock clock; + Clock clock = TestClock.pinned(Instant.ofEpochSecond(NOW_EPOCH_SECONDS)); ReceiptSerial receiptSerial; RedeemedReceiptsManager redeemedReceiptsManager; @BeforeEach void beforeEach() throws InvalidInputException { - clock = mock(Clock.class); - when(clock.millis()).thenReturn(NOW_EPOCH_SECONDS * 1000L); - when(clock.instant()).thenReturn(Instant.ofEpochSecond(NOW_EPOCH_SECONDS)); byte[] receiptSerialBytes = new byte[ReceiptSerial.SIZE]; SECURE_RANDOM.nextBytes(receiptSerialBytes); receiptSerial = new ReceiptSerial(receiptSerialBytes); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/util/TestClock.java b/service/src/test/java/org/whispersystems/textsecuregcm/util/TestClock.java new file mode 100644 index 000000000..ad665f255 --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/util/TestClock.java @@ -0,0 +1,88 @@ +package org.whispersystems.textsecuregcm.util; + +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Optional; + +/** + * Clock class specialized for testing. + * + * This clock can be pinned to a particular instant or can provide the "normal" time. + * + * Unlike normal clocks it can be dynamically pinned and unpinned to help with testing. + * It should not be used in production. + */ +public class TestClock extends java.time.Clock { + + private Optional pinnedInstant; + private final ZoneId zoneId; + + private TestClock(Optional maybePinned, ZoneId id) { + this.pinnedInstant = maybePinned; + this.zoneId = id; + } + + /** + * Instantiate a test clock that returns the "real" time. + * + * The clock can later be pinned to an instant if desired. + * + * @return + */ + public static TestClock now() { + return new TestClock(Optional.empty(), ZoneId.of("UTC")); + } + + /** + * Instantiate a test clock pinned to a particular instant. + * + * The clock can later be pinned to a different instant or unpinned if desired. + * + * Unlike the fixed constructor no time zone is required (it defaults to UTC). + * + * @param instant + * @return test clock pinned to the given instant. + */ + public static TestClock pinned(Instant instant) { + return new TestClock(Optional.of(instant), ZoneId.of("UTC")); + } + + /** + * Pin this test clock to the given instance. + * + * This modifies the existing clock in-place. + * + * @param instant + */ + public void pin(Instant instant) { + this.pinnedInstant = Optional.of(instant); + } + + /** + * Unpin this test clock so it will being returning the "real" time. + * + * This modifies the existing clock in-place. + */ + public void unpin() { + this.pinnedInstant = Optional.empty(); + } + + + public TestClock withZone(ZoneId id) { + return new TestClock(pinnedInstant, id); + } + + public ZoneId getZone() { + return zoneId; + } + + public Instant instant() { + return pinnedInstant.orElseGet(Instant::now); + } + + public long millis() { + return instant().toEpochMilli(); + } + +}