Update ChangeNumber to allow reset of registration IDs.

This commit is contained in:
gram-signal 2022-06-02 16:37:32 -06:00 committed by GitHub
parent 5cfb133f79
commit 7001ad1445
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 139 additions and 90 deletions

View File

@ -25,6 +25,7 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid; import javax.validation.Valid;
@ -86,6 +87,7 @@ import org.whispersystems.textsecuregcm.storage.AbusiveHostRules;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ChangeNumberManager; import org.whispersystems.textsecuregcm.storage.ChangeNumberManager;
import org.whispersystems.textsecuregcm.storage.ChangeNumberManager.DeviceUpdate;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager; import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
@ -408,22 +410,27 @@ public class AccountController {
throw new ForbiddenException(); throw new ForbiddenException();
} }
if (request.getDeviceSignedPrekeys() != null && !request.getDeviceSignedPrekeys().isEmpty()) { Map<Long, DeviceUpdate> devices = Collections.emptyMap();
if (request.getDeviceMessages() == null || request.getDeviceMessages().size() != request.getDeviceSignedPrekeys().size() - 1) { if (request.getDeviceUpdates() != null && !request.getDeviceUpdates().isEmpty()) {
// device_messages should exist and be one shorter than device_signed_prekeys, since it doesn't have the primary's key. devices = request.getDeviceUpdates().entrySet().stream()
throw new WebApplicationException(Response.status(400).build()); .collect(Collectors.toMap(
} e -> e.getKey(),
e -> new DeviceUpdate(e.getValue().getSignedPhoneNumberIdentityPreKey(), e.getValue().getMessage(), e.getValue().getRegistrationID())));
try { try {
// Checks that all except master ID are in device messages // Checks that all except master ID are in device messages
List<IncomingMessage> deviceMessages = devices.entrySet().stream()
.map(e -> e.getValue().message())
.filter(e -> e != null)
.collect(Collectors.toList());
MessageValidation.validateCompleteDeviceList( MessageValidation.validateCompleteDeviceList(
authenticatedAccount.getAccount(), request.getDeviceMessages(), authenticatedAccount.getAccount(), deviceMessages,
IncomingMessage::getDestinationDeviceId, true, Optional.of(Device.MASTER_ID)); IncomingMessage::getDestinationDeviceId, true, Optional.of(Device.MASTER_ID));
MessageValidation.validateRegistrationIds( MessageValidation.validateRegistrationIds(
authenticatedAccount.getAccount(), request.getDeviceMessages(), authenticatedAccount.getAccount(), deviceMessages,
IncomingMessage::getDestinationDeviceId, IncomingMessage::getDestinationRegistrationId); IncomingMessage::getDestinationDeviceId, IncomingMessage::getDestinationRegistrationId);
// Checks that all including master ID are in signed prekeys // Checks that all including master ID are in signed prekeys
MessageValidation.validateCompleteDeviceList( MessageValidation.validateCompleteDeviceList(
authenticatedAccount.getAccount(), request.getDeviceSignedPrekeys().entrySet(), authenticatedAccount.getAccount(), devices.entrySet(),
e -> e.getKey(), false, Optional.empty()); e -> e.getKey(), false, Optional.empty());
} catch (MismatchedDevicesException e) { } catch (MismatchedDevicesException e) {
throw new WebApplicationException(Response.status(409) throw new WebApplicationException(Response.status(409)
@ -437,9 +444,6 @@ public class AccountController {
.entity(new StaleDevices(e.getStaleDevices())) .entity(new StaleDevices(e.getStaleDevices()))
.build()); .build());
} }
} else if (request.getDeviceMessages() != null && !request.getDeviceMessages().isEmpty()) {
// device_messages shouldn't exist without device_signed_prekeys.
throw new WebApplicationException(Response.status(400).build());
} }
final String number = request.getNumber(); final String number = request.getNumber();
@ -470,8 +474,7 @@ public class AccountController {
final Account updatedAccount = changeNumberManager.changeNumber( final Account updatedAccount = changeNumberManager.changeNumber(
authenticatedAccount.getAccount(), authenticatedAccount.getAccount(),
request.getNumber(), request.getNumber(),
Optional.ofNullable(request.getDeviceSignedPrekeys()).orElse(Collections.emptyMap()), devices);
Optional.ofNullable(request.getDeviceMessages()).orElse(Collections.emptyList()));
return new AccountIdentityResponse( return new AccountIdentityResponse(
updatedAccount.getUuid(), updatedAccount.getUuid(),

View File

@ -9,7 +9,6 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
import java.util.List;
import java.util.Map; import java.util.Map;
public class ChangePhoneNumberRequest { public class ChangePhoneNumberRequest {
@ -26,26 +25,48 @@ public class ChangePhoneNumberRequest {
@Nullable @Nullable
final String registrationLock; final String registrationLock;
@JsonProperty("device_messages") @JsonProperty("deviceUpdates")
@Nullable @Nullable
final List<IncomingMessage> deviceMessages; final Map<Long, DeviceUpdate> deviceUpdates;
@JsonProperty("device_signed_prekeys") public static class DeviceUpdate {
@Nullable
final Map<Long, SignedPreKey> deviceSignedPrekeys; private final IncomingMessage message;
private final SignedPreKey signedPhoneNumberIdentityPreKey;
private final Integer registrationID;
@JsonCreator
public DeviceUpdate(
@JsonProperty("message") final IncomingMessage message,
@JsonProperty("signedPhoneNumberIdentityPrekey") final SignedPreKey signedPhoneNumberIdentityPreKey,
@JsonProperty("registratonId") final Integer registrationID) {
this.message = message;
this.signedPhoneNumberIdentityPreKey = signedPhoneNumberIdentityPreKey;
this.registrationID = registrationID;
}
public IncomingMessage getMessage() {
return message;
}
public SignedPreKey getSignedPhoneNumberIdentityPreKey() {
return signedPhoneNumberIdentityPreKey;
}
public Integer getRegistrationID() {
return registrationID;
}
}
@JsonCreator @JsonCreator
public ChangePhoneNumberRequest(@JsonProperty("number") final String number, public ChangePhoneNumberRequest(@JsonProperty("number") final String number,
@JsonProperty("code") final String code, @JsonProperty("code") final String code,
@JsonProperty("reglock") @Nullable final String registrationLock, @JsonProperty("reglock") @Nullable final String registrationLock,
@JsonProperty("device_messages") @Nullable final List<IncomingMessage> deviceMessages, @JsonProperty("deviceUpdates") @Nullable final Map<Long, DeviceUpdate> deviceUpdates) {
@JsonProperty("device_signed_prekeys") @Nullable final Map<Long, SignedPreKey> deviceSignedPrekeys) {
this.number = number; this.number = number;
this.code = code; this.code = code;
this.registrationLock = registrationLock; this.registrationLock = registrationLock;
this.deviceMessages = deviceMessages; this.deviceUpdates = deviceUpdates;
this.deviceSignedPrekeys = deviceSignedPrekeys;
} }
public String getNumber() { public String getNumber() {
@ -62,12 +83,5 @@ public class ChangePhoneNumberRequest {
} }
@Nullable @Nullable
public List<IncomingMessage> getDeviceMessages() { public Map<Long, DeviceUpdate> getDeviceUpdates() { return deviceUpdates; }
return deviceMessages;
}
@Nullable
public Map<Long, SignedPreKey> getDeviceSignedPrekeys() {
return deviceSignedPrekeys;
}
} }

View File

@ -16,7 +16,6 @@ import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.push.MessageSender; import org.whispersystems.textsecuregcm.push.MessageSender;
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException; import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -32,11 +31,13 @@ public class ChangeNumberManager {
this.accountsManager = accountsManager; this.accountsManager = accountsManager;
} }
public record DeviceUpdate(SignedPreKey signedPhoneNumberIdentityPreKey, IncomingMessage message, Integer registrationID) {
}
public Account changeNumber( public Account changeNumber(
@NotNull Account account, @NotNull Account account,
@NotNull final String number, @NotNull final String number,
@NotNull final Map<Long, SignedPreKey> deviceSignedPrekeys, @NotNull final Map<Long, DeviceUpdate> deviceUpdates) throws InterruptedException {
@NotNull final List<IncomingMessage> deviceMessages) throws InterruptedException {
final Account updatedAccount; final Account updatedAccount;
if (number.equals(account.getNumber())) { if (number.equals(account.getNumber())) {
@ -51,14 +52,17 @@ public class ChangeNumberManager {
// This makes it so the client can resend a request they didn't get a response for (timeout, etc) // This makes it so the client can resend a request they didn't get a response for (timeout, etc)
// to make sure their messages sent and prekeys were updated, even if the first time around the // to make sure their messages sent and prekeys were updated, even if the first time around the
// server crashed at/above this point. // server crashed at/above this point.
if (deviceSignedPrekeys != null && !deviceSignedPrekeys.isEmpty()) { if (deviceUpdates != null && !deviceUpdates.isEmpty()) {
for (Map.Entry<Long, SignedPreKey> entry : deviceSignedPrekeys.entrySet()) { for (Map.Entry<Long, DeviceUpdate> entry : deviceUpdates.entrySet()) {
DeviceUpdate deviceUpdate = entry.getValue();
accountsManager.updateDevice(updatedAccount, entry.getKey(), accountsManager.updateDevice(updatedAccount, entry.getKey(),
d -> d.setPhoneNumberIdentitySignedPreKey(entry.getValue())); device -> {
} if (deviceUpdate.signedPhoneNumberIdentityPreKey() != null) device.setPhoneNumberIdentitySignedPreKey(deviceUpdate.signedPhoneNumberIdentityPreKey());
if (deviceUpdate.registrationID() != null) device.setRegistrationId(deviceUpdate.registrationID());
for (IncomingMessage message : deviceMessages) { });
sendMessageToSelf(updatedAccount, updatedAccount.getDevice(message.getDestinationDeviceId()), message); if (deviceUpdate.message() != null) {
sendMessageToSelf(updatedAccount, updatedAccount.getDevice(entry.getKey()), deviceUpdate.message());
}
} }
} }
return updatedAccount; return updatedAccount;

View File

@ -12,9 +12,9 @@ import org.mockito.stubbing.Answer;
import org.whispersystems.textsecuregcm.entities.IncomingMessage; import org.whispersystems.textsecuregcm.entities.IncomingMessage;
import org.whispersystems.textsecuregcm.entities.SignedPreKey; import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.push.MessageSender; import org.whispersystems.textsecuregcm.push.MessageSender;
import org.whispersystems.textsecuregcm.storage.ChangeNumberManager.DeviceUpdate;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@ -60,7 +60,7 @@ public class ChangeNumberManagerTest {
void changeNumberNoMessages() throws Exception { void changeNumberNoMessages() throws Exception {
Account account = mock(Account.class); Account account = mock(Account.class);
when(account.getNumber()).thenReturn("+18005551234"); when(account.getNumber()).thenReturn("+18005551234");
changeNumberManager.changeNumber(account, "+18025551234", Collections.EMPTY_MAP, Collections.EMPTY_LIST); changeNumberManager.changeNumber(account, "+18025551234", Collections.EMPTY_MAP);
verify(accountsManager).changeNumber(account, "+18025551234"); verify(accountsManager).changeNumber(account, "+18025551234");
verify(accountsManager, never()).updateDevice(any(), eq(1L), any()); verify(accountsManager, never()).updateDevice(any(), eq(1L), any());
verify(messageSender, never()).sendMessage(eq(account), any(), any(), eq(false)); verify(messageSender, never()).sendMessage(eq(account), any(), any(), eq(false));
@ -70,8 +70,8 @@ public class ChangeNumberManagerTest {
void changeNumberSetPrimaryDevicePrekey() throws Exception { void changeNumberSetPrimaryDevicePrekey() throws Exception {
Account account = mock(Account.class); Account account = mock(Account.class);
when(account.getNumber()).thenReturn("+18005551234"); when(account.getNumber()).thenReturn("+18005551234");
var prekeys = Map.of(1L, new SignedPreKey()); var devices = Map.of(1L, new DeviceUpdate(new SignedPreKey(), null, null));
changeNumberManager.changeNumber(account, "+18025551234", prekeys, Collections.EMPTY_LIST); changeNumberManager.changeNumber(account, "+18025551234", devices);
verify(accountsManager).changeNumber(account, "+18025551234"); verify(accountsManager).changeNumber(account, "+18025551234");
verify(accountsManager).updateDevice(any(), eq(1L), any()); verify(accountsManager).updateDevice(any(), eq(1L), any());
verify(messageSender, never()).sendMessage(eq(account), any(), any(), eq(false)); verify(messageSender, never()).sendMessage(eq(account), any(), any(), eq(false));
@ -84,11 +84,13 @@ public class ChangeNumberManagerTest {
when(account.getUuid()).thenReturn(UUID.randomUUID()); when(account.getUuid()).thenReturn(UUID.randomUUID());
Device d2 = mock(Device.class); Device d2 = mock(Device.class);
when(account.getDevice(2L)).thenReturn(Optional.of(d2)); when(account.getDevice(2L)).thenReturn(Optional.of(d2));
var prekeys = Map.of(1L, new SignedPreKey(), 2L, new SignedPreKey());
IncomingMessage msg = mock(IncomingMessage.class); IncomingMessage msg = mock(IncomingMessage.class);
when(msg.getDestinationDeviceId()).thenReturn(2L); when(msg.getDestinationDeviceId()).thenReturn(2L);
when(msg.getContent()).thenReturn(Base64.encodeBase64String(new byte[]{1})); when(msg.getContent()).thenReturn(Base64.encodeBase64String(new byte[]{1}));
changeNumberManager.changeNumber(account, "+18025551234", prekeys, List.of(msg)); var devices = Map.of(
1L, new DeviceUpdate(new SignedPreKey(), null, null),
2L, new DeviceUpdate(new SignedPreKey(), msg, null));
changeNumberManager.changeNumber(account, "+18025551234", devices);
verify(accountsManager).changeNumber(account, "+18025551234"); verify(accountsManager).changeNumber(account, "+18025551234");
verify(accountsManager).updateDevice(any(), eq(1L), any()); verify(accountsManager).updateDevice(any(), eq(1L), any());
verify(accountsManager).updateDevice(any(), eq(2L), any()); verify(accountsManager).updateDevice(any(), eq(2L), any());

View File

@ -30,7 +30,7 @@ class DeviceTest {
private static Stream<Arguments> testIsEnabled() { private static Stream<Arguments> testIsEnabled() {
return Stream.of( return Stream.of(
// master fetchesMessages apnId gcmId signedPreKey lastSeen expectEnabled // master fetchesMessages apnId gcmId signedPhoneNumberIdentityPreKey lastSeen expectEnabled
Arguments.of(true, false, null, null, null, Duration.ofDays(60), false), Arguments.of(true, false, null, null, null, Duration.ofDays(60), false),
Arguments.of(true, false, null, null, null, Duration.ofDays(1), false), Arguments.of(true, false, null, null, null, Duration.ofDays(1), false),
Arguments.of(true, false, null, null, mock(SignedPreKey.class), Duration.ofDays(60), false), Arguments.of(true, false, null, null, mock(SignedPreKey.class), Duration.ofDays(60), false),

View File

@ -69,6 +69,7 @@ import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse; import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId; import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
import org.whispersystems.textsecuregcm.entities.ChangePhoneNumberRequest; import org.whispersystems.textsecuregcm.entities.ChangePhoneNumberRequest;
import org.whispersystems.textsecuregcm.entities.ChangePhoneNumberRequest.DeviceUpdate;
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId; import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
import org.whispersystems.textsecuregcm.entities.IncomingMessage; import org.whispersystems.textsecuregcm.entities.IncomingMessage;
import org.whispersystems.textsecuregcm.entities.RegistrationLock; import org.whispersystems.textsecuregcm.entities.RegistrationLock;
@ -250,7 +251,7 @@ class AccountControllerTest {
when(accountsManager.setUsername(AuthHelper.VALID_ACCOUNT, "takenusername")) when(accountsManager.setUsername(AuthHelper.VALID_ACCOUNT, "takenusername"))
.thenThrow(new UsernameNotAvailableException()); .thenThrow(new UsernameNotAvailableException());
when(changeNumberManager.changeNumber(any(), any(), any(), any())).thenAnswer((Answer<Account>) invocation -> { when(changeNumberManager.changeNumber(any(), any(), any())).thenAnswer((Answer<Account>) invocation -> {
final Account account = invocation.getArgument(0, Account.class); final Account account = invocation.getArgument(0, Account.class);
final String number = invocation.getArgument(1, String.class); final String number = invocation.getArgument(1, String.class);
@ -1248,10 +1249,10 @@ class AccountControllerTest {
.target("/v1/accounts/number") .target("/v1/accounts/number")
.request() .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null), .put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null),
MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class); MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class);
verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), eq(number), any(), any()); verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), eq(number), any());
assertThat(accountIdentityResponse.getUuid()).isEqualTo(AuthHelper.VALID_UUID); assertThat(accountIdentityResponse.getUuid()).isEqualTo(AuthHelper.VALID_UUID);
assertThat(accountIdentityResponse.getNumber()).isEqualTo(number); assertThat(accountIdentityResponse.getNumber()).isEqualTo(number);
@ -1268,12 +1269,12 @@ class AccountControllerTest {
.target("/v1/accounts/number") .target("/v1/accounts/number")
.request() .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null), .put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null),
MediaType.APPLICATION_JSON_TYPE)); MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(400); assertThat(response.getStatus()).isEqualTo(400);
assertThat(response.readEntity(String.class)).isBlank(); assertThat(response.readEntity(String.class)).isBlank();
verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any()); verify(changeNumberManager, never()).changeNumber(any(), any(), any());
} }
@Test @Test
@ -1286,7 +1287,7 @@ class AccountControllerTest {
.target("/v1/accounts/number") .target("/v1/accounts/number")
.request() .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null), .put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null),
MediaType.APPLICATION_JSON_TYPE)); MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(400); assertThat(response.getStatus()).isEqualTo(400);
@ -1295,7 +1296,7 @@ class AccountControllerTest {
assertThat(responseEntity.getOriginalNumber()).isEqualTo(number); assertThat(responseEntity.getOriginalNumber()).isEqualTo(number);
assertThat(responseEntity.getNormalizedNumber()).isEqualTo("+447700900111"); assertThat(responseEntity.getNormalizedNumber()).isEqualTo("+447700900111");
verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any()); verify(changeNumberManager, never()).changeNumber(any(), any(), any());
} }
@Test @Test
@ -1305,10 +1306,10 @@ class AccountControllerTest {
.target("/v1/accounts/number") .target("/v1/accounts/number")
.request() .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(AuthHelper.VALID_NUMBER, "567890", null, null, null), .put(Entity.entity(new ChangePhoneNumberRequest(AuthHelper.VALID_NUMBER, "567890", null, null),
MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class); MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class);
verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any(), any(), any()); verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any(), any());
} }
@Test @Test
@ -1323,11 +1324,11 @@ class AccountControllerTest {
.target("/v1/accounts/number") .target("/v1/accounts/number")
.request() .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null), .put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null),
MediaType.APPLICATION_JSON_TYPE)); MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(403); assertThat(response.getStatus()).isEqualTo(403);
verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any()); verify(changeNumberManager, never()).changeNumber(any(), any(), any());
} }
@Test @Test
@ -1343,11 +1344,11 @@ class AccountControllerTest {
.target("/v1/accounts/number") .target("/v1/accounts/number")
.request() .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code + "-incorrect", null, null, null), .put(Entity.entity(new ChangePhoneNumberRequest(number, code + "-incorrect", null, null),
MediaType.APPLICATION_JSON_TYPE)); MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(403); assertThat(response.getStatus()).isEqualTo(403);
verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any()); verify(changeNumberManager, never()).changeNumber(any(), any(), any());
} }
@Test @Test
@ -1373,11 +1374,11 @@ class AccountControllerTest {
.target("/v1/accounts/number") .target("/v1/accounts/number")
.request() .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null), .put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null),
MediaType.APPLICATION_JSON_TYPE)); MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any(), any(), any()); verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any(), any());
} }
@Test @Test
@ -1403,11 +1404,11 @@ class AccountControllerTest {
.target("/v1/accounts/number") .target("/v1/accounts/number")
.request() .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null), .put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null),
MediaType.APPLICATION_JSON_TYPE)); MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(423); assertThat(response.getStatus()).isEqualTo(423);
verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any()); verify(changeNumberManager, never()).changeNumber(any(), any(), any());
} }
@Test @Test
@ -1435,11 +1436,11 @@ class AccountControllerTest {
.target("/v1/accounts/number") .target("/v1/accounts/number")
.request() .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, reglock, null, null), .put(Entity.entity(new ChangePhoneNumberRequest(number, code, reglock, null),
MediaType.APPLICATION_JSON_TYPE)); MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(423); assertThat(response.getStatus()).isEqualTo(423);
verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any()); verify(changeNumberManager, never()).changeNumber(any(), any(), any());
} }
@Test @Test
@ -1467,11 +1468,11 @@ class AccountControllerTest {
.target("/v1/accounts/number") .target("/v1/accounts/number")
.request() .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, reglock, null, null), .put(Entity.entity(new ChangePhoneNumberRequest(number, code, reglock, null),
MediaType.APPLICATION_JSON_TYPE)); MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any(), any(), any()); verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any(), any());
} }
@Test @Test
@ -1488,11 +1489,14 @@ class AccountControllerTest {
.request() .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, .put(Entity.entity(new ChangePhoneNumberRequest(number, code, null,
List.of(new IncomingMessage(1, null, 1, 1, "foo")), null), Map.of(1L, new DeviceUpdate(
new IncomingMessage(1, null, 1, 1, "foo"),
null,
null))),
MediaType.APPLICATION_JSON_TYPE)); MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(400); assertThat(response.getStatus()).isEqualTo(409);
verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any()); verify(changeNumberManager, never()).changeNumber(any(), any(), any());
} }
@Test @Test
@ -1517,14 +1521,19 @@ class AccountControllerTest {
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest( .put(Entity.entity(new ChangePhoneNumberRequest(
number, code, null, number, code, null,
List.of( Map.of(
new IncomingMessage(1, null, 2, 1, "foo"), 2L, new DeviceUpdate(
new IncomingMessage(1, null, 4, 1, "foo")), new IncomingMessage(1, null, 2, 1, "foo"),
Map.of(1L, new SignedPreKey(), 2L, new SignedPreKey(), 3L, new SignedPreKey())), null,
null),
4L, new DeviceUpdate(
new IncomingMessage(1, null, 4, 1, "foo"),
null,
null))),
MediaType.APPLICATION_JSON_TYPE)); MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(409); assertThat(response.getStatus()).isEqualTo(409);
verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any()); verify(changeNumberManager, never()).changeNumber(any(), any(), any());
} }
@Test @Test
@ -1546,10 +1555,19 @@ class AccountControllerTest {
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of( when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
new StoredVerificationCode(code, System.currentTimeMillis(), "push", null))); new StoredVerificationCode(code, System.currentTimeMillis(), "push", null)));
var deviceMessages = List.of( Map<Long, DeviceUpdate> perDevice = Map.of(
1L, new DeviceUpdate(
null,
new SignedPreKey(),
null),
2L, new DeviceUpdate(
new IncomingMessage(1, null, 2, 2, "content2"), new IncomingMessage(1, null, 2, 2, "content2"),
new IncomingMessage(1, null, 3, 3, "content3")); new SignedPreKey(),
var deviceKeys = Map.of(1L, new SignedPreKey(), 2L, new SignedPreKey(), 3L, new SignedPreKey()); null),
3L, new DeviceUpdate(
new IncomingMessage(1, null, 3, 3, "content3"),
new SignedPreKey(),
null));
final AccountIdentityResponse accountIdentityResponse = final AccountIdentityResponse accountIdentityResponse =
resources.getJerseyTest() resources.getJerseyTest()
@ -1557,12 +1575,10 @@ class AccountControllerTest {
.request() .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest( .put(Entity.entity(new ChangePhoneNumberRequest(
number, code, null, number, code, null, perDevice),
deviceMessages,
deviceKeys),
MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class); MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class);
verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), eq(number), any(), any()); verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), eq(number), any());
assertThat(accountIdentityResponse.getUuid()).isEqualTo(AuthHelper.VALID_UUID); assertThat(accountIdentityResponse.getUuid()).isEqualTo(AuthHelper.VALID_UUID);
assertThat(accountIdentityResponse.getNumber()).isEqualTo(number); assertThat(accountIdentityResponse.getNumber()).isEqualTo(number);
@ -1588,17 +1604,27 @@ class AccountControllerTest {
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of( when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
new StoredVerificationCode(code, System.currentTimeMillis(), "push", null))); new StoredVerificationCode(code, System.currentTimeMillis(), "push", null)));
Map<Long, DeviceUpdate> perDevice = Map.of(
1L, new DeviceUpdate(
null,
new SignedPreKey(),
null),
2L, new DeviceUpdate(
new IncomingMessage(1, null, 2, 1, "foo"),
new SignedPreKey(),
null),
3L, new DeviceUpdate(
new IncomingMessage(1, null, 3, 1, "foo"),
new SignedPreKey(),
null));
final Response response = final Response response =
resources.getJerseyTest() resources.getJerseyTest()
.target("/v1/accounts/number") .target("/v1/accounts/number")
.request() .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest( .put(Entity.entity(new ChangePhoneNumberRequest(
number, code, null, number, code, null, perDevice),
List.of(
new IncomingMessage(1, null, 2, 1, "foo"),
new IncomingMessage(1, null, 3, 1, "foo")),
Map.of(1L, new SignedPreKey(), 2L, new SignedPreKey(), 3L, new SignedPreKey())),
MediaType.APPLICATION_JSON_TYPE)); MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(410); assertThat(response.getStatus()).isEqualTo(410);