Add `/v2/accounts/data_report`
This commit is contained in:
parent
890293e429
commit
6075d5137b
|
@ -13,12 +13,17 @@ import io.dropwizard.auth.Auth;
|
||||||
import io.micrometer.core.instrument.Metrics;
|
import io.micrometer.core.instrument.Metrics;
|
||||||
import io.micrometer.core.instrument.Tag;
|
import io.micrometer.core.instrument.Tag;
|
||||||
import io.micrometer.core.instrument.Tags;
|
import io.micrometer.core.instrument.Tags;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import javax.ws.rs.BadRequestException;
|
import javax.ws.rs.BadRequestException;
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.ForbiddenException;
|
import javax.ws.rs.ForbiddenException;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.HeaderParam;
|
import javax.ws.rs.HeaderParam;
|
||||||
import javax.ws.rs.PUT;
|
import javax.ws.rs.PUT;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
|
@ -29,6 +34,7 @@ import javax.ws.rs.core.Response;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||||
import org.whispersystems.textsecuregcm.auth.PhoneVerificationTokenManager;
|
import org.whispersystems.textsecuregcm.auth.PhoneVerificationTokenManager;
|
||||||
import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager;
|
import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.AccountDataReportResponse;
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
|
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
|
||||||
import org.whispersystems.textsecuregcm.entities.ChangeNumberRequest;
|
import org.whispersystems.textsecuregcm.entities.ChangeNumberRequest;
|
||||||
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
|
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
|
||||||
|
@ -144,4 +150,31 @@ public class AccountControllerV2 {
|
||||||
accountsManager.update(auth.getAccount(), a -> a.setDiscoverableByPhoneNumber(
|
accountsManager.update(auth.getAccount(), a -> a.setDiscoverableByPhoneNumber(
|
||||||
phoneNumberDiscoverability.discoverableByPhoneNumber()));
|
phoneNumberDiscoverability.discoverableByPhoneNumber()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@GET
|
||||||
|
@Path("/data_report")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Operation(summary = "Produces a report of non-ephemeral account data stored by the service")
|
||||||
|
@ApiResponse(responseCode = "200",
|
||||||
|
description = "Response with data report. A plain text representation is a field in the response.",
|
||||||
|
useReturnTypeSchema = true)
|
||||||
|
public AccountDataReportResponse getAccountDataReport(@Auth final AuthenticatedAccount auth) {
|
||||||
|
|
||||||
|
final Account account = auth.getAccount();
|
||||||
|
|
||||||
|
return new AccountDataReportResponse(UUID.randomUUID(), Instant.now(),
|
||||||
|
new AccountDataReportResponse.AccountAndDevicesDataReport(
|
||||||
|
new AccountDataReportResponse.AccountDataReport(
|
||||||
|
account.getNumber(),
|
||||||
|
account.getBadges().stream().map(AccountDataReportResponse.BadgeDataReport::new).toList(),
|
||||||
|
account.isUnrestrictedUnidentifiedAccess(),
|
||||||
|
account.isDiscoverableByPhoneNumber()),
|
||||||
|
account.getDevices().stream().map(device ->
|
||||||
|
new AccountDataReportResponse.DeviceDataReport(
|
||||||
|
device.getId(),
|
||||||
|
Instant.ofEpochMilli(device.getLastSeen()),
|
||||||
|
Instant.ofEpochMilli(device.getCreated()),
|
||||||
|
device.getUserAgent())).toList()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.ser.InstantSerializer;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountBadge;
|
||||||
|
|
||||||
|
public record AccountDataReportResponse(UUID reportId,
|
||||||
|
@JsonSerialize(using = InstantSerializer.class)
|
||||||
|
@JsonFormat(pattern = DATE_FORMAT, timezone = UTC)
|
||||||
|
Instant reportTimestamp,
|
||||||
|
AccountAndDevicesDataReport data) {
|
||||||
|
|
||||||
|
private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
|
||||||
|
private static final String UTC = "Etc/UTC";
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@Schema(description = "A plaintext representation of the data report")
|
||||||
|
String text() {
|
||||||
|
|
||||||
|
final StringBuilder builder = new StringBuilder();
|
||||||
|
|
||||||
|
// header
|
||||||
|
builder.append(String.format("""
|
||||||
|
Report ID: %s
|
||||||
|
Report timestamp: %s
|
||||||
|
|
||||||
|
""",
|
||||||
|
reportId,
|
||||||
|
reportTimestamp.truncatedTo(ChronoUnit.SECONDS)));
|
||||||
|
|
||||||
|
// account
|
||||||
|
builder.append(String.format("""
|
||||||
|
# Account
|
||||||
|
Phone number: %s
|
||||||
|
Allow sealed sender from anyone: %s
|
||||||
|
Find account by phone number: %s
|
||||||
|
""",
|
||||||
|
data.account.phoneNumber(),
|
||||||
|
data.account.allowSealedSenderFromAnyone(),
|
||||||
|
data.account.findAccountByPhoneNumber()));
|
||||||
|
|
||||||
|
// badges
|
||||||
|
builder.append("Badges:");
|
||||||
|
|
||||||
|
if (data.account.badges().isEmpty()) {
|
||||||
|
builder.append(" None\n");
|
||||||
|
} else {
|
||||||
|
builder.append("\n");
|
||||||
|
data.account.badges().forEach(badgeDataReport -> builder.append(String.format("""
|
||||||
|
- ID: %s
|
||||||
|
Expiration: %s
|
||||||
|
Visible: %s
|
||||||
|
""",
|
||||||
|
badgeDataReport.id(),
|
||||||
|
badgeDataReport.expiration().truncatedTo(ChronoUnit.SECONDS),
|
||||||
|
badgeDataReport.visible())));
|
||||||
|
}
|
||||||
|
|
||||||
|
// devices
|
||||||
|
builder.append("\n# Devices\n");
|
||||||
|
|
||||||
|
data.devices().forEach(deviceDataReport ->
|
||||||
|
builder.append(String.format("""
|
||||||
|
- ID: %s
|
||||||
|
Created: %s
|
||||||
|
Last seen: %s
|
||||||
|
User-agent: %s
|
||||||
|
""",
|
||||||
|
deviceDataReport.id(),
|
||||||
|
deviceDataReport.created().truncatedTo(ChronoUnit.SECONDS),
|
||||||
|
deviceDataReport.lastSeen().truncatedTo(ChronoUnit.SECONDS),
|
||||||
|
deviceDataReport.userAgent())));
|
||||||
|
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public record AccountAndDevicesDataReport(AccountDataReport account,
|
||||||
|
List<DeviceDataReport> devices) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public record AccountDataReport(String phoneNumber, List<BadgeDataReport> badges, boolean allowSealedSenderFromAnyone,
|
||||||
|
boolean findAccountByPhoneNumber) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DeviceDataReport(long id,
|
||||||
|
@JsonFormat(pattern = DATE_FORMAT, timezone = UTC)
|
||||||
|
Instant lastSeen,
|
||||||
|
@JsonFormat(pattern = DATE_FORMAT, timezone = UTC)
|
||||||
|
Instant created,
|
||||||
|
@Nullable String userAgent) {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public record BadgeDataReport(String id,
|
||||||
|
@JsonFormat(pattern = DATE_FORMAT, timezone = UTC)
|
||||||
|
Instant expiration,
|
||||||
|
boolean visible) {
|
||||||
|
|
||||||
|
public BadgeDataReport(AccountBadge badge) {
|
||||||
|
this(badge.getId(), badge.getExpiration(), badge.isVisible());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.controllers;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
@ -20,17 +21,25 @@ import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||||
|
import com.google.i18n.phonenumbers.Phonenumber;
|
||||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Clock;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.ws.rs.WebApplicationException;
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
@ -57,10 +66,13 @@ import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccou
|
||||||
import org.whispersystems.textsecuregcm.auth.PhoneVerificationTokenManager;
|
import org.whispersystems.textsecuregcm.auth.PhoneVerificationTokenManager;
|
||||||
import org.whispersystems.textsecuregcm.auth.RegistrationLockError;
|
import org.whispersystems.textsecuregcm.auth.RegistrationLockError;
|
||||||
import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager;
|
import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.AccountDataReportResponse;
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
|
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
|
||||||
import org.whispersystems.textsecuregcm.entities.ChangeNumberRequest;
|
import org.whispersystems.textsecuregcm.entities.ChangeNumberRequest;
|
||||||
import org.whispersystems.textsecuregcm.entities.PhoneNumberDiscoverabilityRequest;
|
import org.whispersystems.textsecuregcm.entities.PhoneNumberDiscoverabilityRequest;
|
||||||
import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession;
|
import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper;
|
||||||
|
@ -68,6 +80,7 @@ import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptio
|
||||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
|
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountBadge;
|
||||||
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.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
@ -75,6 +88,7 @@ import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsMan
|
||||||
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
|
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
|
||||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||||
class AccountControllerV2Test {
|
class AccountControllerV2Test {
|
||||||
|
@ -441,14 +455,203 @@ class AccountControllerV2Test {
|
||||||
.request()
|
.request()
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||||
.put(Entity.json(
|
.put(Entity.json(
|
||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"discoverableByPhoneNumber": null
|
"discoverableByPhoneNumber": null
|
||||||
}
|
}
|
||||||
"""));
|
"""));
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(422);
|
assertThat(response.getStatus()).isEqualTo(422);
|
||||||
verify(AuthHelper.VALID_ACCOUNT, never()).setDiscoverableByPhoneNumber(anyBoolean());
|
verify(AuthHelper.VALID_ACCOUNT, never()).setDiscoverableByPhoneNumber(anyBoolean());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource
|
||||||
|
void testGetAccountDataReport(final Account account, final String expectedTextAfterHeader) throws Exception {
|
||||||
|
when(AuthHelper.ACCOUNTS_MANAGER.getByAccountIdentifier(account.getUuid())).thenReturn(Optional.of(account));
|
||||||
|
|
||||||
|
final Response response = resources.getJerseyTest()
|
||||||
|
.target("/v2/accounts/data_report")
|
||||||
|
.request()
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(account.getUuid(), "password"))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
|
||||||
|
final String stringResponse = response.readEntity(String.class);
|
||||||
|
|
||||||
|
final AccountDataReportResponse structuredResponse = SystemMapper.jsonMapper()
|
||||||
|
.readValue(stringResponse, AccountDataReportResponse.class);
|
||||||
|
|
||||||
|
assertEquals(account.getNumber(), structuredResponse.data().account().phoneNumber());
|
||||||
|
assertEquals(account.isDiscoverableByPhoneNumber(),
|
||||||
|
structuredResponse.data().account().findAccountByPhoneNumber());
|
||||||
|
assertEquals(account.isUnrestrictedUnidentifiedAccess(),
|
||||||
|
structuredResponse.data().account().allowSealedSenderFromAnyone());
|
||||||
|
|
||||||
|
final Set<Long> deviceIds = account.getDevices().stream().map(Device::getId).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// all devices should be present
|
||||||
|
structuredResponse.data().devices().forEach(deviceDataReport -> {
|
||||||
|
assertTrue(deviceIds.remove(deviceDataReport.id()));
|
||||||
|
assertEquals(account.getDevice(deviceDataReport.id()).orElseThrow().getUserAgent(),
|
||||||
|
deviceDataReport.userAgent());
|
||||||
|
});
|
||||||
|
assertTrue(deviceIds.isEmpty());
|
||||||
|
|
||||||
|
final String actualText = (String) SystemMapper.jsonMapper().readValue(stringResponse, Map.class).get("text");
|
||||||
|
final int headerEnd = actualText.indexOf("# Account");
|
||||||
|
assertEquals(expectedTextAfterHeader, actualText.substring(headerEnd));
|
||||||
|
|
||||||
|
final String actualHeader = actualText.substring(0, headerEnd);
|
||||||
|
assertTrue(actualHeader.matches(
|
||||||
|
"Report ID: [a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}\nReport timestamp: \\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z\n\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Stream<Arguments> testGetAccountDataReport() {
|
||||||
|
final String exampleNumber1 = toE164(PhoneNumberUtil.getInstance().getExampleNumber("ES"));
|
||||||
|
final String account2PhoneNumber = toE164(PhoneNumberUtil.getInstance().getExampleNumber("AU"));
|
||||||
|
final String account3PhoneNumber = toE164(PhoneNumberUtil.getInstance().getExampleNumber("IN"));
|
||||||
|
|
||||||
|
final Instant account1Device1Created = Instant.ofEpochSecond(1669323142); // 2022-11-24T20:52:22Z
|
||||||
|
final Instant account1Device2Created = Instant.ofEpochSecond(1679155122); // 2023-03-18T15:58:42Z
|
||||||
|
final Instant account1Device1LastSeen = Instant.ofEpochMilli(Util.todayInMillis());
|
||||||
|
final Instant account1Device2LastSeen = Instant.ofEpochSecond(1678838400); // 2023-03-15T00:00:00Z
|
||||||
|
|
||||||
|
final Instant account2Device1Created = Instant.ofEpochSecond(1659123001); // 2022-07-29T19:30:01Z
|
||||||
|
final Instant account2Device1LastSeen = Instant.ofEpochMilli(Util.todayInMillis());
|
||||||
|
final Instant badgeAExpiration = Instant.now().plus(Duration.ofDays(21)).truncatedTo(ChronoUnit.SECONDS);
|
||||||
|
|
||||||
|
final Instant account3Device1Created = Instant.ofEpochSecond(1639923487); // 2021-12-19T14:18:07Z
|
||||||
|
final Instant account3Device1LastSeen = Instant.ofEpochMilli(Util.todayInMillis());
|
||||||
|
final Instant badgeBExpiration = Instant.now().plus(Duration.ofDays(21)).truncatedTo(ChronoUnit.SECONDS);
|
||||||
|
final Instant badgeCExpiration = Instant.now().plus(Duration.ofDays(24)).truncatedTo(ChronoUnit.SECONDS);
|
||||||
|
|
||||||
|
return Stream.of(
|
||||||
|
Arguments.of(
|
||||||
|
buildTestAccountForDataReport(UUID.randomUUID(), exampleNumber1,
|
||||||
|
true, true,
|
||||||
|
Collections.emptyList(),
|
||||||
|
List.of(new DeviceData(1, account1Device1LastSeen, account1Device1Created, null),
|
||||||
|
new DeviceData(2, account1Device2LastSeen, account1Device2Created, "OWP"))),
|
||||||
|
String.format("""
|
||||||
|
# Account
|
||||||
|
Phone number: %s
|
||||||
|
Allow sealed sender from anyone: true
|
||||||
|
Find account by phone number: true
|
||||||
|
Badges: None
|
||||||
|
|
||||||
|
# Devices
|
||||||
|
- ID: 1
|
||||||
|
Created: 2022-11-24T20:52:22Z
|
||||||
|
Last seen: %s
|
||||||
|
User-agent: null
|
||||||
|
- ID: 2
|
||||||
|
Created: 2023-03-18T15:58:42Z
|
||||||
|
Last seen: 2023-03-15T00:00:00Z
|
||||||
|
User-agent: OWP
|
||||||
|
""",
|
||||||
|
exampleNumber1,
|
||||||
|
account1Device1LastSeen)
|
||||||
|
),
|
||||||
|
Arguments.of(
|
||||||
|
buildTestAccountForDataReport(UUID.randomUUID(), account2PhoneNumber,
|
||||||
|
false, true,
|
||||||
|
List.of(new AccountBadge("badge_a", badgeAExpiration, true)),
|
||||||
|
List.of(new DeviceData(1, account2Device1LastSeen, account2Device1Created, "OWI"))),
|
||||||
|
String.format("""
|
||||||
|
# Account
|
||||||
|
Phone number: %s
|
||||||
|
Allow sealed sender from anyone: false
|
||||||
|
Find account by phone number: true
|
||||||
|
Badges:
|
||||||
|
- ID: badge_a
|
||||||
|
Expiration: %s
|
||||||
|
Visible: true
|
||||||
|
|
||||||
|
# Devices
|
||||||
|
- ID: 1
|
||||||
|
Created: 2022-07-29T19:30:01Z
|
||||||
|
Last seen: %s
|
||||||
|
User-agent: OWI
|
||||||
|
""", account2PhoneNumber,
|
||||||
|
badgeAExpiration,
|
||||||
|
account2Device1LastSeen)
|
||||||
|
),
|
||||||
|
Arguments.of(
|
||||||
|
buildTestAccountForDataReport(UUID.randomUUID(), account3PhoneNumber,
|
||||||
|
true, false,
|
||||||
|
List.of(
|
||||||
|
new AccountBadge("badge_b", badgeBExpiration, true),
|
||||||
|
new AccountBadge("badge_c", badgeCExpiration, false)),
|
||||||
|
List.of(new DeviceData(1, account3Device1LastSeen, account3Device1Created, "OWA"))),
|
||||||
|
String.format("""
|
||||||
|
# Account
|
||||||
|
Phone number: %s
|
||||||
|
Allow sealed sender from anyone: true
|
||||||
|
Find account by phone number: false
|
||||||
|
Badges:
|
||||||
|
- ID: badge_b
|
||||||
|
Expiration: %s
|
||||||
|
Visible: true
|
||||||
|
- ID: badge_c
|
||||||
|
Expiration: %s
|
||||||
|
Visible: false
|
||||||
|
|
||||||
|
# Devices
|
||||||
|
- ID: 1
|
||||||
|
Created: 2021-12-19T14:18:07Z
|
||||||
|
Last seen: %s
|
||||||
|
User-agent: OWA
|
||||||
|
""", account3PhoneNumber,
|
||||||
|
badgeBExpiration,
|
||||||
|
badgeCExpiration,
|
||||||
|
account3Device1LastSeen)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an {@link Account} with data sufficient for
|
||||||
|
* {@link AccountControllerV2#getAccountDataReport(AuthenticatedAccount)}.
|
||||||
|
* <p>
|
||||||
|
* Note: All devices will have a {@link SaltedTokenHash} for "password"
|
||||||
|
*/
|
||||||
|
static Account buildTestAccountForDataReport(final UUID aci, final String number,
|
||||||
|
final boolean unrestrictedUnidentifiedAccess, final boolean discoverableByPhoneNumber,
|
||||||
|
List<AccountBadge> badges, List<DeviceData> devices) {
|
||||||
|
final Account account = new Account();
|
||||||
|
account.setUuid(aci);
|
||||||
|
account.setNumber(number, UUID.randomUUID());
|
||||||
|
account.setUnrestrictedUnidentifiedAccess(unrestrictedUnidentifiedAccess);
|
||||||
|
account.setDiscoverableByPhoneNumber(discoverableByPhoneNumber);
|
||||||
|
account.setBadges(Clock.systemUTC(), new ArrayList<>(badges));
|
||||||
|
|
||||||
|
assert !devices.isEmpty();
|
||||||
|
|
||||||
|
final SaltedTokenHash passwordTokenHash = SaltedTokenHash.generateFor("password");
|
||||||
|
|
||||||
|
devices.forEach(deviceData -> {
|
||||||
|
final Device device = new Device();
|
||||||
|
device.setId(deviceData.id);
|
||||||
|
device.setAuthTokenHash(passwordTokenHash);
|
||||||
|
device.setFetchesMessages(true);
|
||||||
|
device.setSignedPreKey(new SignedPreKey(1, "publicKey", "signature"));
|
||||||
|
device.setLastSeen(deviceData.lastSeen().toEpochMilli());
|
||||||
|
device.setCreated(deviceData.created().toEpochMilli());
|
||||||
|
device.setUserAgent(deviceData.userAgent());
|
||||||
|
account.addDevice(device);
|
||||||
|
});
|
||||||
|
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record DeviceData(long id, Instant lastSeen, Instant created, @Nullable String userAgent) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String toE164(Phonenumber.PhoneNumber phoneNumber) {
|
||||||
|
return PhoneNumberUtil.getInstance().format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue