mirror of
https://github.com/google/nomulus
synced 2026-09-25 01:14:43 +00:00
Refactor contact history PII wipeout logic into a Beam pipeline (#1994)
Because we need to check if a contact history is the most recent for its underlying contact resource, the query-wipe out-repeat loop no longer works ideally due to the added overhead with the query. Instead, we refactor the logic into a Beam pipeline where the query only needs to be performed once and history entries eligible for wipe out are handled individually in their own transforms. Because history entries are otherwise immutable, we can run the pipeline in relatively relaxed repeatable read isolation level. We also do not worry about batching for performance, as we do not anticipate this operation to put a lot of strains on the particular table.
This commit is contained in:
@@ -15,348 +15,119 @@
|
||||
package google.registry.batch;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
import static com.google.common.truth.Truth8.assertThat;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static org.apache.http.HttpStatus.SC_OK;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.contact.Contact;
|
||||
import google.registry.model.contact.ContactAddress;
|
||||
import google.registry.model.contact.ContactAuthInfo;
|
||||
import google.registry.model.contact.ContactBase;
|
||||
import google.registry.model.contact.ContactHistory;
|
||||
import google.registry.model.contact.ContactPhoneNumber;
|
||||
import google.registry.model.contact.Disclose;
|
||||
import google.registry.model.contact.PostalInfo;
|
||||
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
|
||||
import google.registry.model.eppcommon.PresenceMarker;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
|
||||
import google.registry.testing.DatabaseHelper;
|
||||
import com.google.api.services.dataflow.model.LaunchFlexTemplateRequest;
|
||||
import google.registry.beam.BeamActionTestBase;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
/** Unit tests for {@link WipeOutContactHistoryPiiAction}. */
|
||||
class WipeOutContactHistoryPiiActionTest {
|
||||
class WipeOutContactHistoryPiiActionTest extends BeamActionTestBase {
|
||||
|
||||
private static final int TEST_BATCH_SIZE = 20;
|
||||
private static final int MIN_MONTHS_BEFORE_WIPE_OUT = 18;
|
||||
private static final Contact DEFAULT_CONTACT =
|
||||
new Contact.Builder()
|
||||
.setContactId("sh8013")
|
||||
.setRepoId("2FF-ROID")
|
||||
.setStatusValues(ImmutableSet.of(StatusValue.CLIENT_DELETE_PROHIBITED))
|
||||
.setLocalizedPostalInfo(
|
||||
new PostalInfo.Builder()
|
||||
.setType(PostalInfo.Type.LOCALIZED)
|
||||
.setAddress(
|
||||
new ContactAddress.Builder()
|
||||
.setStreet(ImmutableList.of("123 Grand Ave"))
|
||||
.build())
|
||||
.build())
|
||||
.setInternationalizedPostalInfo(
|
||||
new PostalInfo.Builder()
|
||||
.setType(PostalInfo.Type.INTERNATIONALIZED)
|
||||
.setName("John Doe")
|
||||
.setOrg("Example Inc.")
|
||||
.setAddress(
|
||||
new ContactAddress.Builder()
|
||||
.setStreet(ImmutableList.of("123 Example Dr.", "Suite 100"))
|
||||
.setCity("Dulles")
|
||||
.setState("VA")
|
||||
.setZip("20166-6503")
|
||||
.setCountryCode("US")
|
||||
.build())
|
||||
.build())
|
||||
.setVoiceNumber(
|
||||
new ContactPhoneNumber.Builder()
|
||||
.setPhoneNumber("+1.7035555555")
|
||||
.setExtension("1234")
|
||||
.build())
|
||||
.setFaxNumber(new ContactPhoneNumber.Builder().setPhoneNumber("+1.7035555556").build())
|
||||
.setEmailAddress("jdoe@example.com")
|
||||
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
|
||||
.setCreationRegistrarId("NewRegistrar")
|
||||
.setLastEppUpdateRegistrarId("NewRegistrar")
|
||||
.setCreationTimeForTest(DateTime.parse("1999-04-03T22:00:00.0Z"))
|
||||
.setLastEppUpdateTime(DateTime.parse("1999-12-03T09:00:00.0Z"))
|
||||
.setLastTransferTime(DateTime.parse("2000-04-08T09:00:00.0Z"))
|
||||
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("2fooBAR")))
|
||||
.setDisclose(
|
||||
new Disclose.Builder()
|
||||
.setFlag(true)
|
||||
.setVoice(new PresenceMarker())
|
||||
.setEmail(new PresenceMarker())
|
||||
.build())
|
||||
.build();
|
||||
|
||||
@RegisterExtension
|
||||
final JpaIntegrationTestExtension jpa =
|
||||
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
|
||||
|
||||
private final FakeClock clock = new FakeClock(DateTime.parse("2021-08-26T20:21:22Z"));
|
||||
|
||||
private FakeResponse response;
|
||||
private WipeOutContactHistoryPiiAction action;
|
||||
private final DateTime now = DateTime.parse("2019-01-19T01:02:03Z");
|
||||
private final FakeClock clock = new FakeClock(now);
|
||||
private final Map<String, String> expectedParameters = new HashMap<>();
|
||||
private final ArgumentCaptor<LaunchFlexTemplateRequest> launchRequest =
|
||||
ArgumentCaptor.forClass(LaunchFlexTemplateRequest.class);
|
||||
private WipeOutContactHistoryPiiAction action =
|
||||
new WipeOutContactHistoryPiiAction(
|
||||
clock,
|
||||
false,
|
||||
Optional.empty(),
|
||||
8,
|
||||
"tucketBucket",
|
||||
"testProject",
|
||||
"testRegion",
|
||||
dataflow,
|
||||
response);
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
response = new FakeResponse();
|
||||
void before() {
|
||||
expectedParameters.put("registryEnvironment", "UNITTEST");
|
||||
expectedParameters.put("isDryRun", "false");
|
||||
expectedParameters.put("cutoffTime", "2018-05-19T01:02:03.000Z");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess() throws Exception {
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo("Launched contact history PII wipeout pipeline: jobid");
|
||||
verify(templates, times(1))
|
||||
.launch(eq("testProject"), eq("testRegion"), launchRequest.capture());
|
||||
assertThat(launchRequest.getValue().getLaunchParameter().getParameters())
|
||||
.containsExactlyEntriesIn(expectedParameters);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_providedCutoffTime() throws Exception {
|
||||
action =
|
||||
new WipeOutContactHistoryPiiAction(
|
||||
clock, MIN_MONTHS_BEFORE_WIPE_OUT, TEST_BATCH_SIZE, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllHistoryEntitiesOlderThan_returnsAllPersistedEntities() {
|
||||
ImmutableList<ContactHistory> expectedToBeWipedOut =
|
||||
persistLotsOfContactHistoryEntities(20, MIN_MONTHS_BEFORE_WIPE_OUT + 1, 0, DEFAULT_CONTACT);
|
||||
tm().transact(
|
||||
() ->
|
||||
assertThat(
|
||||
action.getNextContactHistoryEntitiesWithPiiBatch(
|
||||
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT)))
|
||||
.containsExactlyElementsIn(expectedToBeWipedOut));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllHistoryEntitiesOlderThan_returnsOnlyOldEnoughPersistedEntities() {
|
||||
ImmutableList<ContactHistory> expectedToBeWipedOut =
|
||||
persistLotsOfContactHistoryEntities(19, MIN_MONTHS_BEFORE_WIPE_OUT + 2, 0, DEFAULT_CONTACT);
|
||||
|
||||
// persisted entities that should not be part of the actual result
|
||||
persistLotsOfContactHistoryEntities(15, 17, MIN_MONTHS_BEFORE_WIPE_OUT - 1, DEFAULT_CONTACT);
|
||||
|
||||
tm().transact(
|
||||
() ->
|
||||
assertThat(
|
||||
action.getNextContactHistoryEntitiesWithPiiBatch(
|
||||
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT)))
|
||||
.containsExactlyElementsIn(expectedToBeWipedOut));
|
||||
}
|
||||
|
||||
@Test
|
||||
void run_withNoEntitiesToWipeOut_success() {
|
||||
assertThat(
|
||||
tm().transact(
|
||||
() ->
|
||||
action
|
||||
.getNextContactHistoryEntitiesWithPiiBatch(
|
||||
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT))
|
||||
.count()))
|
||||
.isEqualTo(0);
|
||||
clock,
|
||||
false,
|
||||
Optional.of(now.minusYears(1)),
|
||||
8,
|
||||
"tucketBucket",
|
||||
"testProject",
|
||||
"testRegion",
|
||||
dataflow,
|
||||
response);
|
||||
action.run();
|
||||
|
||||
assertThat(
|
||||
tm().transact(
|
||||
() ->
|
||||
action
|
||||
.getNextContactHistoryEntitiesWithPiiBatch(
|
||||
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT))
|
||||
.count()))
|
||||
.isEqualTo(0);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
expectedParameters.put("cutoffTime", "2018-01-19T01:02:03.000Z");
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo("Done. Wiped out PII of 0 ContactHistory entities in total.");
|
||||
.isEqualTo("Launched contact history PII wipeout pipeline: jobid");
|
||||
verify(templates, times(1))
|
||||
.launch(eq("testProject"), eq("testRegion"), launchRequest.capture());
|
||||
assertThat(launchRequest.getValue().getLaunchParameter().getParameters())
|
||||
.containsExactlyEntriesIn(expectedParameters);
|
||||
}
|
||||
|
||||
@Test
|
||||
void run_withOneBatchOfEntities_success() {
|
||||
int numOfMonthsFromNow = MIN_MONTHS_BEFORE_WIPE_OUT + 2;
|
||||
ImmutableList<ContactHistory> expectedToBeWipedOut =
|
||||
persistLotsOfContactHistoryEntities(20, numOfMonthsFromNow, 0, DEFAULT_CONTACT);
|
||||
|
||||
// The query should return a stream of all persisted entities.
|
||||
assertThat(
|
||||
tm().transact(
|
||||
() ->
|
||||
action
|
||||
.getNextContactHistoryEntitiesWithPiiBatch(
|
||||
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT))
|
||||
.count()))
|
||||
.isEqualTo(expectedToBeWipedOut.size());
|
||||
|
||||
assertAllEntitiesContainPii(DatabaseHelper.loadByEntitiesIfPresent(expectedToBeWipedOut));
|
||||
|
||||
void testSuccess_dryRun() throws Exception {
|
||||
action =
|
||||
new WipeOutContactHistoryPiiAction(
|
||||
clock,
|
||||
true,
|
||||
Optional.empty(),
|
||||
8,
|
||||
"tucketBucket",
|
||||
"testProject",
|
||||
"testRegion",
|
||||
dataflow,
|
||||
response);
|
||||
action.run();
|
||||
expectedParameters.put("isDryRun", "true");
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo("Done. Wiped out PII of 20 ContactHistory entities in total.");
|
||||
|
||||
// The query should return an empty stream after the wipe out action.
|
||||
assertThat(
|
||||
tm().transact(
|
||||
() ->
|
||||
action
|
||||
.getNextContactHistoryEntitiesWithPiiBatch(
|
||||
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT))
|
||||
.count()))
|
||||
.isEqualTo(0);
|
||||
|
||||
assertAllPiiFieldsAreWipedOut(DatabaseHelper.loadByEntitiesIfPresent(expectedToBeWipedOut));
|
||||
.isEqualTo("Launched contact history PII wipeout pipeline: jobid");
|
||||
verify(templates, times(1))
|
||||
.launch(eq("testProject"), eq("testRegion"), launchRequest.capture());
|
||||
assertThat(launchRequest.getValue().getLaunchParameter().getParameters())
|
||||
.containsExactlyEntriesIn(expectedParameters);
|
||||
}
|
||||
|
||||
@Test
|
||||
void run_withMultipleBatches_numOfEntitiesAsNonMultipleOfBatchSize_success() {
|
||||
int numOfMonthsFromNow = MIN_MONTHS_BEFORE_WIPE_OUT + 2;
|
||||
ImmutableList<ContactHistory> expectedToBeWipedOut =
|
||||
persistLotsOfContactHistoryEntities(56, numOfMonthsFromNow, 0, DEFAULT_CONTACT);
|
||||
|
||||
// The query should return a subset of all persisted data.
|
||||
assertThat(
|
||||
tm().transact(
|
||||
() ->
|
||||
action
|
||||
.getNextContactHistoryEntitiesWithPiiBatch(
|
||||
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT))
|
||||
.count()))
|
||||
.isEqualTo(TEST_BATCH_SIZE);
|
||||
|
||||
assertAllEntitiesContainPii(DatabaseHelper.loadByEntitiesIfPresent(expectedToBeWipedOut));
|
||||
void testFailure_launchError() throws Exception {
|
||||
when(launch.execute()).thenThrow(new IOException("cannot launch"));
|
||||
action.run();
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo("Done. Wiped out PII of 56 ContactHistory entities in total.");
|
||||
|
||||
// The query should return an empty stream after the wipe out action.
|
||||
assertThat(
|
||||
tm().transact(
|
||||
() ->
|
||||
action
|
||||
.getNextContactHistoryEntitiesWithPiiBatch(
|
||||
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT))
|
||||
.count()))
|
||||
.isEqualTo(0);
|
||||
|
||||
assertAllPiiFieldsAreWipedOut(DatabaseHelper.loadByEntitiesIfPresent(expectedToBeWipedOut));
|
||||
}
|
||||
|
||||
@Test
|
||||
void run_withMultipleBatches_numOfEntitiesAsMultiplesOfBatchSize_success() {
|
||||
int numOfMonthsFromNow = MIN_MONTHS_BEFORE_WIPE_OUT + 2;
|
||||
ImmutableList<ContactHistory> expectedToBeWipedOut =
|
||||
persistLotsOfContactHistoryEntities(
|
||||
TEST_BATCH_SIZE * 2, numOfMonthsFromNow, 0, DEFAULT_CONTACT);
|
||||
|
||||
// The query should return a subset of all persisted data.
|
||||
assertThat(
|
||||
tm().transact(
|
||||
() ->
|
||||
action
|
||||
.getNextContactHistoryEntitiesWithPiiBatch(
|
||||
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT))
|
||||
.count()))
|
||||
.isEqualTo(TEST_BATCH_SIZE);
|
||||
|
||||
assertAllEntitiesContainPii(DatabaseHelper.loadByEntitiesIfPresent(expectedToBeWipedOut));
|
||||
action.run();
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo("Done. Wiped out PII of 40 ContactHistory entities in total.");
|
||||
|
||||
// The query should return an empty stream after the wipe out action.
|
||||
assertThat(
|
||||
tm().transact(
|
||||
() ->
|
||||
action
|
||||
.getNextContactHistoryEntitiesWithPiiBatch(
|
||||
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT))
|
||||
.count()))
|
||||
.isEqualTo(0);
|
||||
|
||||
assertAllPiiFieldsAreWipedOut(DatabaseHelper.loadByEntitiesIfPresent(expectedToBeWipedOut));
|
||||
}
|
||||
|
||||
@Test
|
||||
void wipeOutContactHistoryData_wipesOutNoEntity() {
|
||||
tm().transact(
|
||||
() ->
|
||||
assertThat(
|
||||
action.wipeOutContactHistoryData(
|
||||
action.getNextContactHistoryEntitiesWithPiiBatch(
|
||||
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT))))
|
||||
.isEqualTo(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void wipeOutContactHistoryData_wipesOutMultipleEntities() {
|
||||
int numOfMonthsFromNow = MIN_MONTHS_BEFORE_WIPE_OUT + 3;
|
||||
ImmutableList<ContactHistory> expectedToBeWipedOut =
|
||||
persistLotsOfContactHistoryEntities(20, numOfMonthsFromNow, 0, DEFAULT_CONTACT);
|
||||
|
||||
assertAllEntitiesContainPii(DatabaseHelper.loadByEntitiesIfPresent(expectedToBeWipedOut));
|
||||
|
||||
tm().transact(
|
||||
() -> {
|
||||
action.wipeOutContactHistoryData(
|
||||
action.getNextContactHistoryEntitiesWithPiiBatch(
|
||||
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT)));
|
||||
});
|
||||
|
||||
assertAllPiiFieldsAreWipedOut(DatabaseHelper.loadByEntitiesIfPresent(expectedToBeWipedOut));
|
||||
}
|
||||
|
||||
/** persists a number of ContactHistory entities for load and query testing. */
|
||||
ImmutableList<ContactHistory> persistLotsOfContactHistoryEntities(
|
||||
int numOfEntities, int minusMonths, int minusDays, Contact contact) {
|
||||
ImmutableList.Builder<ContactHistory> expectedEntitiesBuilder = new ImmutableList.Builder<>();
|
||||
for (int i = 0; i < numOfEntities; i++) {
|
||||
expectedEntitiesBuilder.add(
|
||||
persistResource(
|
||||
new ContactHistory()
|
||||
.asBuilder()
|
||||
.setRegistrarId("NewRegistrar")
|
||||
.setModificationTime(clock.nowUtc().minusMonths(minusMonths).minusDays(minusDays))
|
||||
.setType(ContactHistory.Type.CONTACT_DELETE)
|
||||
.setContact(persistResource(contact))
|
||||
.build()));
|
||||
}
|
||||
return expectedEntitiesBuilder.build();
|
||||
}
|
||||
|
||||
boolean areAllPiiFieldsWiped(ContactBase contactBase) {
|
||||
return contactBase.getEmailAddress() == null
|
||||
&& contactBase.getFaxNumber() == null
|
||||
&& contactBase.getInternationalizedPostalInfo() == null
|
||||
&& contactBase.getLocalizedPostalInfo() == null
|
||||
&& contactBase.getVoiceNumber() == null;
|
||||
}
|
||||
|
||||
boolean containsPii(ContactBase contactBase) {
|
||||
return contactBase.getEmailAddress() != null
|
||||
|| contactBase.getFaxNumber() != null
|
||||
|| contactBase.getInternationalizedPostalInfo() != null
|
||||
|| contactBase.getLocalizedPostalInfo() != null
|
||||
|| contactBase.getVoiceNumber() != null;
|
||||
}
|
||||
|
||||
void assertAllPiiFieldsAreWipedOut(ImmutableList<ContactHistory> entities) {
|
||||
ImmutableList.Builder<ContactHistory> notWipedEntities = new ImmutableList.Builder<>();
|
||||
for (ContactHistory entity : entities) {
|
||||
if (!areAllPiiFieldsWiped(entity.getContactBase().get())) {
|
||||
notWipedEntities.add(entity);
|
||||
}
|
||||
}
|
||||
assertWithMessage("Not all PII fields of the contact history entities were wiped.")
|
||||
.that(notWipedEntities.build())
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
void assertAllEntitiesContainPii(ImmutableList<ContactHistory> entities) {
|
||||
ImmutableList.Builder<ContactHistory> entitiesWithNoPii = new ImmutableList.Builder<>();
|
||||
for (ContactHistory entity : entities) {
|
||||
if (!containsPii(entity.getContactBase().get())) {
|
||||
entitiesWithNoPii.add(entity);
|
||||
}
|
||||
}
|
||||
assertWithMessage("Not all contact history entities contain PII.")
|
||||
.that(entitiesWithNoPii.build())
|
||||
.isEmpty();
|
||||
assertThat(response.getStatus()).isEqualTo(500);
|
||||
assertThat(response.getPayload()).isEqualTo("Pipeline launch failed: cannot launch");
|
||||
verify(templates, times(1))
|
||||
.launch(eq("testProject"), eq("testRegion"), launchRequest.capture());
|
||||
assertThat(launchRequest.getValue().getLaunchParameter().getParameters())
|
||||
.containsExactlyEntriesIn(expectedParameters);
|
||||
}
|
||||
}
|
||||
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.beam.wipeout;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.CONTACT_CREATE;
|
||||
import static google.registry.persistence.PersistenceModule.TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ;
|
||||
import static google.registry.testing.DatabaseHelper.loadAllOf;
|
||||
import static google.registry.testing.DatabaseHelper.newContact;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static org.hibernate.cfg.AvailableSettings.ISOLATION;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import google.registry.beam.TestPipelineExtension;
|
||||
import google.registry.model.contact.Contact;
|
||||
import google.registry.model.contact.ContactHistory;
|
||||
import google.registry.model.contact.ContactPhoneNumber;
|
||||
import google.registry.model.reporting.HistoryEntryDao;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
|
||||
import google.registry.testing.FakeClock;
|
||||
import org.apache.beam.sdk.options.PipelineOptionsFactory;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
import org.joda.time.format.DateTimeFormat;
|
||||
import org.joda.time.format.DateTimeFormatter;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
/** Unit tests for {@link WipeOutContactHistoryPiiPipeline}. */
|
||||
public class WipeOutContactHistoryPiiPipelineTest {
|
||||
|
||||
private static final int MIN_AGE_IN_MONTHS = 18;
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER =
|
||||
DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
|
||||
|
||||
private final FakeClock clock = new FakeClock(DateTime.parse("2020-02-02T12:34:56Z"));
|
||||
private final WipeOutContactHistoryPiiPipelineOptions options =
|
||||
PipelineOptionsFactory.create().as(WipeOutContactHistoryPiiPipelineOptions.class);
|
||||
private Contact contact1;
|
||||
private Contact contact2;
|
||||
|
||||
@RegisterExtension
|
||||
final JpaIntegrationTestExtension jpa =
|
||||
new JpaTestExtensions.Builder()
|
||||
.withClock(clock)
|
||||
.withProperty(ISOLATION, TRANSACTION_REPEATABLE_READ.name())
|
||||
.buildIntegrationTestExtension();
|
||||
|
||||
@RegisterExtension
|
||||
final TestPipelineExtension pipeline =
|
||||
TestPipelineExtension.create().enableAbandonedNodeEnforcement(true);
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
contact1 =
|
||||
persistResource(
|
||||
newContact("my-contact1")
|
||||
.asBuilder()
|
||||
.setEmailAddress("test@example.com")
|
||||
.setFaxNumber(
|
||||
new ContactPhoneNumber.Builder().setPhoneNumber("+12122122122").build())
|
||||
.build());
|
||||
contact2 =
|
||||
persistResource(
|
||||
newContact("my-contact2")
|
||||
.asBuilder()
|
||||
.setEmailAddress("test@example.tld")
|
||||
.setVoiceNumber(
|
||||
new ContactPhoneNumber.Builder().setPhoneNumber("+19177199177").build())
|
||||
.build());
|
||||
// T = 0 month;
|
||||
persistResource(createHistory(contact1));
|
||||
// T = 5 months;
|
||||
advanceMonths(5);
|
||||
persistResource(createHistory(contact2));
|
||||
// T = 10 months;
|
||||
advanceMonths(5);
|
||||
persistResource(createHistory(contact1));
|
||||
persistResource(createHistory(contact2));
|
||||
// T = 20 months;
|
||||
advanceMonths(10);
|
||||
persistResource(createHistory(contact2));
|
||||
// T = 30 months;
|
||||
advanceMonths(10);
|
||||
options.setCutoffTime(DATE_TIME_FORMATTER.print(clock.nowUtc().minusMonths(MIN_AGE_IN_MONTHS)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess() {
|
||||
// Before the pipeline runs, every history entry should have an emali address.
|
||||
assertThat(
|
||||
loadAllOf(ContactHistory.class).stream()
|
||||
.filter(e -> e.getContactBase().get().getEmailAddress() != null)
|
||||
.count())
|
||||
.isEqualTo(5);
|
||||
// Before the pipeline runs, contact history for contact1 should have fax numbers.
|
||||
ImmutableList<ContactHistory> histories =
|
||||
HistoryEntryDao.loadHistoryObjectsForResource(contact1.createVKey(), ContactHistory.class);
|
||||
assertThat(
|
||||
histories.stream().filter(e -> e.getContactBase().get().getFaxNumber() != null).count())
|
||||
.isEqualTo(2);
|
||||
// Before the pipeline runs, contact history for contact2 should have voice numbers.
|
||||
histories =
|
||||
HistoryEntryDao.loadHistoryObjectsForResource(contact2.createVKey(), ContactHistory.class);
|
||||
assertThat(
|
||||
histories.stream()
|
||||
.filter(e -> e.getContactBase().get().getVoiceNumber() != null)
|
||||
.count())
|
||||
.isEqualTo(3);
|
||||
WipeOutContactHistoryPiiPipeline wipeOutContactHistoryPiiPipeline =
|
||||
new WipeOutContactHistoryPiiPipeline(options);
|
||||
wipeOutContactHistoryPiiPipeline.run(pipeline).waitUntilFinish();
|
||||
histories =
|
||||
HistoryEntryDao.loadHistoryObjectsForResource(contact1.createVKey(), ContactHistory.class);
|
||||
assertThat(histories.size()).isEqualTo(2);
|
||||
ImmutableList<ContactHistory> wipedEntries =
|
||||
histories.stream()
|
||||
.filter(e -> e.getContactBase().get().getEmailAddress() == null)
|
||||
.collect(toImmutableList());
|
||||
// Only the history entry at T = 10 is wiped. The one at T = 10 is over 18 months old, but it
|
||||
// is the most recent entry, so it is kept.
|
||||
assertThat(wipedEntries.size()).isEqualTo(1);
|
||||
assertThat(wipedEntries.get(0).getContactBase().get().getFaxNumber()).isNull();
|
||||
// With a new history entry at T = 30, the one at T = 10 is eligible for wipe out. Note the
|
||||
// current time itself (therefore the cutoff time) has not changed.
|
||||
persistResource(createHistory(contact1));
|
||||
wipeOutContactHistoryPiiPipeline.run(pipeline).waitUntilFinish();
|
||||
histories =
|
||||
HistoryEntryDao.loadHistoryObjectsForResource(contact1.createVKey(), ContactHistory.class);
|
||||
assertThat(histories.size()).isEqualTo(3);
|
||||
wipedEntries =
|
||||
histories.stream()
|
||||
.filter(e -> e.getContactBase().get().getEmailAddress() == null)
|
||||
.collect(toImmutableList());
|
||||
assertThat(wipedEntries.size()).isEqualTo(2);
|
||||
// Check that the pipeline deals with multiple contacts correctly.
|
||||
histories =
|
||||
HistoryEntryDao.loadHistoryObjectsForResource(contact2.createVKey(), ContactHistory.class);
|
||||
assertThat(histories.size()).isEqualTo(3);
|
||||
wipedEntries =
|
||||
histories.stream()
|
||||
.filter(e -> e.getContactBase().get().getEmailAddress() == null)
|
||||
.collect(toImmutableList());
|
||||
// Only the history entry at T = 10 is wiped. The one at T = 10 is over 18 months old, but it
|
||||
// is the most recent entry, so it is kept.
|
||||
assertThat(wipedEntries.size()).isEqualTo(2);
|
||||
assertThat(wipedEntries.get(0).getContactBase().get().getVoiceNumber()).isNull();
|
||||
assertThat(wipedEntries.get(1).getContactBase().get().getVoiceNumber()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_dryRun() {
|
||||
options.setIsDryRun(true);
|
||||
WipeOutContactHistoryPiiPipeline wipeOutContactHistoryPiiPipeline =
|
||||
new WipeOutContactHistoryPiiPipeline(options);
|
||||
wipeOutContactHistoryPiiPipeline.run(pipeline).waitUntilFinish();
|
||||
ImmutableList<ContactHistory> histories =
|
||||
HistoryEntryDao.loadHistoryObjectsForResource(contact1.createVKey(), ContactHistory.class);
|
||||
assertThat(histories.size()).isEqualTo(2);
|
||||
assertThat(
|
||||
histories.stream()
|
||||
.filter(e -> e.getContactBase().get().getEmailAddress() == null)
|
||||
.collect(toImmutableList()))
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
private ContactHistory createHistory(Contact contact) {
|
||||
return new ContactHistory.Builder()
|
||||
.setContact(contact)
|
||||
.setType(CONTACT_CREATE)
|
||||
.setRegistrarId("TheRegistrar")
|
||||
.setModificationTime(clock.nowUtc())
|
||||
.build();
|
||||
}
|
||||
|
||||
private void advanceMonths(int months) {
|
||||
DateTime now = clock.nowUtc();
|
||||
DateTime next = now.plusMonths(months);
|
||||
clock.advanceBy(new Duration(now, next));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user