_id INTEGER PRIMARY KEY AUTOINCREMENT,
number TEXT UNIQUE,
uuid BLOB UNIQUE,
+ pni BLOB UNIQUE,
profile_key BLOB,
profile_key_credential BLOB,
public RecipientAddress resolveRecipientAddress(RecipientId recipientId) {
final var sql = (
"""
- SELECT r.number, r.uuid
+ SELECT r.number, r.uuid, r.pni
FROM %s r
WHERE r._id = ?
"""
final Optional<ACI> aci, final Optional<PNI> pni, final Optional<String> number
) {
final var serviceId = aci.map(a -> (ServiceId) a).or(() -> pni);
- return resolveRecipientTrusted(new RecipientAddress(serviceId, number), false);
+ return resolveRecipientTrusted(new RecipientAddress(serviceId, pni, number), false);
}
@Override
final var sql = (
"""
SELECT r._id,
- r.number, r.uuid,
+ r.number, r.uuid, r.pni,
r.profile_key, r.profile_key_credential,
r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived,
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities
}
private RecipientId resolveRecipientTrusted(RecipientAddress address, boolean isSelf) {
- final Pair<RecipientId, Optional<RecipientId>> pair;
+ final Pair<RecipientId, List<RecipientId>> pair;
synchronized (recipientsLock) {
try (final var connection = database.getConnection()) {
connection.setAutoCommit(false);
- pair = resolveRecipientTrustedLocked(connection, address, isSelf);
+ if (address.hasSingleIdentifier() || (
+ !isSelf && selfAddressProvider.getSelfAddress().matches(address)
+ )) {
+ pair = new Pair<>(resolveRecipientLocked(connection, address), List.of());
+ } else {
+ pair = MergeRecipientHelper.resolveRecipientTrustedLocked(new HelperStore(connection), address);
+
+ for (final var toBeMergedRecipientId : pair.second()) {
+ mergeRecipientsLocked(connection, pair.first(), toBeMergedRecipientId);
+ }
+ }
connection.commit();
} catch (SQLException e) {
throw new RuntimeException("Failed update recipient store", e);
}
}
- if (pair.second().isPresent()) {
+ if (pair.second().size() > 0) {
try (final var connection = database.getConnection()) {
- recipientMergeHandler.mergeRecipients(connection, pair.first(), pair.second().get());
- deleteRecipient(connection, pair.second().get());
+ for (final var toBeMergedRecipientId : pair.second()) {
+ recipientMergeHandler.mergeRecipients(connection, pair.first(), toBeMergedRecipientId);
+ deleteRecipient(connection, toBeMergedRecipientId);
+ }
} catch (SQLException e) {
throw new RuntimeException("Failed update recipient store", e);
}
return pair.first();
}
- private Pair<RecipientId, Optional<RecipientId>> resolveRecipientTrustedLocked(
- Connection connection, RecipientAddress address, boolean isSelf
- ) throws SQLException {
- if (!isSelf) {
- if (selfAddressProvider.getSelfAddress().matches(address)) {
- return new Pair<>(resolveRecipientLocked(connection, address), Optional.empty());
- }
- }
- final var byNumber = address.number().isEmpty()
- ? Optional.<RecipientWithAddress>empty()
- : findByNumber(connection, address.number().get());
- final var byUuid = address.serviceId().isEmpty()
- ? Optional.<RecipientWithAddress>empty()
- : findByServiceId(connection, address.serviceId().get());
-
- if (byNumber.isEmpty() && byUuid.isEmpty()) {
- logger.debug("Got new recipient, both uuid and number are unknown");
- return new Pair<>(addNewRecipient(connection, address), Optional.empty());
- }
-
- if (address.serviceId().isEmpty() || address.number().isEmpty() || byNumber.equals(byUuid)) {
- return new Pair<>(byUuid.or(() -> byNumber).map(RecipientWithAddress::id).get(), Optional.empty());
- }
-
- if (byNumber.isEmpty()) {
- logger.debug("Got recipient {} existing with uuid, updating with high trust number", byUuid.get().id());
- updateRecipientAddress(connection, byUuid.get().id(), address);
- return new Pair<>(byUuid.get().id(), Optional.empty());
- }
-
- final var byNumberRecipient = byNumber.get();
-
- if (byUuid.isEmpty()) {
- if (byNumberRecipient.address().serviceId().isPresent()) {
- logger.debug(
- "Got recipient {} existing with number, but different uuid, so stripping its number and adding new recipient",
- byNumberRecipient.id());
-
- updateRecipientAddress(connection,
- byNumberRecipient.id(),
- new RecipientAddress(byNumberRecipient.address().serviceId().get()));
- return new Pair<>(addNewRecipient(connection, address), Optional.empty());
- }
-
- logger.debug("Got recipient {} existing with number and no uuid, updating with high trust uuid",
- byNumberRecipient.id());
- updateRecipientAddress(connection, byNumberRecipient.id(), address);
- return new Pair<>(byNumberRecipient.id(), Optional.empty());
- }
-
- final var byUuidRecipient = byUuid.get();
-
- if (byNumberRecipient.address().serviceId().isPresent()) {
- logger.debug(
- "Got separate recipients for high trust number {} and uuid {}, recipient for number has different uuid, so stripping its number",
- byNumberRecipient.id(),
- byUuidRecipient.id());
-
- updateRecipientAddress(connection,
- byNumberRecipient.id(),
- new RecipientAddress(byNumberRecipient.address().serviceId().get()));
- updateRecipientAddress(connection, byUuidRecipient.id(), address);
- return new Pair<>(byUuidRecipient.id(), Optional.empty());
- }
-
- logger.debug("Got separate recipients for high trust number {} and uuid {}, need to merge them",
- byNumberRecipient.id(),
- byUuidRecipient.id());
- // Create a fixed RecipientId that won't update its id after merge
- final var toBeMergedRecipientId = new RecipientId(byNumberRecipient.id().id(), null);
- mergeRecipientsLocked(connection, byUuidRecipient.id(), toBeMergedRecipientId);
- removeRecipientAddress(connection, toBeMergedRecipientId);
- updateRecipientAddress(connection, byUuidRecipient.id(), address);
- return new Pair<>(byUuidRecipient.id(), Optional.of(toBeMergedRecipientId));
- }
-
private RecipientId resolveRecipientLocked(
Connection connection, RecipientAddress address
) throws SQLException {
) throws SQLException {
final var sql = (
"""
- INSERT INTO %s (number, uuid)
- VALUES (?, ?)
+ INSERT INTO %s (number, uuid, pni)
+ VALUES (?, ?, ?)
"""
).formatted(TABLE_RECIPIENT);
try (final var statement = connection.prepareStatement(sql)) {
statement.setString(1, address.number().orElse(null));
statement.setBytes(2, address.serviceId().map(ServiceId::uuid).map(UuidUtil::toByteArray).orElse(null));
+ statement.setBytes(3, address.pni().map(PNI::uuid).map(UuidUtil::toByteArray).orElse(null));
statement.executeUpdate();
final var generatedKeys = statement.getGeneratedKeys();
if (generatedKeys.next()) {
final var sql = (
"""
UPDATE %s
- SET number = NULL, uuid = NULL
+ SET number = NULL, uuid = NULL, pni = NULL
WHERE _id = ?
"""
).formatted(TABLE_RECIPIENT);
final var sql = (
"""
UPDATE %s
- SET number = ?, uuid = ?
+ SET number = ?, uuid = ?, pni = ?
WHERE _id = ?
"""
).formatted(TABLE_RECIPIENT);
try (final var statement = connection.prepareStatement(sql)) {
statement.setString(1, address.number().orElse(null));
statement.setBytes(2, address.serviceId().map(ServiceId::uuid).map(UuidUtil::toByteArray).orElse(null));
- statement.setLong(3, recipientId.id());
+ statement.setBytes(3, address.pni().map(PNI::uuid).map(UuidUtil::toByteArray).orElse(null));
+ statement.setLong(4, recipientId.id());
statement.executeUpdate();
}
}
final Connection connection, final String number
) throws SQLException {
final var sql = """
- SELECT r._id, r.number, r.uuid
+ SELECT r._id, r.number, r.uuid, r.pni
FROM %s r
WHERE r.number = ?
+ LIMIT 1
""".formatted(TABLE_RECIPIENT);
try (final var statement = connection.prepareStatement(sql)) {
statement.setString(1, number);
final Connection connection, final ServiceId serviceId
) throws SQLException {
final var sql = """
- SELECT r._id, r.number, r.uuid
+ SELECT r._id, r.number, r.uuid, r.pni
FROM %s r
- WHERE r.uuid = ?
+ WHERE r.uuid = ? OR r.pni = ?
+ LIMIT 1
""".formatted(TABLE_RECIPIENT);
try (final var statement = connection.prepareStatement(sql)) {
statement.setBytes(1, UuidUtil.toByteArray(serviceId.uuid()));
}
}
+ private Set<RecipientWithAddress> findAllByAddress(
+ final Connection connection, final RecipientAddress address
+ ) throws SQLException {
+ final var sql = """
+ SELECT r._id, r.number, r.uuid, r.pni
+ FROM %s r
+ WHERE r.uuid = ?1 OR r.pni = ?1 OR
+ r.uuid = ?2 OR r.pni = ?2 OR
+ r.number = ?3
+ """.formatted(TABLE_RECIPIENT);
+ try (final var statement = connection.prepareStatement(sql)) {
+ statement.setBytes(1, address.serviceId().map(ServiceId::uuid).map(UuidUtil::toByteArray).orElse(null));
+ statement.setBytes(2, address.pni().map(ServiceId::uuid).map(UuidUtil::toByteArray).orElse(null));
+ statement.setString(3, address.number().orElse(null));
+ return Utils.executeQueryForStream(statement, this::getRecipientWithAddressFromResultSet)
+ .collect(Collectors.toSet());
+ }
+ }
+
private Contact getContact(final Connection connection, final RecipientId recipientId) throws SQLException {
final var sql = (
"""
private RecipientAddress getRecipientAddressFromResultSet(ResultSet resultSet) throws SQLException {
final var serviceId = Optional.ofNullable(resultSet.getBytes("uuid")).map(ServiceId::parseOrNull);
+ final var pni = Optional.ofNullable(resultSet.getBytes("pni")).map(PNI::parseOrNull);
final var number = Optional.ofNullable(resultSet.getString("number"));
- return new RecipientAddress(serviceId, Optional.empty(), number);
+ return new RecipientAddress(serviceId, pni, number);
}
private RecipientId getRecipientIdFromResultSet(ResultSet resultSet) throws SQLException {
) throws SQLException;
}
- private record RecipientWithAddress(RecipientId id, RecipientAddress address) {}
+ private class HelperStore implements MergeRecipientHelper.Store {
+
+ private final Connection connection;
+
+ public HelperStore(final Connection connection) {
+ this.connection = connection;
+ }
+
+ @Override
+ public Set<RecipientWithAddress> findAllByAddress(final RecipientAddress address) throws SQLException {
+ return RecipientStore.this.findAllByAddress(connection, address);
+ }
+
+ @Override
+ public RecipientId addNewRecipient(final RecipientAddress address) throws SQLException {
+ return RecipientStore.this.addNewRecipient(connection, address);
+ }
+
+ @Override
+ public void updateRecipientAddress(
+ final RecipientId recipientId, final RecipientAddress address
+ ) throws SQLException {
+ RecipientStore.this.updateRecipientAddress(connection, recipientId, address);
+ }
+
+ @Override
+ public void removeRecipientAddress(final RecipientId recipientId) throws SQLException {
+ RecipientStore.this.removeRecipientAddress(connection, recipientId);
+ }
+ }
}
--- /dev/null
+package org.asamk.signal.manager.storage.recipients;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.whispersystems.signalservice.api.push.PNI;
+import org.whispersystems.signalservice.api.push.ServiceId;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class MergeRecipientHelperTest {
+
+ static final ServiceId SERVICE_ID_A = ServiceId.from(UUID.randomUUID());
+ static final ServiceId SERVICE_ID_B = ServiceId.from(UUID.randomUUID());
+ static final ServiceId SERVICE_ID_C = ServiceId.from(UUID.randomUUID());
+ static final PNI PNI_A = PNI.from(UUID.randomUUID());
+ static final PNI PNI_B = PNI.from(UUID.randomUUID());
+ static final PNI PNI_C = PNI.from(UUID.randomUUID());
+ static final String NUMBER_A = "+AAA";
+ static final String NUMBER_B = "+BBB";
+ static final String NUMBER_C = "+CCC";
+
+ static final PartialAddresses ADDR_A = new PartialAddresses(SERVICE_ID_A, PNI_A, NUMBER_A);
+ static final PartialAddresses ADDR_B = new PartialAddresses(SERVICE_ID_B, PNI_B, NUMBER_B);
+
+ static T[] testInstancesNone = new T[]{
+ // 1
+ new T(Set.of(), ADDR_A.FULL, Set.of(rec(1000000, ADDR_A.FULL))),
+ new T(Set.of(), ADDR_A.ACI_NUM, Set.of(rec(1000000, ADDR_A.ACI_NUM))),
+ new T(Set.of(), ADDR_A.ACI_PNI, Set.of(rec(1000000, ADDR_A.ACI_PNI))),
+ new T(Set.of(), ADDR_A.PNI_S_NUM, Set.of(rec(1000000, ADDR_A.PNI_S_NUM))),
+ new T(Set.of(), ADDR_A.PNI_NUM, Set.of(rec(1000000, ADDR_A.PNI_NUM))),
+ };
+
+ static T[] testInstancesSingle = new T[]{
+ // 1
+ new T(Set.of(rec(1, ADDR_A.FULL)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.ACI)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.PNI)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.PNI_S)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.ACI_NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.PNI_NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.PNI_S_NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.ACI_PNI)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
+
+ // 10
+ new T(Set.of(rec(1, ADDR_A.FULL)), ADDR_A.ACI_NUM, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.ACI)), ADDR_A.ACI_NUM, Set.of(rec(1, ADDR_A.ACI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.PNI)), ADDR_A.ACI_NUM, Set.of(rec(1, ADDR_A.PNI), rec(1000000, ADDR_A.ACI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.PNI_S)),
+ ADDR_A.ACI_NUM,
+ Set.of(rec(1, ADDR_A.PNI_S), rec(1000000, ADDR_A.ACI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.NUM)), ADDR_A.ACI_NUM, Set.of(rec(1, ADDR_A.ACI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.ACI_NUM)), ADDR_A.ACI_NUM, Set.of(rec(1, ADDR_A.ACI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.PNI_NUM)),
+ ADDR_A.ACI_NUM,
+ Set.of(rec(1, ADDR_A.PNI), rec(1000000, ADDR_A.ACI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.PNI_S_NUM)),
+ ADDR_A.ACI_NUM,
+ Set.of(rec(1, ADDR_A.PNI_S), rec(1000000, ADDR_A.ACI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.ACI_PNI)), ADDR_A.ACI_NUM, Set.of(rec(1, ADDR_A.FULL))),
+
+ // 19
+ new T(Set.of(rec(1, ADDR_A.FULL)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.ACI)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.ACI), rec(1000000, ADDR_A.PNI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.PNI)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.PNI_S)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.NUM)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.ACI_NUM)),
+ ADDR_A.PNI_NUM,
+ Set.of(rec(1, ADDR_A.ACI), rec(1000000, ADDR_A.PNI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.PNI_NUM)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.PNI_S_NUM)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.ACI_PNI)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.FULL))),
+
+ // 28
+ new T(Set.of(rec(1, ADDR_A.FULL)), ADDR_A.PNI_S_NUM, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.ACI)),
+ ADDR_A.PNI_S_NUM,
+ Set.of(rec(1, ADDR_A.ACI), rec(1000000, ADDR_A.PNI_S_NUM))),
+ new T(Set.of(rec(1, ADDR_A.PNI)), ADDR_A.PNI_S_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.PNI_S)), ADDR_A.PNI_S_NUM, Set.of(rec(1, ADDR_A.PNI_S_NUM))),
+ new T(Set.of(rec(1, ADDR_A.NUM)), ADDR_A.PNI_S_NUM, Set.of(rec(1, ADDR_A.PNI_S_NUM))),
+ new T(Set.of(rec(1, ADDR_A.ACI_NUM)),
+ ADDR_A.PNI_S_NUM,
+ Set.of(rec(1, ADDR_A.ACI), rec(1000000, ADDR_A.PNI_S_NUM))),
+ new T(Set.of(rec(1, ADDR_A.PNI_NUM)), ADDR_A.PNI_S_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.PNI_S_NUM)), ADDR_A.PNI_S_NUM, Set.of(rec(1, ADDR_A.PNI_S_NUM))),
+ new T(Set.of(rec(1, ADDR_A.ACI_PNI)), ADDR_A.PNI_S_NUM, Set.of(rec(1, ADDR_A.FULL))),
+
+ // 37
+ new T(Set.of(rec(1, ADDR_A.FULL)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.ACI)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.ACI_PNI))),
+ new T(Set.of(rec(1, ADDR_A.PNI)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.ACI_PNI))),
+ new T(Set.of(rec(1, ADDR_A.PNI_S)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.ACI_PNI))),
+ new T(Set.of(rec(1, ADDR_A.NUM)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.NUM), rec(1000000, ADDR_A.ACI_PNI))),
+ new T(Set.of(rec(1, ADDR_A.ACI_NUM)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.PNI_NUM)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.PNI_S_NUM)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.ACI_PNI)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.ACI_PNI))),
+
+ new T(Set.of(rec(1, ADDR_A.FULL)), ADDR_B.FULL, Set.of(rec(1, ADDR_A.FULL), rec(1000000, ADDR_B.FULL))),
+ };
+
+ static T[] testInstancesTwo = new T[]{
+ // 1
+ new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_S)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_S_NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.PNI), rec(2, ADDR_A.NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.PNI), rec(2, ADDR_A.ACI_NUM)), ADDR_A.FULL, Set.of(rec(2, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.PNI_S), rec(2, ADDR_A.NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.PNI_S), rec(2, ADDR_A.ACI_NUM)), ADDR_A.FULL, Set.of(rec(2, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.NUM), rec(2, ADDR_A.PNI_S)), ADDR_A.FULL, Set.of(rec(2, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.NUM), rec(2, ADDR_A.ACI_PNI)), ADDR_A.FULL, Set.of(rec(2, ADDR_A.FULL))),
+
+ // 12
+ new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.NUM)), ADDR_A.ACI_NUM, Set.of(rec(1, ADDR_A.ACI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_NUM)), ADDR_A.ACI_NUM, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_S_NUM)),
+ ADDR_A.ACI_NUM,
+ Set.of(rec(1, ADDR_A.ACI_NUM), rec(2, ADDR_A.PNI_S))),
+ new T(Set.of(rec(1, ADDR_A.NUM), rec(2, ADDR_A.ACI_PNI)), ADDR_A.ACI_NUM, Set.of(rec(2, ADDR_A.FULL))),
+
+ // 16
+ new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_NUM)),
+ ADDR_A.PNI_NUM,
+ Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_S_NUM)),
+ ADDR_A.PNI_NUM,
+ Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.PNI), rec(2, ADDR_A.NUM)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.PNI), rec(2, ADDR_A.ACI_NUM)),
+ ADDR_A.PNI_NUM,
+ Set.of(rec(1, ADDR_A.PNI_NUM), rec(2, ADDR_A.ACI))),
+ new T(Set.of(rec(1, ADDR_A.PNI_S), rec(2, ADDR_A.NUM)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.PNI_S), rec(2, ADDR_A.ACI_NUM)),
+ ADDR_A.PNI_NUM,
+ Set.of(rec(1, ADDR_A.PNI_NUM), rec(2, ADDR_A.ACI))),
+ new T(Set.of(rec(1, ADDR_A.NUM), rec(2, ADDR_A.PNI_S)), ADDR_A.PNI_NUM, Set.of(rec(2, ADDR_A.PNI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.NUM), rec(2, ADDR_A.ACI_PNI)), ADDR_A.PNI_NUM, Set.of(rec(2, ADDR_A.FULL))),
+
+ // 24
+ new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_NUM)),
+ ADDR_A.PNI_S_NUM,
+ Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_S_NUM)),
+ ADDR_A.PNI_S_NUM,
+ Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_S_NUM))),
+ new T(Set.of(rec(1, ADDR_A.PNI), rec(2, ADDR_A.NUM)), ADDR_A.PNI_S_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
+ new T(Set.of(rec(1, ADDR_A.PNI), rec(2, ADDR_A.ACI_NUM)),
+ ADDR_A.PNI_S_NUM,
+ Set.of(rec(1, ADDR_A.PNI_NUM), rec(2, ADDR_A.ACI))),
+ new T(Set.of(rec(1, ADDR_A.PNI_S), rec(2, ADDR_A.NUM)), ADDR_A.PNI_S_NUM, Set.of(rec(1, ADDR_A.PNI_S_NUM))),
+ new T(Set.of(rec(1, ADDR_A.PNI_S), rec(2, ADDR_A.ACI_NUM)),
+ ADDR_A.PNI_S_NUM,
+ Set.of(rec(1, ADDR_A.PNI_S_NUM), rec(2, ADDR_A.ACI))),
+ new T(Set.of(rec(1, ADDR_A.NUM), rec(2, ADDR_A.PNI_S)), ADDR_A.PNI_S_NUM, Set.of(rec(2, ADDR_A.PNI_S_NUM))),
+ new T(Set.of(rec(1, ADDR_A.NUM), rec(2, ADDR_A.ACI_PNI)), ADDR_A.PNI_S_NUM, Set.of(rec(2, ADDR_A.FULL))),
+
+ // 32
+ new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.ACI_PNI))),
+ new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_S)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.ACI_PNI))),
+ new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_NUM)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_S_NUM)),
+ ADDR_A.ACI_PNI,
+ Set.of(rec(1, ADDR_A.ACI_PNI), rec(2, ADDR_A.NUM))),
+ new T(Set.of(rec(1, ADDR_A.PNI), rec(2, ADDR_A.ACI_NUM)), ADDR_A.ACI_PNI, Set.of(rec(2, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.PNI_S), rec(2, ADDR_A.ACI_NUM)), ADDR_A.ACI_PNI, Set.of(rec(2, ADDR_A.FULL))),
+ };
+
+ static T[] testInstancesThree = new T[]{
+ // 1
+ new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI), rec(3, ADDR_A.NUM)),
+ ADDR_A.FULL,
+ Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.ACI.withIdentifiersFrom(ADDR_B.PNI)), rec(2, ADDR_A.PNI), rec(3, ADDR_A.NUM)),
+ ADDR_A.FULL,
+ Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.ACI.withIdentifiersFrom(ADDR_B.NUM)), rec(2, ADDR_A.PNI), rec(3, ADDR_A.NUM)),
+ ADDR_A.FULL,
+ Set.of(rec(1, ADDR_A.FULL))),
+ new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI), rec(3, ADDR_A.NUM.withIdentifiersFrom(ADDR_B.ACI))),
+ ADDR_A.FULL,
+ Set.of(rec(1, ADDR_A.FULL), rec(3, ADDR_B.ACI))),
+ new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI.withIdentifiersFrom(ADDR_B.ACI)), rec(3, ADDR_A.NUM)),
+ ADDR_A.FULL,
+ Set.of(rec(1, ADDR_A.FULL), rec(2, ADDR_B.ACI))),
+ };
+
+ @ParameterizedTest
+ @MethodSource
+ void resolveRecipientTrustedLocked_NoneExisting(T test) throws Exception {
+ final var testStore = new TestStore(test.input);
+ MergeRecipientHelper.resolveRecipientTrustedLocked(testStore, test.request);
+ assertEquals(test.output, testStore.getRecipients());
+ }
+
+ private static Stream<Arguments> resolveRecipientTrustedLocked_NoneExisting() {
+ return Arrays.stream(testInstancesNone).map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void resolveRecipientTrustedLocked_SingleExisting(T test) throws Exception {
+ final var testStore = new TestStore(test.input);
+ MergeRecipientHelper.resolveRecipientTrustedLocked(testStore, test.request);
+ assertEquals(test.output, testStore.getRecipients());
+ }
+
+ private static Stream<Arguments> resolveRecipientTrustedLocked_SingleExisting() {
+ return Arrays.stream(testInstancesSingle).map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void resolveRecipientTrustedLocked_TwoExisting(T test) throws Exception {
+ final var testStore = new TestStore(test.input);
+ MergeRecipientHelper.resolveRecipientTrustedLocked(testStore, test.request);
+ assertEquals(test.output, testStore.getRecipients());
+ }
+
+ private static Stream<Arguments> resolveRecipientTrustedLocked_TwoExisting() {
+ return Arrays.stream(testInstancesTwo).map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void resolveRecipientTrustedLocked_ThreeExisting(T test) throws Exception {
+ final var testStore = new TestStore(test.input);
+ MergeRecipientHelper.resolveRecipientTrustedLocked(testStore, test.request);
+ assertEquals(test.output, testStore.getRecipients());
+ }
+
+ private static Stream<Arguments> resolveRecipientTrustedLocked_ThreeExisting() {
+ return Arrays.stream(testInstancesThree).map(Arguments::of);
+ }
+
+ private static RecipientWithAddress rec(long recipientId, RecipientAddress address) {
+ return new RecipientWithAddress(new RecipientId(recipientId, null), address);
+ }
+
+ record T(
+ Set<RecipientWithAddress> input, RecipientAddress request, Set<RecipientWithAddress> output
+ ) {
+
+ @Override
+ public String toString() {
+ return "T{#input=%s, request=%s_%s_%s, #output=%s}".formatted(input.size(),
+ request.serviceId().isPresent() ? "SVI" : "",
+ request.pni().isPresent() ? "PNI" : "",
+ request.number().isPresent() ? "NUM" : "",
+ output.size());
+ }
+ }
+
+ static class TestStore implements MergeRecipientHelper.Store {
+
+ final Set<RecipientWithAddress> recipients;
+ long nextRecipientId = 1000000;
+
+ TestStore(final Set<RecipientWithAddress> recipients) {
+ this.recipients = new HashSet<>(recipients);
+ }
+
+ public Set<RecipientWithAddress> getRecipients() {
+ return recipients;
+ }
+
+ @Override
+ public Set<RecipientWithAddress> findAllByAddress(final RecipientAddress address) {
+ return recipients.stream().filter(r -> r.address().matches(address)).collect(Collectors.toSet());
+ }
+
+ @Override
+ public RecipientId addNewRecipient(final RecipientAddress address) {
+ final var recipientId = new RecipientId(nextRecipientId++, null);
+ recipients.add(new RecipientWithAddress(recipientId, address));
+ return recipientId;
+ }
+
+ @Override
+ public void updateRecipientAddress(
+ final RecipientId recipientId, final RecipientAddress address
+ ) {
+ recipients.removeIf(r -> r.id().equals(recipientId));
+ recipients.add(new RecipientWithAddress(recipientId, address));
+ }
+
+ @Override
+ public void removeRecipientAddress(final RecipientId recipientId) {
+ recipients.removeIf(r -> r.id().equals(recipientId));
+ }
+ }
+
+ private record PartialAddresses(
+ RecipientAddress FULL,
+ RecipientAddress ACI,
+ RecipientAddress PNI,
+ RecipientAddress PNI_S,
+ RecipientAddress NUM,
+ RecipientAddress ACI_NUM,
+ RecipientAddress PNI_NUM,
+ RecipientAddress PNI_S_NUM,
+ RecipientAddress ACI_PNI
+ ) {
+
+ PartialAddresses(ServiceId serviceId, PNI pni, String number) {
+ this(new RecipientAddress(serviceId, pni, number),
+ new RecipientAddress(serviceId, null, null),
+ new RecipientAddress(null, pni, null),
+ new RecipientAddress(ServiceId.from(pni.uuid()), null, null),
+ new RecipientAddress(null, null, number),
+ new RecipientAddress(serviceId, null, number),
+ new RecipientAddress(null, pni, number),
+ new RecipientAddress(ServiceId.from(pni.uuid()), null, number),
+ new RecipientAddress(serviceId, pni, null));
+ }
+ }
+}