From 7d41c1219b78808a2c6961fc43ceca61a040c299 Mon Sep 17 00:00:00 2001 From: Ravi Khadiwala Date: Tue, 8 Jul 2025 09:51:10 -0500 Subject: [PATCH] Add /v2/svr as an alternative name for /v2/backup --- .../SecureValueRecovery2Controller.java | 4 +- .../entities/AuthCheckResponseV3.java | 64 ---- .../SecureValueRecovery2ControllerTest.java | 331 +++++++++++++++++- ...SecureValueRecoveryControllerBaseTest.java | 324 ----------------- 4 files changed, 319 insertions(+), 404 deletions(-) delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/entities/AuthCheckResponseV3.java delete mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecoveryControllerBaseTest.java diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecovery2Controller.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecovery2Controller.java index 6c456b567..40c41fa24 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecovery2Controller.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecovery2Controller.java @@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.controllers; import com.google.common.annotations.VisibleForTesting; import io.dropwizard.auth.Auth; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -35,8 +36,9 @@ import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; -@Path("/v2/backup") +@Path("/v2/{name: backup|svr}") @Tag(name = "Secure Value Recovery") +@Schema(description = "Note: /v2/backup is deprecated. Use /v2/svr instead.") public class SecureValueRecovery2Controller { private static final long MAX_AGE_SECONDS = TimeUnit.DAYS.toSeconds(30); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/AuthCheckResponseV3.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/AuthCheckResponseV3.java deleted file mode 100644 index df204ffde..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/AuthCheckResponseV3.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.entities; - -import com.fasterxml.jackson.annotation.JsonValue; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import io.swagger.v3.oas.annotations.media.Schema; -import java.util.Map; -import javax.annotation.Nullable; -import jakarta.validation.constraints.NotNull; -import org.whispersystems.textsecuregcm.util.ByteArrayAdapter; - -public record AuthCheckResponseV3( - @Schema(description = """ - A dictionary with the auth check results, keyed by the token corresponding token provided in the request. - """) - @NotNull Map matches) { - - public record Result( - @Schema(description = "The status of the credential. Either match, no-match, or invalid") - CredentialStatus status, - - @Schema(description = """ - If the credential was a match, the stored shareSet that can be used to restore a value from SVR. Encoded in - standard un-padded base64. - """, implementation = String.class) - @JsonSerialize(using = ByteArrayAdapter.Serializing.class) - @JsonDeserialize(using = ByteArrayAdapter.Deserializing.class) - @Nullable byte[] shareSet) { - - public static Result invalid() { - return new Result(CredentialStatus.INVALID, null); - } - - public static Result noMatch() { - return new Result(CredentialStatus.NO_MATCH, null); - } - - public static Result match(@Nullable final byte[] shareSet) { - return new Result(CredentialStatus.MATCH, shareSet); - } - } - - public enum CredentialStatus { - MATCH("match"), - NO_MATCH("no-match"), - INVALID("invalid"); - - private final String clientCode; - - CredentialStatus(final String clientCode) { - this.clientCode = clientCode; - } - - @JsonValue - public String clientCode() { - return clientCode; - } - } -} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecovery2ControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecovery2ControllerTest.java index 67c98f3ce..b73a87555 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecovery2ControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecovery2ControllerTest.java @@ -6,26 +6,41 @@ package org.whispersystems.textsecuregcm.controllers; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; import static org.whispersystems.textsecuregcm.util.MockUtils.randomSecretBytes; import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; import io.dropwizard.testing.junit5.ResourceExtension; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import java.util.Map; -import java.util.stream.Collectors; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration; +import org.whispersystems.textsecuregcm.entities.AuthCheckRequest; import org.whispersystems.textsecuregcm.entities.AuthCheckResponseV2; +import org.whispersystems.textsecuregcm.storage.Account; 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; +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 java.util.stream.Collectors; @ExtendWith(DropwizardExtensionsSupport.class) -public class SecureValueRecovery2ControllerTest extends SecureValueRecoveryControllerBaseTest { +public class SecureValueRecovery2ControllerTest { private static final SecureValueRecovery2Configuration CFG = new SecureValueRecovery2Configuration( "", @@ -52,19 +67,305 @@ public class SecureValueRecovery2ControllerTest extends SecureValueRecoveryContr .addResource(CONTROLLER) .build(); - protected SecureValueRecovery2ControllerTest() { - super("/v2", ACCOUNTS_MANAGER, CLOCK, RESOURCES, CREDENTIAL_GENERATOR); + @Nested + class WithBackupsPrefix extends SecureValueRecoveryControllerBaseTest { + protected WithBackupsPrefix() { + super("/v2/backup"); + } } - @Override - Map parseCheckResponse(final Response response) { - final AuthCheckResponseV2 authCheckResponseV2 = response.readEntity(AuthCheckResponseV2.class); - return authCheckResponseV2.matches().entrySet().stream().collect(Collectors.toMap( - Map.Entry::getKey, e -> switch (e.getValue()) { - case MATCH -> CheckStatus.MATCH; - case INVALID -> CheckStatus.INVALID; - case NO_MATCH -> CheckStatus.NO_MATCH; - } - )); + @Nested + class WithSvr2Prefix extends SecureValueRecoveryControllerBaseTest { + protected WithSvr2Prefix() { + super("/v2/svr"); + } + } + + static 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; + + @BeforeEach + public void before() throws Exception { + Mockito.reset(ACCOUNTS_MANAGER); + Mockito.when(ACCOUNTS_MANAGER.getByE164(E164_VALID)).thenReturn(Optional.of(account(USER_1))); + } + + protected SecureValueRecoveryControllerBaseTest(final String pathPrefix) { + this.pathPrefix = pathPrefix; + } + + enum CheckStatus { + MATCH, + NO_MATCH, + INVALID + } + + private Map parseCheckResponse(final Response response) { + final AuthCheckResponseV2 authCheckResponseV2 = response.readEntity(AuthCheckResponseV2.class); + return authCheckResponseV2.matches().entrySet().stream().collect(Collectors.toMap( + Map.Entry::getKey, e -> switch (e.getValue()) { + case MATCH -> CheckStatus.MATCH; + case INVALID -> CheckStatus.INVALID; + case NO_MATCH -> CheckStatus.NO_MATCH; + } + )); + } + + @Test + public void testOneMatch() { + validate(Map.of( + token(USER_1, day(1)), CheckStatus.MATCH, + token(USER_2, day(1)), CheckStatus.NO_MATCH, + token(USER_3, day(1)), CheckStatus.NO_MATCH + ), day(2)); + } + + @Test + public void testNoMatch() { + validate(Map.of( + token(USER_2, day(1)), CheckStatus.NO_MATCH, + token(USER_3, day(1)), CheckStatus.NO_MATCH + ), day(2)); + } + + @Test + public void testSomeInvalid() { + 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), CheckStatus.MATCH, + token(user2Cred), CheckStatus.NO_MATCH, + fakeToken, CheckStatus.INVALID + ), day(2)); + } + + @Test + public void testSomeExpired() { + validate(Map.of( + token(USER_1, day(100)), CheckStatus.MATCH, + token(USER_2, day(100)), CheckStatus.NO_MATCH, + token(USER_3, day(10)), CheckStatus.INVALID, + token(USER_3, day(20)), CheckStatus.INVALID + ), day(110)); + } + + @Test + public void testSomeHaveNewerVersions() { + validate(Map.of( + token(USER_1, day(10)), CheckStatus.INVALID, + token(USER_1, day(20)), CheckStatus.MATCH, + token(USER_2, day(10)), CheckStatus.NO_MATCH, + token(USER_3, day(20)), CheckStatus.NO_MATCH, + token(USER_3, day(10)), CheckStatus.INVALID + ), day(25)); + } + + private void validate( + final Map expected, + final long nowMillis) { + CLOCK.setTimeMillis(nowMillis); + final AuthCheckRequest request = new AuthCheckRequest(E164_VALID, List.copyOf(expected.keySet())); + final Response response = RESOURCES.getJerseyTest().target(pathPrefix + "/auth/check") + .request() + .post(Entity.entity(request, MediaType.APPLICATION_JSON)); + try (response) { + assertEquals(200, response.getStatus()); + final Map res = parseCheckResponse(response); + assertEquals(expected, res); + } + } + + @Test + public void testHttpResponseCodeSuccess() { + final Map expected = Map.of( + token(USER_1, day(10)), CheckStatus.INVALID, + token(USER_1, day(20)), CheckStatus.MATCH, + token(USER_2, day(10)), CheckStatus.NO_MATCH, + token(USER_3, day(20)), CheckStatus.NO_MATCH, + token(USER_3, day(10)), CheckStatus.INVALID + ); + + CLOCK.setTimeMillis(day(25)); + + final AuthCheckRequest in = new AuthCheckRequest(E164_VALID, List.copyOf(expected.keySet())); + + final Response response = RESOURCES.getJerseyTest() + .target(pathPrefix + "/auth/check") + .request() + .post(Entity.entity(in, MediaType.APPLICATION_JSON)); + + try (response) { + assertEquals(200, response.getStatus()); + assertEquals(expected, parseCheckResponse(response)); + } + } + + @Test + public void testHttpResponseCodeWhenInvalidNumber() { + final AuthCheckRequest in = new AuthCheckRequest(E164_INVALID, Collections.singletonList("1")); + final Response response = RESOURCES.getJerseyTest() + .target(pathPrefix + "/auth/check") + .request() + .post(Entity.entity(in, MediaType.APPLICATION_JSON)); + + try (response) { + assertEquals(422, response.getStatus()); + } + } + + @Test + public void testHttpResponseCodeWhenTooManyTokens() { + 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(pathPrefix + "/auth/check") + .request() + .post(Entity.entity(inOkay, MediaType.APPLICATION_JSON)); + + final Response responseError1 = RESOURCES.getJerseyTest() + .target(pathPrefix + "/auth/check") + .request() + .post(Entity.entity(inTooMany, MediaType.APPLICATION_JSON)); + + final Response responseError2 = RESOURCES.getJerseyTest() + .target(pathPrefix + "/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() { + final Response response = RESOURCES.getJerseyTest() + .target(pathPrefix + "/auth/check") + .request() + .post(Entity.entity(""" + { + "number": "123" + } + """, MediaType.APPLICATION_JSON)); + + try (response) { + assertEquals(422, response.getStatus()); + } + } + + @Test + public void testHttpResponseCodeWhenNumberMissing() { + final Response response = RESOURCES.getJerseyTest() + .target(pathPrefix + "/auth/check") + .request() + .post(Entity.entity(""" + { + "passwords": ["aaa:bbb"] + } + """, MediaType.APPLICATION_JSON)); + + try (response) { + assertEquals(422, response.getStatus()); + } + } + + @Test + public void testHttpResponseCodeWhenExtraFields() { + final Response response = RESOURCES.getJerseyTest() + .target(pathPrefix + "/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 testAcceptsPasswordsOrTokens() { + final Response passwordsResponse = RESOURCES.getJerseyTest() + .target(pathPrefix + "/auth/check") + .request() + .post(Entity.entity(""" + { + "number": "+18005550123", + "passwords": ["aaa:bbb"] + } + """, MediaType.APPLICATION_JSON)); + try (passwordsResponse) { + assertEquals(200, passwordsResponse.getStatus()); + } + + final Response tokensResponse = RESOURCES.getJerseyTest() + .target(pathPrefix + "/auth/check") + .request() + .post(Entity.entity(""" + { + "number": "+18005550123", + "tokens": ["aaa:bbb"] + } + """, MediaType.APPLICATION_JSON)); + try (tokensResponse) { + assertEquals(200, tokensResponse.getStatus()); + } + } + + @Test + public void testHttpResponseCodeWhenNotAJson() { + final Response response = RESOURCES.getJerseyTest() + .target(pathPrefix + "/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(final ExternalServiceCredentials credentials) { + return credentials.username() + ":" + credentials.password(); + } + + private ExternalServiceCredentials credentials(final UUID uuid, final long timeMillis) { + CLOCK.setTimeMillis(timeMillis); + return CREDENTIAL_GENERATOR.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; + } } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecoveryControllerBaseTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecoveryControllerBaseTest.java deleted file mode 100644 index e67079c29..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecoveryControllerBaseTest.java +++ /dev/null @@ -1,324 +0,0 @@ -/* - * 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 jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -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 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.storage.Account; -import org.whispersystems.textsecuregcm.storage.AccountsManager; -import org.whispersystems.textsecuregcm.util.MutableClock; -import org.whispersystems.textsecuregcm.util.TestRandomUtil; - -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; - } - - enum CheckStatus { - MATCH, - NO_MATCH, - INVALID - } - abstract Map parseCheckResponse(Response response); - - @Test - public void testOneMatch() throws Exception { - validate(Map.of( - token(USER_1, day(1)), CheckStatus.MATCH, - token(USER_2, day(1)), CheckStatus.NO_MATCH, - token(USER_3, day(1)), CheckStatus.NO_MATCH - ), day(2)); - } - - @Test - public void testNoMatch() throws Exception { - validate(Map.of( - token(USER_2, day(1)), CheckStatus.NO_MATCH, - token(USER_3, day(1)), CheckStatus.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), CheckStatus.MATCH, - token(user2Cred), CheckStatus.NO_MATCH, - fakeToken, CheckStatus.INVALID - ), day(2)); - } - - @Test - public void testSomeExpired() throws Exception { - validate(Map.of( - token(USER_1, day(100)), CheckStatus.MATCH, - token(USER_2, day(100)), CheckStatus.NO_MATCH, - token(USER_3, day(10)), CheckStatus.INVALID, - token(USER_3, day(20)), CheckStatus.INVALID - ), day(110)); - } - - @Test - public void testSomeHaveNewerVersions() throws Exception { - validate(Map.of( - token(USER_1, day(10)), CheckStatus.INVALID, - token(USER_1, day(20)), CheckStatus.MATCH, - token(USER_2, day(10)), CheckStatus.NO_MATCH, - token(USER_3, day(20)), CheckStatus.NO_MATCH, - token(USER_3, day(10)), CheckStatus.INVALID - ), day(25)); - } - - private void validate( - final Map 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) { - assertEquals(200, response.getStatus()); - final Map res = parseCheckResponse(response); - assertEquals(expected, res); - } - } - - @Test - public void testHttpResponseCodeSuccess() throws Exception { - final Map expected = Map.of( - token(USER_1, day(10)), CheckStatus.INVALID, - token(USER_1, day(20)), CheckStatus.MATCH, - token(USER_2, day(10)), CheckStatus.NO_MATCH, - token(USER_3, day(20)), CheckStatus.NO_MATCH, - token(USER_3, day(10)), CheckStatus.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) { - assertEquals(200, response.getStatus()); - assertEquals(expected, parseCheckResponse(response)); - } - } - - @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 testAcceptsPasswordsOrTokens() { - final Response passwordsResponse = resourceExtension.getJerseyTest() - .target(pathPrefix + "/backup/auth/check") - .request() - .post(Entity.entity(""" - { - "number": "+18005550123", - "passwords": ["aaa:bbb"] - } - """, MediaType.APPLICATION_JSON)); - try (passwordsResponse) { - assertEquals(200, passwordsResponse.getStatus()); - } - - final Response tokensResponse = resourceExtension.getJerseyTest() - .target(pathPrefix + "/backup/auth/check") - .request() - .post(Entity.entity(""" - { - "number": "+18005550123", - "tokens": ["aaa:bbb"] - } - """, MediaType.APPLICATION_JSON)); - try (tokensResponse) { - assertEquals(200, tokensResponse.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()); - } - } - - String token(final UUID uuid, final long timeMillis) { - return token(credentials(uuid, timeMillis)); - } - - static String token(final ExternalServiceCredentials credentials) { - return credentials.username() + ":" + credentials.password(); - } - - private ExternalServiceCredentials credentials(final UUID uuid, final long timeMillis) { - clock.setTimeMillis(timeMillis); - return credentialsGenerator.generateForUuid(uuid); - } - - 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; - } -}