Bring badge configuration into levels information

This commit is contained in:
Ehren Kret 2021-10-14 11:35:18 -05:00
parent fe21d014f7
commit c0837104cd
5 changed files with 118 additions and 46 deletions

View File

@ -72,7 +72,6 @@ import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.auth.WebsocketRefreshApplicationEventListener; import org.whispersystems.textsecuregcm.auth.WebsocketRefreshApplicationEventListener;
import org.whispersystems.textsecuregcm.badges.ConfiguredProfileBadgeConverter; import org.whispersystems.textsecuregcm.badges.ConfiguredProfileBadgeConverter;
import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter;
import org.whispersystems.textsecuregcm.configuration.DirectoryServerConfiguration; import org.whispersystems.textsecuregcm.configuration.DirectoryServerConfiguration;
import org.whispersystems.textsecuregcm.controllers.AccountController; import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV1; import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV1;
@ -296,7 +295,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.getObjectMapper().setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); environment.getObjectMapper().setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
environment.getObjectMapper().setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); environment.getObjectMapper().setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
ProfileBadgeConverter profileBadgeConverter = new ConfiguredProfileBadgeConverter(clock, config.getBadges()); ConfiguredProfileBadgeConverter profileBadgeConverter =
new ConfiguredProfileBadgeConverter(clock, config.getBadges());
JdbiFactory jdbiFactory = new JdbiFactory(DefaultNameStrategy.CHECK_EMPTY); JdbiFactory jdbiFactory = new JdbiFactory(DefaultNameStrategy.CHECK_EMPTY);
Jdbi accountJdbi = jdbiFactory.build(environment, config.getAccountsDatabaseConfiguration(), "accountdb"); Jdbi accountJdbi = jdbiFactory.build(environment, config.getAccountsDatabaseConfiguration(), "accountdb");
@ -636,7 +636,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
); );
if (config.getSubscription() != null) { if (config.getSubscription() != null) {
commonControllers.add(new SubscriptionController(clock, config.getSubscription(), subscriptionManager, commonControllers.add(new SubscriptionController(clock, config.getSubscription(), subscriptionManager,
stripeManager, zkReceiptOperations, issuedReceiptsManager)); stripeManager, zkReceiptOperations, issuedReceiptsManager, profileBadgeConverter));
} }
for (Object controller : commonControllers) { for (Object controller : commonControllers) {

View File

@ -0,0 +1,14 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.badges;
import java.util.List;
import java.util.Locale;
import org.whispersystems.textsecuregcm.entities.Badge;
public interface BadgeTranslator {
Badge translate(List<Locale> acceptableLanguages, String badgeId);
}

View File

@ -18,13 +18,14 @@ import java.util.ResourceBundle;
import java.util.ResourceBundle.Control; import java.util.ResourceBundle.Control;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration; import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration;
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration; import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
import org.whispersystems.textsecuregcm.entities.Badge; import org.whispersystems.textsecuregcm.entities.Badge;
import org.whispersystems.textsecuregcm.entities.SelfBadge; import org.whispersystems.textsecuregcm.entities.SelfBadge;
import org.whispersystems.textsecuregcm.storage.AccountBadge; import org.whispersystems.textsecuregcm.storage.AccountBadge;
public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter { public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter, BadgeTranslator {
private static final int MAX_LOCALES = 15; private static final int MAX_LOCALES = 15;
@VisibleForTesting @VisibleForTesting
@ -53,6 +54,23 @@ public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter {
this.resourceBundleFactory = resourceBundleFactory; this.resourceBundleFactory = resourceBundleFactory;
} }
@Override
public Badge translate(final List<Locale> acceptableLanguages, final String badgeId) {
final List<Locale> acceptableLocales = getAcceptableLocales(acceptableLanguages);
final ResourceBundle resourceBundle = getResourceBundle(acceptableLocales);
final BadgeConfiguration configuration = knownBadges.get(badgeId);
return newBadge(
false,
configuration.getId(),
configuration.getCategory(),
resourceBundle.getString(configuration.getId() + "_name"),
resourceBundle.getString(configuration.getId() + "_description"),
configuration.getSprites(),
configuration.getSvgs(),
null,
false);
}
@Override @Override
public List<Badge> convert( public List<Badge> convert(
final List<Locale> acceptableLanguages, final List<Locale> acceptableLanguages,
@ -63,35 +81,8 @@ public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter {
} }
final Instant now = clock.instant(); final Instant now = clock.instant();
final List<Locale> acceptableLocales = getAcceptableLocales(acceptableLanguages);
final List<Locale> acceptableLocales = acceptableLanguages.stream().limit(MAX_LOCALES).distinct() final ResourceBundle resourceBundle = getResourceBundle(acceptableLocales);
.collect(Collectors.toList());
final Locale desiredLocale = acceptableLocales.isEmpty() ? Locale.getDefault() : acceptableLocales.get(0);
// define a control with a fallback order as specified in the header
ResourceBundle.Control control = new Control() {
@Override
public List<String> getFormats(final String baseName) {
Objects.requireNonNull(baseName);
return Control.FORMAT_PROPERTIES;
}
@Override
public Locale getFallbackLocale(final String baseName, final Locale locale) {
Objects.requireNonNull(baseName);
if (locale.equals(Locale.getDefault())) {
return null;
}
final int localeIndex = acceptableLocales.indexOf(locale);
if (localeIndex < 0 || localeIndex >= acceptableLocales.size() - 1) {
return Locale.getDefault();
}
// [0, acceptableLocales.size() - 2] is now the possible range for localeIndex
return acceptableLocales.get(localeIndex + 1);
}
};
final ResourceBundle resourceBundle = resourceBundleFactory.createBundle(BASE_NAME, desiredLocale, control);
List<Badge> badges = accountBadges.stream() List<Badge> badges = accountBadges.stream()
.filter(accountBadge -> (isSelf || accountBadge.isVisible()) .filter(accountBadge -> (isSelf || accountBadge.isVisible())
&& now.isBefore(accountBadge.getExpiration()) && now.isBefore(accountBadge.getExpiration())
@ -126,6 +117,40 @@ public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter {
return badges; return badges;
} }
@Nonnull
private ResourceBundle getResourceBundle(final List<Locale> acceptableLocales) {
final Locale desiredLocale = acceptableLocales.isEmpty() ? Locale.getDefault() : acceptableLocales.get(0);
// define a control with a fallback order as specified in the header
Control control = new Control() {
@Override
public List<String> getFormats(final String baseName) {
Objects.requireNonNull(baseName);
return Control.FORMAT_PROPERTIES;
}
@Override
public Locale getFallbackLocale(final String baseName, final Locale locale) {
Objects.requireNonNull(baseName);
if (locale.equals(Locale.getDefault())) {
return null;
}
final int localeIndex = acceptableLocales.indexOf(locale);
if (localeIndex < 0 || localeIndex >= acceptableLocales.size() - 1) {
return Locale.getDefault();
}
// [0, acceptableLocales.size() - 2] is now the possible range for localeIndex
return acceptableLocales.get(localeIndex + 1);
}
};
return resourceBundleFactory.createBundle(BASE_NAME, desiredLocale, control);
}
@Nonnull
private List<Locale> getAcceptableLocales(final List<Locale> acceptableLanguages) {
return acceptableLanguages.stream().limit(MAX_LOCALES).distinct().collect(Collectors.toList());
}
private Badge newBadge( private Badge newBadge(
final boolean isSelf, final boolean isSelf,
final String id, final String id,

View File

@ -44,7 +44,10 @@ import javax.ws.rs.POST;
import javax.ws.rs.PUT; import javax.ws.rs.PUT;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.Status;
@ -56,9 +59,11 @@ import org.signal.zkgroup.receipts.ServerZkReceiptOperations;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.badges.BadgeTranslator;
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration;
import org.whispersystems.textsecuregcm.configuration.SubscriptionPriceConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionPriceConfiguration;
import org.whispersystems.textsecuregcm.entities.Badge;
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager; import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
import org.whispersystems.textsecuregcm.storage.SubscriptionManager; import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
import org.whispersystems.textsecuregcm.storage.SubscriptionManager.GetResult; import org.whispersystems.textsecuregcm.storage.SubscriptionManager.GetResult;
@ -76,6 +81,7 @@ public class SubscriptionController {
private final StripeManager stripeManager; private final StripeManager stripeManager;
private final ServerZkReceiptOperations zkReceiptOperations; private final ServerZkReceiptOperations zkReceiptOperations;
private final IssuedReceiptsManager issuedReceiptsManager; private final IssuedReceiptsManager issuedReceiptsManager;
private final BadgeTranslator badgeTranslator;
public SubscriptionController( public SubscriptionController(
@Nonnull Clock clock, @Nonnull Clock clock,
@ -83,13 +89,15 @@ public class SubscriptionController {
@Nonnull SubscriptionManager subscriptionManager, @Nonnull SubscriptionManager subscriptionManager,
@Nonnull StripeManager stripeManager, @Nonnull StripeManager stripeManager,
@Nonnull ServerZkReceiptOperations zkReceiptOperations, @Nonnull ServerZkReceiptOperations zkReceiptOperations,
@Nonnull IssuedReceiptsManager issuedReceiptsManager) { @Nonnull IssuedReceiptsManager issuedReceiptsManager,
@Nonnull BadgeTranslator badgeTranslator) {
this.clock = Objects.requireNonNull(clock); this.clock = Objects.requireNonNull(clock);
this.config = Objects.requireNonNull(config); this.config = Objects.requireNonNull(config);
this.subscriptionManager = Objects.requireNonNull(subscriptionManager); this.subscriptionManager = Objects.requireNonNull(subscriptionManager);
this.stripeManager = Objects.requireNonNull(stripeManager); this.stripeManager = Objects.requireNonNull(stripeManager);
this.zkReceiptOperations = Objects.requireNonNull(zkReceiptOperations); this.zkReceiptOperations = Objects.requireNonNull(zkReceiptOperations);
this.issuedReceiptsManager = Objects.requireNonNull(issuedReceiptsManager); this.issuedReceiptsManager = Objects.requireNonNull(issuedReceiptsManager);
this.badgeTranslator = Objects.requireNonNull(badgeTranslator);
} }
@Timed @Timed
@ -328,19 +336,19 @@ public class SubscriptionController {
public static class Level { public static class Level {
private final String badgeId; private final Badge badge;
private final Map<String, BigDecimal> currencies; private final Map<String, BigDecimal> currencies;
@JsonCreator @JsonCreator
public Level( public Level(
@JsonProperty("badgeId") String badgeId, @JsonProperty("badge") Badge badge,
@JsonProperty("currencies") Map<String, BigDecimal> currencies) { @JsonProperty("currencies") Map<String, BigDecimal> currencies) {
this.badgeId = badgeId; this.badge = badge;
this.currencies = currencies; this.currencies = currencies;
} }
public String getBadgeId() { public Badge getBadge() {
return badgeId; return badge;
} }
public Map<String, BigDecimal> getCurrencies() { public Map<String, BigDecimal> getCurrencies() {
@ -366,11 +374,13 @@ public class SubscriptionController {
@Path("/levels") @Path("/levels")
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public CompletableFuture<Response> getLevels() { public CompletableFuture<Response> getLevels(@Context ContainerRequestContext containerRequestContext) {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
List<Locale> acceptableLanguages = getAcceptableLanguagesForRequest(containerRequestContext);
GetLevelsResponse getLevelsResponse = new GetLevelsResponse( GetLevelsResponse getLevelsResponse = new GetLevelsResponse(
config.getLevels().entrySet().stream().collect(Collectors.toMap(Entry::getKey, config.getLevels().entrySet().stream().collect(Collectors.toMap(Entry::getKey,
entry -> new GetLevelsResponse.Level(entry.getValue().getBadge(), entry -> new GetLevelsResponse.Level(
badgeTranslator.translate(acceptableLanguages, entry.getValue().getBadge()),
entry.getValue().getPrices().entrySet().stream().collect( entry.getValue().getPrices().entrySet().stream().collect(
Collectors.toMap(levelEntry -> levelEntry.getKey().toUpperCase(Locale.ROOT), Collectors.toMap(levelEntry -> levelEntry.getKey().toUpperCase(Locale.ROOT),
levelEntry -> levelEntry.getValue().getAmount())))))); levelEntry -> levelEntry.getValue().getAmount()))))));
@ -620,6 +630,15 @@ public class SubscriptionController {
} }
} }
private List<Locale> getAcceptableLanguagesForRequest(ContainerRequestContext containerRequestContext) {
try {
return containerRequestContext.getAcceptableLanguages();
} catch (final ProcessingException e) {
logger.warn("Could not get acceptable languages", e);
return List.of();
}
}
private static class RequestData { private static class RequestData {
public final byte[] subscriberBytes; public final byte[] subscriberBytes;

View File

@ -6,6 +6,8 @@
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset; import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -15,6 +17,7 @@ import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
import io.dropwizard.testing.junit5.ResourceExtension; import io.dropwizard.testing.junit5.ResourceExtension;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.Clock; import java.time.Clock;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.glassfish.jersey.server.ServerProperties; import org.glassfish.jersey.server.ServerProperties;
@ -25,10 +28,12 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.signal.zkgroup.receipts.ServerZkReceiptOperations; import org.signal.zkgroup.receipts.ServerZkReceiptOperations;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
import org.whispersystems.textsecuregcm.badges.BadgeTranslator;
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration;
import org.whispersystems.textsecuregcm.configuration.SubscriptionPriceConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionPriceConfiguration;
import org.whispersystems.textsecuregcm.controllers.SubscriptionController.GetLevelsResponse; import org.whispersystems.textsecuregcm.controllers.SubscriptionController.GetLevelsResponse;
import org.whispersystems.textsecuregcm.entities.Badge;
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager; import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
import org.whispersystems.textsecuregcm.storage.SubscriptionManager; import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
import org.whispersystems.textsecuregcm.stripe.StripeManager; import org.whispersystems.textsecuregcm.stripe.StripeManager;
@ -44,8 +49,10 @@ class SubscriptionControllerTest {
private static final StripeManager STRIPE_MANAGER = mock(StripeManager.class); private static final StripeManager STRIPE_MANAGER = mock(StripeManager.class);
private static final ServerZkReceiptOperations ZK_OPS = mock(ServerZkReceiptOperations.class); private static final ServerZkReceiptOperations ZK_OPS = mock(ServerZkReceiptOperations.class);
private static final IssuedReceiptsManager ISSUED_RECEIPTS_MANAGER = mock(IssuedReceiptsManager.class); private static final IssuedReceiptsManager ISSUED_RECEIPTS_MANAGER = mock(IssuedReceiptsManager.class);
private static final BadgeTranslator BADGE_TRANSLATOR = mock(BadgeTranslator.class);
private static final SubscriptionController SUBSCRIPTION_CONTROLLER = new SubscriptionController( private static final SubscriptionController SUBSCRIPTION_CONTROLLER = new SubscriptionController(
CLOCK, SUBSCRIPTION_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS, ISSUED_RECEIPTS_MANAGER); CLOCK, SUBSCRIPTION_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS, ISSUED_RECEIPTS_MANAGER,
BADGE_TRANSLATOR);
private static final ResourceExtension RESOURCE_EXTENSION = ResourceExtension.builder() private static final ResourceExtension RESOURCE_EXTENSION = ResourceExtension.builder()
.addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE) .addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE)
.addProvider(AuthHelper.getAuthFilter()) .addProvider(AuthHelper.getAuthFilter())
@ -58,7 +65,8 @@ class SubscriptionControllerTest {
@AfterEach @AfterEach
void tearDown() { void tearDown() {
reset(CLOCK, SUBSCRIPTION_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS, ISSUED_RECEIPTS_MANAGER); reset(CLOCK, SUBSCRIPTION_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS, ISSUED_RECEIPTS_MANAGER,
BADGE_TRANSLATOR);
} }
@Test @Test
@ -68,6 +76,12 @@ class SubscriptionControllerTest {
2L, new SubscriptionLevelConfiguration("B2", "P2", Map.of("USD", new SubscriptionPriceConfiguration("R2", BigDecimal.valueOf(200)))), 2L, new SubscriptionLevelConfiguration("B2", "P2", Map.of("USD", new SubscriptionPriceConfiguration("R2", BigDecimal.valueOf(200)))),
3L, new SubscriptionLevelConfiguration("B3", "P3", Map.of("USD", new SubscriptionPriceConfiguration("R3", BigDecimal.valueOf(300)))) 3L, new SubscriptionLevelConfiguration("B3", "P3", Map.of("USD", new SubscriptionPriceConfiguration("R3", BigDecimal.valueOf(300))))
)); ));
when(BADGE_TRANSLATOR.translate(any(), eq("B1"))).thenReturn(new Badge("B1", "cat1", "name1", "desc1",
List.of("l", "m", "h", "x", "xx", "xxx"), List.of("s", "m", "M", "S")));
when(BADGE_TRANSLATOR.translate(any(), eq("B2"))).thenReturn(new Badge("B2", "cat2", "name2", "desc2",
List.of("l", "m", "h", "x", "xx", "xxx"), List.of("s", "m", "M", "S")));
when(BADGE_TRANSLATOR.translate(any(), eq("B3"))).thenReturn(new Badge("B3", "cat3", "name3", "desc3",
List.of("l", "m", "h", "x", "xx", "xxx"), List.of("s", "m", "M", "S")));
GetLevelsResponse response = RESOURCE_EXTENSION.target("/v1/subscription/levels") GetLevelsResponse response = RESOURCE_EXTENSION.target("/v1/subscription/levels")
.request() .request()
@ -75,19 +89,19 @@ class SubscriptionControllerTest {
assertThat(response.getLevels()).containsKeys(1L, 2L, 3L).satisfies(longLevelMap -> { assertThat(response.getLevels()).containsKeys(1L, 2L, 3L).satisfies(longLevelMap -> {
assertThat(longLevelMap).extractingByKey(1L).satisfies(level -> { assertThat(longLevelMap).extractingByKey(1L).satisfies(level -> {
assertThat(level.getBadgeId()).isEqualTo("B1"); assertThat(level.getBadge().getId()).isEqualTo("B1");
assertThat(level.getCurrencies()).containsKeys("USD").extractingByKey("USD").satisfies(price -> { assertThat(level.getCurrencies()).containsKeys("USD").extractingByKey("USD").satisfies(price -> {
assertThat(price).isEqualTo("100"); assertThat(price).isEqualTo("100");
}); });
}); });
assertThat(longLevelMap).extractingByKey(2L).satisfies(level -> { assertThat(longLevelMap).extractingByKey(2L).satisfies(level -> {
assertThat(level.getBadgeId()).isEqualTo("B2"); assertThat(level.getBadge().getId()).isEqualTo("B2");
assertThat(level.getCurrencies()).containsKeys("USD").extractingByKey("USD").satisfies(price -> { assertThat(level.getCurrencies()).containsKeys("USD").extractingByKey("USD").satisfies(price -> {
assertThat(price).isEqualTo("200"); assertThat(price).isEqualTo("200");
}); });
}); });
assertThat(longLevelMap).extractingByKey(3L).satisfies(level -> { assertThat(longLevelMap).extractingByKey(3L).satisfies(level -> {
assertThat(level.getBadgeId()).isEqualTo("B3"); assertThat(level.getBadge().getId()).isEqualTo("B3");
assertThat(level.getCurrencies()).containsKeys("USD").extractingByKey("USD").satisfies(price -> { assertThat(level.getCurrencies()).containsKeys("USD").extractingByKey("USD").satisfies(price -> {
assertThat(price).isEqualTo("300"); assertThat(price).isEqualTo("300");
}); });