Clean up testing with clocks.

This commit is contained in:
erik-signal 2022-10-21 12:39:47 -04:00 committed by GitHub
parent 0c357bc340
commit fe60cf003f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 133 additions and 52 deletions

View File

@ -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));

View File

@ -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) {

View File

@ -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);

View File

@ -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<String> 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<PushNotification> 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);

View File

@ -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<DynamicConfiguration> 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<UUID> 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,

View File

@ -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) {

View File

@ -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));

View File

@ -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);

View File

@ -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<Instant> pinnedInstant;
private final ZoneId zoneId;
private TestClock(Optional<Instant> 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();
}
}