diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 868340b16..13cab1082 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -1142,7 +1142,7 @@ public class WhisperServerService extends Application cdn3BackupCredentialGenerator.generateUpload(cdnMessageBackupName(backupUser))); + .thenApply(storedBackupAttributes -> { + final Instant previousRefreshTime = storedBackupAttributes.lastRefresh(); + // Only publish a metric update once per day + if (previousRefreshTime.isBefore(today)) { + final Tags tags = Tags.of( + UserAgentTagUtil.getPlatformTag(backupUser.userAgent()), + Tag.of("tier", backupUser.backupLevel().name())); + + DistributionSummary.builder(NUM_OBJECTS_SUMMARY_NAME) + .tags(tags) + .publishPercentileHistogram() + .register(Metrics.globalRegistry) + .record(storedBackupAttributes.numObjects()); + DistributionSummary.builder(BYTES_USED_SUMMARY_NAME) + .tags(tags) + .publishPercentileHistogram() + .register(Metrics.globalRegistry) + .record(storedBackupAttributes.bytesUsed()); + + // Report that the backup is out of quota if it cannot store a max size media object + final boolean quotaExhausted = storedBackupAttributes.bytesUsed() >= + (maxTotalMediaSize - BackupManager.MAX_MEDIA_OBJECT_SIZE); + + Metrics.counter(BACKUPS_COUNTER_NAME, + tags.and("quotaExhausted", String.valueOf(quotaExhausted))) + .increment(); + } + return cdn3BackupCredentialGenerator.generateUpload(cdnMessageBackupName(backupUser)); + }); } public CompletableFuture createTemporaryAttachmentUploadDescriptor( @@ -312,12 +347,14 @@ public class BackupManager { }) .sum(); - final Duration maxQuotaStaleness = - dynamicConfigurationManager.getConfiguration().getBackupConfiguration().maxQuotaStaleness(); + final DynamicBackupConfiguration backupConfiguration = + dynamicConfigurationManager.getConfiguration().getBackupConfiguration(); + final Duration maxQuotaStaleness = backupConfiguration.maxQuotaStaleness(); + final long maxTotalMediaSize = backupConfiguration.maxTotalMediaSize(); return backupsDb.getMediaUsage(backupUser) .thenComposeAsync(info -> { - long remainingQuota = MAX_TOTAL_BACKUP_MEDIA_BYTES - info.usageInfo().bytesUsed(); + long remainingQuota = maxTotalMediaSize - info.usageInfo().bytesUsed(); final boolean canStore = remainingQuota >= totalBytesAdded; if (canStore || info.lastRecalculationTime().isAfter(clock.instant().minus(maxQuotaStaleness))) { return CompletableFuture.completedFuture(remainingQuota); @@ -336,7 +373,7 @@ public class BackupManager { Tag.of("usageChanged", String.valueOf(usageChanged)))) .increment(); }) - .thenApply(newUsage -> MAX_TOTAL_BACKUP_MEDIA_BYTES - newUsage.bytesUsed()); + .thenApply(newUsage -> maxTotalMediaSize - newUsage.bytesUsed()); }) .thenApply(remainingQuota -> { // Figure out how many of the requested objects fit in the remaining quota diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupsDb.java b/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupsDb.java index 836143eb9..888d77c94 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupsDb.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupsDb.java @@ -87,10 +87,6 @@ public class BackupsDb { private final SecureRandom secureRandom; - private static final String NUM_OBJECTS_SUMMARY_NAME = MetricsUtil.name(BackupsDb.class, "numObjects"); - private static final String BYTES_USED_SUMMARY_NAME = MetricsUtil.name(BackupsDb.class, "bytesUsed"); - private static final String BACKUPS_COUNTER_NAME = MetricsUtil.name(BackupsDb.class, "backups"); - // The backups table // B: 16 bytes that identifies the backup @@ -257,12 +253,14 @@ public class BackupsDb { .thenRun(Util.NOOP); } + /** * Track that a backup will be stored for the user * * @param backupUser an already authorized backup user + * @return A future that completes with the attributes of the backup before the update */ - CompletableFuture addMessageBackup(final AuthenticatedBackupUser backupUser) { + CompletableFuture addMessageBackup(final AuthenticatedBackupUser backupUser) { final Instant today = clock.instant().truncatedTo(ChronoUnit.DAYS); // this could race with concurrent updates, but the only effect would be last-writer-wins on the timestamp return dynamoClient.updateItem( @@ -272,40 +270,7 @@ public class BackupsDb { .updateItemBuilder() .returnValues(ReturnValue.ALL_OLD) .build()) - .thenAccept(updateItemResponse -> - updateMetricsAfterUpload(backupUser, today, updateItemResponse.attributes())); - } - - private void updateMetricsAfterUpload(final AuthenticatedBackupUser backupUser, final Instant today, final Map item) { - final Instant previousRefreshTime = Instant.ofEpochSecond( - AttributeValues.getLong(item, ATTR_LAST_REFRESH, 0L)); - // Only publish a metric update once per day - if (previousRefreshTime.isBefore(today)) { - final long mediaCount = AttributeValues.getLong(item, ATTR_MEDIA_COUNT, 0L); - final long bytesUsed = AttributeValues.getLong(item, ATTR_MEDIA_BYTES_USED, 0L); - final Tags tags = Tags.of( - UserAgentTagUtil.getPlatformTag(backupUser.userAgent()), - Tag.of("tier", backupUser.backupLevel().name())); - - DistributionSummary.builder(NUM_OBJECTS_SUMMARY_NAME) - .tags(tags) - .publishPercentileHistogram() - .register(Metrics.globalRegistry) - .record(mediaCount); - DistributionSummary.builder(BYTES_USED_SUMMARY_NAME) - .tags(tags) - .publishPercentileHistogram() - .register(Metrics.globalRegistry) - .record(bytesUsed); - - // Report that the backup is out of quota if it cannot store a max size media object - final boolean quotaExhausted = bytesUsed >= - (BackupManager.MAX_TOTAL_BACKUP_MEDIA_BYTES - BackupManager.MAX_MEDIA_OBJECT_SIZE); - - Metrics.counter(BACKUPS_COUNTER_NAME, - tags.and("quotaExhausted", String.valueOf(quotaExhausted))) - .increment(); - } + .thenApply(updateItemResponse -> fromItem(updateItemResponse.attributes())); } /** @@ -520,14 +485,18 @@ public class BackupsDb { // Don't use the SDK's item publisher, works around https://github.com/aws/aws-sdk-java-v2/issues/6411 .concatMap(page -> Flux.fromIterable(page.items())) .filter(item -> item.containsKey(KEY_BACKUP_ID_HASH)) - .map(item -> new StoredBackupAttributes( - AttributeValues.getByteArray(item, KEY_BACKUP_ID_HASH, null), - AttributeValues.getString(item, ATTR_BACKUP_DIR, null), - AttributeValues.getString(item, ATTR_MEDIA_DIR, null), - Instant.ofEpochSecond(AttributeValues.getLong(item, ATTR_LAST_REFRESH, 0L)), - Instant.ofEpochSecond(AttributeValues.getLong(item, ATTR_LAST_MEDIA_REFRESH, 0L)), - AttributeValues.getLong(item, ATTR_MEDIA_BYTES_USED, 0L), - AttributeValues.getLong(item, ATTR_MEDIA_COUNT, 0L))); + .map(BackupsDb::fromItem); + } + + private static StoredBackupAttributes fromItem(Map item) { + return new StoredBackupAttributes( + AttributeValues.getByteArray(item, KEY_BACKUP_ID_HASH, null), + AttributeValues.getString(item, ATTR_BACKUP_DIR, null), + AttributeValues.getString(item, ATTR_MEDIA_DIR, null), + Instant.ofEpochSecond(AttributeValues.getLong(item, ATTR_LAST_REFRESH, 0L)), + Instant.ofEpochSecond(AttributeValues.getLong(item, ATTR_LAST_MEDIA_REFRESH, 0L)), + AttributeValues.getLong(item, ATTR_MEDIA_BYTES_USED, 0L), + AttributeValues.getLong(item, ATTR_MEDIA_COUNT, 0L)); } Flux getExpiredBackups(final int segments, final Scheduler scheduler, final Instant purgeTime) { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicBackupConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicBackupConfiguration.java index 2962c942e..79d320116 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicBackupConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicBackupConfiguration.java @@ -5,6 +5,8 @@ package org.whispersystems.textsecuregcm.configuration.dynamic; +import io.dropwizard.util.DataSize; +import jakarta.validation.constraints.NotNull; import java.time.Duration; /** @@ -13,12 +15,14 @@ import java.time.Duration; * @param copyConcurrency How many cdn object copy requests can be outstanding at a time per batch copy-to-backup operation * @param usageCheckpointCount When doing batch operations, how often persist usage deltas * @param maxQuotaStaleness The maximum age of a quota estimate that can be used to enforce a quota limit + * @param maxTotalMediaSize The number of media bytes a paid-tier user may store */ public record DynamicBackupConfiguration( - Integer deletionConcurrency, - Integer copyConcurrency, - Integer usageCheckpointCount, - Duration maxQuotaStaleness) { + @NotNull Integer deletionConcurrency, + @NotNull Integer copyConcurrency, + @NotNull Integer usageCheckpointCount, + @NotNull Duration maxQuotaStaleness, + @NotNull Long maxTotalMediaSize) { public DynamicBackupConfiguration { if (deletionConcurrency == null) { @@ -33,9 +37,12 @@ public record DynamicBackupConfiguration( if (maxQuotaStaleness == null) { maxQuotaStaleness = Duration.ofSeconds(10); } + if (maxTotalMediaSize == null) { + maxTotalMediaSize = DataSize.gibibytes(100).toBytes(); + } } public DynamicBackupConfiguration() { - this(null, null, null, null); + this(null, null, null, null, null); } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java index 79ed8ec85..daf7e292c 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java @@ -65,11 +65,13 @@ import org.whispersystems.textsecuregcm.configuration.OneTimeDonationConfigurati import org.whispersystems.textsecuregcm.configuration.OneTimeDonationCurrencyConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; import org.whispersystems.textsecuregcm.entities.Badge; import org.whispersystems.textsecuregcm.entities.PurchasableBadge; import org.whispersystems.textsecuregcm.mappers.SubscriptionExceptionMapper; import org.whispersystems.textsecuregcm.metrics.MetricsUtil; import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; import org.whispersystems.textsecuregcm.storage.PaymentTime; import org.whispersystems.textsecuregcm.storage.SubscriberCredentials; import org.whispersystems.textsecuregcm.subscriptions.SubscriptionException; @@ -109,6 +111,7 @@ public class SubscriptionController { private final AppleAppStoreManager appleAppStoreManager; private final BadgeTranslator badgeTranslator; private final BankMandateTranslator bankMandateTranslator; + private final DynamicConfigurationManager dynamicConfigurationManager; static final String RECEIPT_ISSUED_COUNTER_NAME = MetricsUtil.name(SubscriptionController.class, "receiptIssued"); static final String PROCESSOR_TAG_NAME = "processor"; static final String TYPE_TAG_NAME = "type"; @@ -124,7 +127,8 @@ public class SubscriptionController { @Nonnull GooglePlayBillingManager googlePlayBillingManager, @Nonnull AppleAppStoreManager appleAppStoreManager, @Nonnull BadgeTranslator badgeTranslator, - @Nonnull BankMandateTranslator bankMandateTranslator) { + @Nonnull BankMandateTranslator bankMandateTranslator, + @NotNull DynamicConfigurationManager dynamicConfigurationManager) { this.subscriptionManager = subscriptionManager; this.clock = Objects.requireNonNull(clock); this.subscriptionConfiguration = Objects.requireNonNull(subscriptionConfiguration); @@ -135,6 +139,7 @@ public class SubscriptionController { this.appleAppStoreManager = appleAppStoreManager; this.badgeTranslator = Objects.requireNonNull(badgeTranslator); this.bankMandateTranslator = Objects.requireNonNull(bankMandateTranslator); + this.dynamicConfigurationManager = dynamicConfigurationManager; } private Map buildCurrencyConfiguration() { @@ -207,12 +212,14 @@ public class SubscriptionController { giftBadge, oneTimeDonationConfiguration.gift().expiration()))); + final long maxTotalBackupMediaBytes = + dynamicConfigurationManager.getConfiguration().getBackupConfiguration().maxTotalMediaSize(); final Map backupLevels = subscriptionConfiguration.getBackupLevels() .entrySet().stream() .collect(Collectors.toMap( e -> String.valueOf(e.getKey()), e -> new BackupLevelConfiguration( - BackupManager.MAX_TOTAL_BACKUP_MEDIA_BYTES, + maxTotalBackupMediaBytes, e.getValue().playProductId(), e.getValue().mediaTtl().toDays()))); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/workers/ClearIssuedReceiptRedemptionsCommand.java b/service/src/main/java/org/whispersystems/textsecuregcm/workers/ClearIssuedReceiptRedemptionsCommand.java index 99f2ff960..14db0e34a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/workers/ClearIssuedReceiptRedemptionsCommand.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/workers/ClearIssuedReceiptRedemptionsCommand.java @@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.workers; import io.dropwizard.core.Application; import io.dropwizard.core.setup.Environment; import java.time.Clock; +import java.util.Base64; import java.util.Optional; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; @@ -75,4 +76,17 @@ public class ClearIssuedReceiptRedemptionsCommand extends AbstractCommandWithDep throw new RuntimeException(ex); } } + + public static void main(String[] args) throws Exception { + final String subscriberId = "7ywqmymkSMBkBi9v06Iy4AN8DiN_lg8gHXM8TSpO0Z0"; + + final SubscriberCredentials creds = SubscriberCredentials + .process(Optional.empty(), subscriberId, Clock.systemUTC()); + System.out.println(Base64.getEncoder().encodeToString(creds.subscriberUser())); + + final String pc = "AWN1c19TV3hDUEhlWDBldzB0UA=="; + final byte[] bc = Base64.getDecoder().decode(pc); + System.out.println(bc[0]); + System.out.println(new String(bc, 1, bc.length - 1)); + } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupManagerTest.java index e4fa07a6b..f09ad6fc5 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupManagerTest.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.whispersystems.textsecuregcm.util.MockUtils.randomSecretBytes; +import io.dropwizard.util.DataSize; import io.grpc.Status; import io.grpc.StatusRuntimeException; import java.io.IOException; @@ -103,6 +104,7 @@ public class BackupManagerTest { private static final CopyParameters COPY_PARAM = new CopyParameters( 3, "abc", 100, COPY_ENCRYPTION_PARAM, TestRandomUtil.nextBytes(15)); + private static final long MAX_TOTAL_MEDIA_BYTES = DataSize.mebibytes(1).toBytes(); private final TestClock testClock = TestClock.now(); private final BackupAuthTestUtil backupAuthTestUtil = new BackupAuthTestUtil(testClock); @@ -113,7 +115,7 @@ public class BackupManagerTest { private final byte[] backupKey = TestRandomUtil.nextBytes(32); private final UUID aci = UUID.randomUUID(); private final DynamicBackupConfiguration backupConfiguration = new DynamicBackupConfiguration( - 3, 4, 5, Duration.ofSeconds(30)); + 3, 4, 5, Duration.ofSeconds(30), MAX_TOTAL_MEDIA_BYTES); private static final SecureValueRecoveryConfiguration CFG = new SecureValueRecoveryConfiguration( @@ -599,7 +601,7 @@ public class BackupManagerTest { // set the backupsDb to be out of quota at t=0 testClock.pin(Instant.ofEpochSecond(1)); - backupsDb.setMediaUsage(backupUser, new UsageInfo(BackupManager.MAX_TOTAL_BACKUP_MEDIA_BYTES, 1000)).join(); + backupsDb.setMediaUsage(backupUser, new UsageInfo(MAX_TOTAL_MEDIA_BYTES, 1000)).join(); // check still within staleness bound (t=0 + 1 day - 1 sec) testClock.pin(Instant.ofEpochSecond(0) .plus(backupConfiguration.maxQuotaStaleness()) @@ -614,7 +616,7 @@ public class BackupManagerTest { final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID); final String backupMediaPrefix = "%s/%s/".formatted(backupUser.backupDir(), backupUser.mediaDir()); - final long remainingAfterRecalc = BackupManager.MAX_TOTAL_BACKUP_MEDIA_BYTES - COPY_PARAM.destinationObjectSize(); + final long remainingAfterRecalc = MAX_TOTAL_MEDIA_BYTES - COPY_PARAM.destinationObjectSize(); // on recalculation, say there's actually enough left to do the copy when(remoteStorageManager.calculateBytesUsed(eq(backupMediaPrefix))) @@ -622,7 +624,7 @@ public class BackupManagerTest { // set the backupsDb to be totally out of quota at t=0 testClock.pin(Instant.ofEpochSecond(0)); - backupsDb.setMediaUsage(backupUser, new UsageInfo(BackupManager.MAX_TOTAL_BACKUP_MEDIA_BYTES, 1000)).join(); + backupsDb.setMediaUsage(backupUser, new UsageInfo(MAX_TOTAL_MEDIA_BYTES, 1000)).join(); testClock.pin(Instant.ofEpochSecond(0).plus(backupConfiguration.maxQuotaStaleness())); // Should recalculate quota and copy can succeed @@ -632,7 +634,7 @@ public class BackupManagerTest { final BackupsDb.TimestampedUsageInfo info = backupsDb.getMediaUsage(backupUser).join(); assertThat(info.lastRecalculationTime()) .isEqualTo(Instant.ofEpochSecond(0).plus(backupConfiguration.maxQuotaStaleness())); - assertThat(info.usageInfo().bytesUsed()).isEqualTo(BackupManager.MAX_TOTAL_BACKUP_MEDIA_BYTES); + assertThat(info.usageInfo().bytesUsed()).isEqualTo(MAX_TOTAL_MEDIA_BYTES); assertThat(info.usageInfo().numObjects()).isEqualTo(1001); } @@ -646,9 +648,9 @@ public class BackupManagerTest { final long destSize = COPY_PARAM.destinationObjectSize(); final long originalRemainingSpace = - BackupManager.MAX_TOTAL_BACKUP_MEDIA_BYTES - (hasSpaceBeforeRecalc ? destSize : (destSize - 1)); + MAX_TOTAL_MEDIA_BYTES - (hasSpaceBeforeRecalc ? destSize : (destSize - 1)); final long afterRecalcRemainingSpace = - BackupManager.MAX_TOTAL_BACKUP_MEDIA_BYTES - (hasSpaceAfterRecalc ? destSize : (destSize - 1)); + MAX_TOTAL_MEDIA_BYTES - (hasSpaceAfterRecalc ? destSize : (destSize - 1)); // set the backupsDb to be out of quota at t=0 testClock.pin(Instant.ofEpochSecond(0)); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupsDbTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupsDbTest.java index d14ba96d1..ced11f7aa 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupsDbTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupsDbTest.java @@ -16,10 +16,7 @@ import java.time.temporal.ChronoUnit; import java.util.Comparator; import java.util.List; import java.util.Optional; -import java.util.concurrent.TimeUnit; import java.util.function.Function; -import java.util.stream.Stream; -import org.assertj.core.util.Streams; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java index e60581e94..a9294283d 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java @@ -63,16 +63,18 @@ import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse; import org.signal.libsignal.zkgroup.receipts.ReceiptSerial; import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations; import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice; -import org.whispersystems.textsecuregcm.backup.BackupManager; import org.whispersystems.textsecuregcm.badges.BadgeTranslator; import org.whispersystems.textsecuregcm.configuration.OneTimeDonationConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicBackupConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; import org.whispersystems.textsecuregcm.controllers.SubscriptionController.GetBankMandateResponse; import org.whispersystems.textsecuregcm.controllers.SubscriptionController.GetSubscriptionConfigurationResponse; import org.whispersystems.textsecuregcm.entities.Badge; import org.whispersystems.textsecuregcm.entities.BadgeSvg; import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper; import org.whispersystems.textsecuregcm.mappers.SubscriptionExceptionMapper; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager; import org.whispersystems.textsecuregcm.storage.OneTimeDonationsManager; import org.whispersystems.textsecuregcm.storage.PaymentTime; @@ -112,6 +114,7 @@ class SubscriptionControllerTest { private static final ObjectMapper YAML_MAPPER = SystemMapper.yamlMapper(); + private static final long MAX_TOTAL_BACKUP_MEDIA_BYTES = 1234L; private static final SubscriptionConfiguration SUBSCRIPTION_CONFIG = ConfigHelper.getSubscriptionConfig(); private static final OneTimeDonationConfiguration ONETIME_CONFIG = ConfigHelper.getOneTimeConfig(); private static final Subscriptions SUBSCRIPTIONS = mock(Subscriptions.class); @@ -129,11 +132,12 @@ class SubscriptionControllerTest { private static final OneTimeDonationsManager ONE_TIME_DONATIONS_MANAGER = mock(OneTimeDonationsManager.class); private static final BadgeTranslator BADGE_TRANSLATOR = mock(BadgeTranslator.class); private static final BankMandateTranslator BANK_MANDATE_TRANSLATOR = mock(BankMandateTranslator.class); + private static final DynamicConfigurationManager DYNAMIC_CONFIGURATION_MANAGER = mock(DynamicConfigurationManager.class); private final static SubscriptionController SUBSCRIPTION_CONTROLLER = new SubscriptionController(CLOCK, SUBSCRIPTION_CONFIG, ONETIME_CONFIG, new SubscriptionManager(SUBSCRIPTIONS, List.of(STRIPE_MANAGER, BRAINTREE_MANAGER, PLAY_MANAGER, APPSTORE_MANAGER), ZK_OPS, ISSUED_RECEIPTS_MANAGER), STRIPE_MANAGER, BRAINTREE_MANAGER, PLAY_MANAGER, APPSTORE_MANAGER, - BADGE_TRANSLATOR, BANK_MANDATE_TRANSLATOR); + BADGE_TRANSLATOR, BANK_MANDATE_TRANSLATOR, DYNAMIC_CONFIGURATION_MANAGER); private static final OneTimeDonationController ONE_TIME_CONTROLLER = new OneTimeDonationController(CLOCK, ONETIME_CONFIG, STRIPE_MANAGER, BRAINTREE_MANAGER, ZK_OPS, ISSUED_RECEIPTS_MANAGER, ONE_TIME_DONATIONS_MANAGER); private static final ResourceExtension RESOURCE_EXTENSION = ResourceExtension.builder() @@ -154,6 +158,10 @@ class SubscriptionControllerTest { when(STRIPE_MANAGER.getProvider()).thenReturn(PaymentProvider.STRIPE); when(BRAINTREE_MANAGER.getProvider()).thenReturn(PaymentProvider.BRAINTREE); + final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); + when(dynamicConfiguration.getBackupConfiguration()) + .thenReturn(new DynamicBackupConfiguration(null, null, null, null, MAX_TOTAL_BACKUP_MEDIA_BYTES)); + when(DYNAMIC_CONFIGURATION_MANAGER.getConfiguration()).thenReturn(dynamicConfiguration); List.of(STRIPE_MANAGER, BRAINTREE_MANAGER) .forEach(manager -> when(manager.supportsPaymentMethod(any())) @@ -1223,7 +1231,7 @@ class SubscriptionControllerTest { }); assertThat(response.backup().levels()).containsOnlyKeys("201").extractingByKey("201").satisfies(configuration -> { - assertThat(configuration.storageAllowanceBytes()).isEqualTo(BackupManager.MAX_TOTAL_BACKUP_MEDIA_BYTES); + assertThat(configuration.storageAllowanceBytes()).isEqualTo(MAX_TOTAL_BACKUP_MEDIA_BYTES); assertThat(configuration.playProductId()).isEqualTo("testPlayProductId"); assertThat(configuration.mediaTtlDays()).isEqualTo(40); });