Implement /v2/backup/auth/check

This commit is contained in:
ravi-signal
2023-05-04 09:23:33 -07:00
committed by GitHub
parent 0e0c0c5dfe
commit 08333d5989
9 changed files with 684 additions and 294 deletions

View File

@@ -6,6 +6,7 @@
package org.whispersystems.textsecuregcm.controllers;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
import io.dropwizard.testing.junit5.ResourceExtension;
@@ -37,17 +38,7 @@ import org.whispersystems.textsecuregcm.util.MutableClock;
import org.whispersystems.textsecuregcm.util.SystemMapper;
@ExtendWith(DropwizardExtensionsSupport.class)
class SecureBackupControllerTest {
private static final UUID USER_1 = UUID.randomUUID();
private static final UUID USER_2 = UUID.randomUUID();
private static final UUID USER_3 = UUID.randomUUID();
private static final String E164_VALID = "+18005550123";
private static final String E164_INVALID = "1(800)555-0123";
class SecureBackupControllerTest extends SecureValueRecoveryControllerBaseTest {
private static final byte[] SECRET = RandomUtils.nextBytes(32);
@@ -55,14 +46,14 @@ class SecureBackupControllerTest {
SecureBackupServiceConfiguration.class,
cfg -> Mockito.when(cfg.getUserAuthenticationTokenSharedSecret()).thenReturn(SECRET)
);
private static final MutableClock CLOCK = MockUtils.mutableClock(0);
private static final MutableClock CLOCK = new MutableClock();
private static final ExternalServiceCredentialsGenerator CREDENTIAL_GENERATOR =
SecureBackupController.credentialsGenerator(CFG, CLOCK);
private static final AccountsManager ACCOUNTS_MANAGER = Mockito.mock(AccountsManager.class);
private static final AccountsManager ACCOUNTS_MANAGER = mock(AccountsManager.class);
private static final SecureBackupController CONTROLLER =
new SecureBackupController(CREDENTIAL_GENERATOR, ACCOUNTS_MANAGER);
@@ -73,219 +64,7 @@ class SecureBackupControllerTest {
.addResource(CONTROLLER)
.build();
@BeforeAll
public static void before() throws Exception {
Mockito.when(ACCOUNTS_MANAGER.getByE164(E164_VALID)).thenReturn(Optional.of(account(USER_1)));
}
@Test
public void testOneMatch() throws Exception {
validate(Map.of(
token(USER_1, day(1)), AuthCheckResponse.Result.MATCH,
token(USER_2, day(1)), AuthCheckResponse.Result.NO_MATCH,
token(USER_3, day(1)), AuthCheckResponse.Result.NO_MATCH
), day(2));
}
@Test
public void testNoMatch() throws Exception {
validate(Map.of(
token(USER_2, day(1)), AuthCheckResponse.Result.NO_MATCH,
token(USER_3, day(1)), AuthCheckResponse.Result.NO_MATCH
), day(2));
}
@Test
public void testEmptyInput() throws Exception {
validate(Collections.emptyMap(), day(2));
}
@Test
public void testSomeInvalid() throws Exception {
final String fakeToken = token(USER_3, day(1)).replaceAll(USER_3.toString(), USER_2.toString());
validate(Map.of(
token(USER_1, day(1)), AuthCheckResponse.Result.MATCH,
token(USER_2, day(1)), AuthCheckResponse.Result.NO_MATCH,
fakeToken, AuthCheckResponse.Result.INVALID
), day(2));
}
@Test
public void testSomeExpired() throws Exception {
validate(Map.of(
token(USER_1, day(100)), AuthCheckResponse.Result.MATCH,
token(USER_2, day(100)), AuthCheckResponse.Result.NO_MATCH,
token(USER_3, day(10)), AuthCheckResponse.Result.INVALID,
token(USER_3, day(20)), AuthCheckResponse.Result.INVALID
), day(110));
}
@Test
public void testSomeHaveNewerVersions() throws Exception {
validate(Map.of(
token(USER_1, day(10)), AuthCheckResponse.Result.INVALID,
token(USER_1, day(20)), AuthCheckResponse.Result.MATCH,
token(USER_2, day(10)), AuthCheckResponse.Result.NO_MATCH,
token(USER_3, day(20)), AuthCheckResponse.Result.NO_MATCH,
token(USER_3, day(10)), AuthCheckResponse.Result.INVALID
), day(25));
}
private static void validate(
final Map<String, AuthCheckResponse.Result> expected,
final long nowMillis) throws Exception {
CLOCK.setTimeMillis(nowMillis);
final AuthCheckRequest request = new AuthCheckRequest(E164_VALID, List.copyOf(expected.keySet()));
final AuthCheckResponse response = CONTROLLER.authCheck(request);
assertEquals(expected, response.matches());
}
@Test
public void testHttpResponseCodeSuccess() throws Exception {
final Map<String, AuthCheckResponse.Result> expected = Map.of(
token(USER_1, day(10)), AuthCheckResponse.Result.INVALID,
token(USER_1, day(20)), AuthCheckResponse.Result.MATCH,
token(USER_2, day(10)), AuthCheckResponse.Result.NO_MATCH,
token(USER_3, day(20)), AuthCheckResponse.Result.NO_MATCH,
token(USER_3, day(10)), AuthCheckResponse.Result.INVALID
);
CLOCK.setTimeMillis(day(25));
final AuthCheckRequest in = new AuthCheckRequest(E164_VALID, List.copyOf(expected.keySet()));
final Response response = RESOURCES.getJerseyTest()
.target("/v1/backup/auth/check")
.request()
.post(Entity.entity(in, MediaType.APPLICATION_JSON));
try (response) {
final AuthCheckResponse res = response.readEntity(AuthCheckResponse.class);
assertEquals(200, response.getStatus());
assertEquals(expected, res.matches());
}
}
@Test
public void testHttpResponseCodeWhenInvalidNumber() throws Exception {
final AuthCheckRequest in = new AuthCheckRequest(E164_INVALID, Collections.singletonList("1"));
final Response response = RESOURCES.getJerseyTest()
.target("/v1/backup/auth/check")
.request()
.post(Entity.entity(in, MediaType.APPLICATION_JSON));
try (response) {
assertEquals(422, response.getStatus());
}
}
@Test
public void testHttpResponseCodeWhenTooManyTokens() throws Exception {
final AuthCheckRequest inOkay = new AuthCheckRequest(E164_VALID, List.of(
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"
));
final AuthCheckRequest inTooMany = new AuthCheckRequest(E164_VALID, List.of(
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"
));
final AuthCheckRequest inNoTokens = new AuthCheckRequest(E164_VALID, Collections.emptyList());
final Response responseOkay = RESOURCES.getJerseyTest()
.target("/v1/backup/auth/check")
.request()
.post(Entity.entity(inOkay, MediaType.APPLICATION_JSON));
final Response responseError1 = RESOURCES.getJerseyTest()
.target("/v1/backup/auth/check")
.request()
.post(Entity.entity(inTooMany, MediaType.APPLICATION_JSON));
final Response responseError2 = RESOURCES.getJerseyTest()
.target("/v1/backup/auth/check")
.request()
.post(Entity.entity(inNoTokens, MediaType.APPLICATION_JSON));
try (responseOkay; responseError1; responseError2) {
assertEquals(200, responseOkay.getStatus());
assertEquals(422, responseError1.getStatus());
assertEquals(422, responseError2.getStatus());
}
}
@Test
public void testHttpResponseCodeWhenPasswordsMissing() throws Exception {
final Response response = RESOURCES.getJerseyTest()
.target("/v1/backup/auth/check")
.request()
.post(Entity.entity("""
{
"number": "123"
}
""", MediaType.APPLICATION_JSON));
try (response) {
assertEquals(422, response.getStatus());
}
}
@Test
public void testHttpResponseCodeWhenNumberMissing() throws Exception {
final Response response = RESOURCES.getJerseyTest()
.target("/v1/backup/auth/check")
.request()
.post(Entity.entity("""
{
"passwords": ["aaa:bbb"]
}
""", MediaType.APPLICATION_JSON));
try (response) {
assertEquals(422, response.getStatus());
}
}
@Test
public void testHttpResponseCodeWhenExtraFields() throws Exception {
final Response response = RESOURCES.getJerseyTest()
.target("/v1/backup/auth/check")
.request()
.post(Entity.entity("""
{
"number": "+18005550123",
"passwords": ["aaa:bbb"],
"unexpected": "value"
}
""", MediaType.APPLICATION_JSON));
try (response) {
assertEquals(200, response.getStatus());
}
}
@Test
public void testHttpResponseCodeWhenNotAJson() throws Exception {
final Response response = RESOURCES.getJerseyTest()
.target("/v1/backup/auth/check")
.request()
.post(Entity.entity("random text", MediaType.APPLICATION_JSON));
try (response) {
assertEquals(400, response.getStatus());
}
}
private static String token(final UUID uuid, final long timeMillis) {
CLOCK.setTimeMillis(timeMillis);
final ExternalServiceCredentials credentials = CREDENTIAL_GENERATOR.generateForUuid(uuid);
return credentials.username() + ":" + credentials.password();
}
private static long day(final int n) {
return TimeUnit.DAYS.toMillis(n);
}
private static Account account(final UUID uuid) {
final Account a = new Account();
a.setUuid(uuid);
return a;
protected SecureBackupControllerTest() {
super("/v1", ACCOUNTS_MANAGER, CLOCK, RESOURCES, CREDENTIAL_GENERATOR);
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import static org.mockito.Mockito.mock;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
import io.dropwizard.testing.junit5.ResourceExtension;
import org.apache.commons.lang3.RandomUtils;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.jupiter.api.extension.ExtendWith;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.MutableClock;
import org.whispersystems.textsecuregcm.util.SystemMapper;
@ExtendWith(DropwizardExtensionsSupport.class)
public class SecureValueRecovery2ControllerTest extends SecureValueRecoveryControllerBaseTest {
private static final SecureValueRecovery2Configuration CFG = new SecureValueRecovery2Configuration(
true,
"",
RandomUtils.nextBytes(32),
RandomUtils.nextBytes(32),
null,
null,
null
);
private static final MutableClock CLOCK = new MutableClock();
private static final ExternalServiceCredentialsGenerator CREDENTIAL_GENERATOR =
SecureValueRecovery2Controller.credentialsGenerator(CFG, CLOCK);
private static final AccountsManager ACCOUNTS_MANAGER = mock(AccountsManager.class);
private static final SecureValueRecovery2Controller CONTROLLER =
new SecureValueRecovery2Controller(CREDENTIAL_GENERATOR, ACCOUNTS_MANAGER, CFG);
private static final ResourceExtension RESOURCES = ResourceExtension.builder()
.addProvider(AuthHelper.getAuthFilter())
.setMapper(SystemMapper.jsonMapper())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(CONTROLLER)
.build();
protected SecureValueRecovery2ControllerTest() {
super("/v2", ACCOUNTS_MANAGER, CLOCK, RESOURCES, CREDENTIAL_GENERATOR);
}
}

View File

@@ -0,0 +1,289 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import static org.junit.jupiter.api.Assertions.assertEquals;
import io.dropwizard.testing.junit5.ResourceExtension;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
import org.whispersystems.textsecuregcm.entities.AuthCheckRequest;
import org.whispersystems.textsecuregcm.entities.AuthCheckResponse;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.util.MutableClock;
abstract class SecureValueRecoveryControllerBaseTest {
private static final UUID USER_1 = UUID.randomUUID();
private static final UUID USER_2 = UUID.randomUUID();
private static final UUID USER_3 = UUID.randomUUID();
private static final String E164_VALID = "+18005550123";
private static final String E164_INVALID = "1(800)555-0123";
private final String pathPrefix;
private final ResourceExtension resourceExtension;
private final AccountsManager mockAccountsManager;
private final ExternalServiceCredentialsGenerator credentialsGenerator;
private final MutableClock clock;
@BeforeEach
public void before() throws Exception {
Mockito.when(mockAccountsManager.getByE164(E164_VALID)).thenReturn(Optional.of(account(USER_1)));
}
protected SecureValueRecoveryControllerBaseTest(
final String pathPrefix,
final AccountsManager mockAccountsManager,
final MutableClock mutableClock,
final ResourceExtension resourceExtension,
final ExternalServiceCredentialsGenerator credentialsGenerator) {
this.pathPrefix = pathPrefix;
this.resourceExtension = resourceExtension;
this.mockAccountsManager = mockAccountsManager;
this.credentialsGenerator = credentialsGenerator;
this.clock = mutableClock;
}
@Test
public void testOneMatch() throws Exception {
validate(Map.of(
token(USER_1, day(1)), AuthCheckResponse.Result.MATCH,
token(USER_2, day(1)), AuthCheckResponse.Result.NO_MATCH,
token(USER_3, day(1)), AuthCheckResponse.Result.NO_MATCH
), day(2));
}
@Test
public void testNoMatch() throws Exception {
validate(Map.of(
token(USER_2, day(1)), AuthCheckResponse.Result.NO_MATCH,
token(USER_3, day(1)), AuthCheckResponse.Result.NO_MATCH
), day(2));
}
@Test
public void testSomeInvalid() throws Exception {
final ExternalServiceCredentials user1Cred = credentials(USER_1, day(1));
final ExternalServiceCredentials user2Cred = credentials(USER_2, day(1));
final ExternalServiceCredentials user3Cred = credentials(USER_3, day(1));
final String fakeToken = token(new ExternalServiceCredentials(user2Cred.username(), user3Cred.password()));
validate(Map.of(
token(user1Cred), AuthCheckResponse.Result.MATCH,
token(user2Cred), AuthCheckResponse.Result.NO_MATCH,
fakeToken, AuthCheckResponse.Result.INVALID
), day(2));
}
@Test
public void testSomeExpired() throws Exception {
validate(Map.of(
token(USER_1, day(100)), AuthCheckResponse.Result.MATCH,
token(USER_2, day(100)), AuthCheckResponse.Result.NO_MATCH,
token(USER_3, day(10)), AuthCheckResponse.Result.INVALID,
token(USER_3, day(20)), AuthCheckResponse.Result.INVALID
), day(110));
}
@Test
public void testSomeHaveNewerVersions() throws Exception {
validate(Map.of(
token(USER_1, day(10)), AuthCheckResponse.Result.INVALID,
token(USER_1, day(20)), AuthCheckResponse.Result.MATCH,
token(USER_2, day(10)), AuthCheckResponse.Result.NO_MATCH,
token(USER_3, day(20)), AuthCheckResponse.Result.NO_MATCH,
token(USER_3, day(10)), AuthCheckResponse.Result.INVALID
), day(25));
}
private void validate(
final Map<String, AuthCheckResponse.Result> expected,
final long nowMillis) throws Exception {
clock.setTimeMillis(nowMillis);
final AuthCheckRequest request = new AuthCheckRequest(E164_VALID, List.copyOf(expected.keySet()));
final Response response = resourceExtension.getJerseyTest().target(pathPrefix + "/backup/auth/check")
.request()
.post(Entity.entity(request, MediaType.APPLICATION_JSON));
try (response) {
final AuthCheckResponse res = response.readEntity(AuthCheckResponse.class);
assertEquals(200, response.getStatus());
assertEquals(expected, res.matches());
}
}
@Test
public void testHttpResponseCodeSuccess() throws Exception {
final Map<String, AuthCheckResponse.Result> expected = Map.of(
token(USER_1, day(10)), AuthCheckResponse.Result.INVALID,
token(USER_1, day(20)), AuthCheckResponse.Result.MATCH,
token(USER_2, day(10)), AuthCheckResponse.Result.NO_MATCH,
token(USER_3, day(20)), AuthCheckResponse.Result.NO_MATCH,
token(USER_3, day(10)), AuthCheckResponse.Result.INVALID
);
clock.setTimeMillis(day(25));
final AuthCheckRequest in = new AuthCheckRequest(E164_VALID, List.copyOf(expected.keySet()));
final Response response = resourceExtension.getJerseyTest()
.target(pathPrefix + "/backup/auth/check")
.request()
.post(Entity.entity(in, MediaType.APPLICATION_JSON));
try (response) {
final AuthCheckResponse res = response.readEntity(AuthCheckResponse.class);
assertEquals(200, response.getStatus());
assertEquals(expected, res.matches());
}
}
@Test
public void testHttpResponseCodeWhenInvalidNumber() throws Exception {
final AuthCheckRequest in = new AuthCheckRequest(E164_INVALID, Collections.singletonList("1"));
final Response response = resourceExtension.getJerseyTest()
.target(pathPrefix + "/backup/auth/check")
.request()
.post(Entity.entity(in, MediaType.APPLICATION_JSON));
try (response) {
assertEquals(422, response.getStatus());
}
}
@Test
public void testHttpResponseCodeWhenTooManyTokens() throws Exception {
final AuthCheckRequest inOkay = new AuthCheckRequest(E164_VALID, List.of(
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"
));
final AuthCheckRequest inTooMany = new AuthCheckRequest(E164_VALID, List.of(
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"
));
final AuthCheckRequest inNoTokens = new AuthCheckRequest(E164_VALID, Collections.emptyList());
final Response responseOkay = resourceExtension.getJerseyTest()
.target(pathPrefix + "/backup/auth/check")
.request()
.post(Entity.entity(inOkay, MediaType.APPLICATION_JSON));
final Response responseError1 = resourceExtension.getJerseyTest()
.target(pathPrefix + "/backup/auth/check")
.request()
.post(Entity.entity(inTooMany, MediaType.APPLICATION_JSON));
final Response responseError2 = resourceExtension.getJerseyTest()
.target(pathPrefix + "/backup/auth/check")
.request()
.post(Entity.entity(inNoTokens, MediaType.APPLICATION_JSON));
try (responseOkay; responseError1; responseError2) {
assertEquals(200, responseOkay.getStatus());
assertEquals(422, responseError1.getStatus());
assertEquals(422, responseError2.getStatus());
}
}
@Test
public void testHttpResponseCodeWhenPasswordsMissing() throws Exception {
final Response response = resourceExtension.getJerseyTest()
.target(pathPrefix + "/backup/auth/check")
.request()
.post(Entity.entity("""
{
"number": "123"
}
""", MediaType.APPLICATION_JSON));
try (response) {
assertEquals(422, response.getStatus());
}
}
@Test
public void testHttpResponseCodeWhenNumberMissing() throws Exception {
final Response response = resourceExtension.getJerseyTest()
.target(pathPrefix + "/backup/auth/check")
.request()
.post(Entity.entity("""
{
"passwords": ["aaa:bbb"]
}
""", MediaType.APPLICATION_JSON));
try (response) {
assertEquals(422, response.getStatus());
}
}
@Test
public void testHttpResponseCodeWhenExtraFields() throws Exception {
final Response response = resourceExtension.getJerseyTest()
.target(pathPrefix + "/backup/auth/check")
.request()
.post(Entity.entity("""
{
"number": "+18005550123",
"passwords": ["aaa:bbb"],
"unexpected": "value"
}
""", MediaType.APPLICATION_JSON));
try (response) {
assertEquals(200, response.getStatus());
}
}
@Test
public void testHttpResponseCodeWhenNotAJson() throws Exception {
final Response response = resourceExtension.getJerseyTest()
.target(pathPrefix + "/backup/auth/check")
.request()
.post(Entity.entity("random text", MediaType.APPLICATION_JSON));
try (response) {
assertEquals(400, response.getStatus());
}
}
private String token(final UUID uuid, final long timeMillis) {
return token(credentials(uuid, timeMillis));
}
private static String token(ExternalServiceCredentials credentials) {
return credentials.username() + ":" + credentials.password();
}
private ExternalServiceCredentials credentials(final UUID uuid, final long timeMillis) {
clock.setTimeMillis(timeMillis);
return credentialsGenerator.generateForUuid(uuid);
}
private static long day(final int n) {
return TimeUnit.DAYS.toMillis(n);
}
private static Account account(final UUID uuid) {
final Account a = new Account();
a.setUuid(uuid);
return a;
}
}

View File

@@ -0,0 +1,145 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.tests.auth;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.apache.commons.lang3.RandomUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsSelector;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsSelector.CredentialInfo;
import org.whispersystems.textsecuregcm.util.MockUtils;
import org.whispersystems.textsecuregcm.util.MutableClock;
public class ExternalServiceCredentialsSelectorTest {
private static final UUID UUID1 = UUID.randomUUID();
private static final UUID UUID2 = UUID.randomUUID();
private static final MutableClock CLOCK = MockUtils.mutableClock(TimeUnit.DAYS.toSeconds(1));
private static final ExternalServiceCredentialsGenerator GEN1 =
ExternalServiceCredentialsGenerator
.builder(RandomUtils.nextBytes(32))
.prependUsername(true)
.withClock(CLOCK)
.build();
private static final ExternalServiceCredentialsGenerator GEN2 =
ExternalServiceCredentialsGenerator
.builder(RandomUtils.nextBytes(32))
.withUserDerivationKey(RandomUtils.nextBytes(32))
.prependUsername(false)
.withDerivedUsernameTruncateLength(16)
.withClock(CLOCK)
.build();
private static ExternalServiceCredentials atTime(
final ExternalServiceCredentialsGenerator gen,
final long deltaMillis,
final UUID identity) {
final Instant old = CLOCK.instant();
try {
CLOCK.incrementMillis(deltaMillis);
return gen.generateForUuid(identity);
} finally {
CLOCK.setTimeInstant(old);
}
}
private static String token(final ExternalServiceCredentials cred) {
return cred.username() + ":" + cred.password();
}
@Test
void single() {
final ExternalServiceCredentials cred = GEN1.generateForUuid(UUID1);
var result = ExternalServiceCredentialsSelector.check(
List.of(token(cred)), GEN1, TimeUnit.MINUTES.toSeconds(1));
assertThat(result).singleElement()
.matches(CredentialInfo::valid)
.matches(info -> info.credentials().equals(cred));
}
@Test
void multipleUsernames() {
final ExternalServiceCredentials cred1New = GEN1.generateForUuid(UUID1);
final ExternalServiceCredentials cred1Old = atTime(GEN1, -1, UUID1);
final ExternalServiceCredentials cred2New = GEN1.generateForUuid(UUID2);
final ExternalServiceCredentials cred2Old = atTime(GEN1, -1, UUID2);
final List<String> tokens = Stream.of(cred1New, cred1Old, cred2New, cred2Old)
.map(ExternalServiceCredentialsSelectorTest::token)
.toList();
final List<CredentialInfo> result = ExternalServiceCredentialsSelector.check(tokens, GEN1,
TimeUnit.MINUTES.toSeconds(1));
assertThat(result).hasSize(4);
assertThat(result).filteredOn(CredentialInfo::valid)
.hasSize(2)
.map(CredentialInfo::credentials)
.containsExactlyInAnyOrder(cred1New, cred2New);
assertThat(result).filteredOn(info -> !info.valid())
.map(CredentialInfo::token)
.containsExactlyInAnyOrder(token(cred1Old), token(cred2Old));
}
@Test
void multipleGenerators() {
final ExternalServiceCredentials gen1Cred = GEN1.generateForUuid(UUID1);
final ExternalServiceCredentials gen2Cred = GEN2.generateForUuid(UUID1);
final List<CredentialInfo> result = ExternalServiceCredentialsSelector.check(
List.of(token(gen1Cred), token(gen2Cred)),
GEN2,
TimeUnit.MINUTES.toSeconds(1));
assertThat(result)
.hasSize(2)
.filteredOn(CredentialInfo::valid)
.singleElement()
.matches(info -> info.credentials().equals(gen2Cred));
assertThat(result)
.filteredOn(info -> !info.valid())
.singleElement()
.matches(info -> info.token().equals(token(gen1Cred)));
}
@ParameterizedTest
@MethodSource
void invalidCredentials(final String invalidCredential) {
final ExternalServiceCredentials validCredential = GEN1.generateForUuid(UUID1);
var result = ExternalServiceCredentialsSelector.check(
List.of(invalidCredential, token(validCredential)), GEN1, TimeUnit.MINUTES.toSeconds(1));
assertThat(result).hasSize(2);
assertThat(result).filteredOn(CredentialInfo::valid).singleElement()
.matches(info -> info.credentials().equals(validCredential));
assertThat(result).filteredOn(info -> !info.valid()).singleElement()
.matches(info -> info.token().equals(invalidCredential));
}
static Stream<String> invalidCredentials() {
return Stream.of(
"blah:blah",
token(atTime(GEN1, -TimeUnit.MINUTES.toSeconds(2), UUID1)), // too old
"nocolon",
"nothingaftercolon:",
":nothingbeforecolon",
token(GEN2.generateForUuid(UUID1))
);
}
}

View File

@@ -28,6 +28,11 @@ public class MutableClock extends Clock {
this(Clock.systemUTC());
}
public MutableClock setTimeInstant(final Instant instant) {
delegate.set(Clock.fixed(instant, ZoneId.of("Etc/UTC")));
return this;
}
public MutableClock setTimeMillis(final long timeMillis) {
delegate.set(fixedTimeMillis(timeMillis));
return this;