1
0
mirror of https://github.com/google/nomulus synced 2025-12-23 14:25:44 +00:00

Disable primary-contact editing in console (#2745)

This is necessary because we'll use primary-contact emails as a way of
resetting passwords.

In the UI, don't allow editing of email address for primary contacts,
and don't allow addition/removal of the primary contact field
post-creation.

In the backend, make sure that all emails previously added still exist.
This commit is contained in:
gbrodman
2025-04-29 13:32:29 -04:00
committed by GitHub
parent 56cd2ad282
commit daa7ab3bfa
8 changed files with 147 additions and 117 deletions

View File

@@ -16,11 +16,7 @@ import { Component, effect, ViewEncapsulation } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { take } from 'rxjs';
import { RegistrarService } from 'src/app/registrar/registrar.service';
import {
ContactService,
contactTypeToViewReadyContact,
ViewReadyContact,
} from './contact.service';
import { ContactService, ViewReadyContact } from './contact.service';
@Component({
selector: 'app-contact',

View File

@@ -83,7 +83,7 @@ export class ContactService {
: contactTypeToViewReadyContact({
emailAddress: '',
name: '',
types: ['ADMIN'],
types: ['TECH'],
faxNumber: '',
phoneNumber: '',
registrarId: '',

View File

@@ -56,6 +56,7 @@
[required]="true"
[(ngModel)]="contactService.contactInEdit.emailAddress"
[ngModelOptions]="{ standalone: true }"
[disabled]="emailAddressIsDisabled()"
/>
</mat-form-field>
@@ -85,14 +86,18 @@
<mat-icon color="accent">error</mat-icon>Required to select at least one
</p>
<div class="">
<mat-checkbox
<ng-container
*ngFor="let contactType of contactTypeToTextMap | keyvalue"
>
<mat-checkbox
*ngIf="shouldDisplayCheckbox(contactType.key)"
[checked]="checkboxIsChecked(contactType.key)"
(change)="checkboxOnChange($event, contactType.key)"
[disabled]="checkboxIsDisabled(contactType.key)"
>
{{ contactType.value }}
</mat-checkbox>
</ng-container>
</div>
</section>

View File

@@ -82,6 +82,10 @@ export class ContactDetailsComponent {
});
}
shouldDisplayCheckbox(type: string) {
return type !== 'ADMIN' || this.checkboxIsChecked(type);
}
checkboxIsChecked(type: string) {
return this.contactService.contactInEdit.types.includes(
type as contactType
@@ -89,6 +93,9 @@ export class ContactDetailsComponent {
}
checkboxIsDisabled(type: string) {
if (type === 'ADMIN') {
return true;
}
return (
this.contactService.contactInEdit.types.length === 1 &&
this.contactService.contactInEdit.types[0] === (type as contactType)
@@ -105,4 +112,8 @@ export class ContactDetailsComponent {
);
}
}
emailAddressIsDisabled() {
return this.contactService.contactInEdit.types.includes('ADMIN');
}
}

View File

@@ -169,6 +169,7 @@ public class ContactAction extends ConsoleApiAction {
throw new ContactRequirementException(t);
}
}
enforcePrimaryContactRestrictions(oldContactsByType, newContactsByType);
ensurePhoneNumberNotRemovedForContactTypes(oldContactsByType, newContactsByType, Type.TECH);
Optional<RegistrarPoc> domainWhoisAbuseContact =
getDomainWhoisVisibleAbuseContact(updatedContacts);
@@ -187,6 +188,23 @@ public class ContactAction extends ConsoleApiAction {
checkContactRegistryLockRequirements(existingContacts, updatedContacts);
}
private static void enforcePrimaryContactRestrictions(
Multimap<Type, RegistrarPoc> oldContactsByType,
Multimap<Type, RegistrarPoc> newContactsByType) {
ImmutableSet<String> oldAdminEmails =
oldContactsByType.get(Type.ADMIN).stream()
.map(RegistrarPoc::getEmailAddress)
.collect(toImmutableSet());
ImmutableSet<String> newAdminEmails =
newContactsByType.get(Type.ADMIN).stream()
.map(RegistrarPoc::getEmailAddress)
.collect(toImmutableSet());
if (!newAdminEmails.containsAll(oldAdminEmails)) {
throw new ContactRequirementException(
"Cannot remove or change the email address of primary contacts");
}
}
private static void checkContactRegistryLockRequirements(
ImmutableSet<RegistrarPoc> existingContacts, ImmutableSet<RegistrarPoc> updatedContacts) {
// Any contact(s) with new passwords must be allowed to set them

View File

@@ -18,6 +18,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.registrar.RegistrarPoc.Type.ABUSE;
import static google.registry.model.registrar.RegistrarPoc.Type.ADMIN;
import static google.registry.model.registrar.RegistrarPoc.Type.MARKETING;
import static google.registry.model.registrar.RegistrarPoc.Type.TECH;
import static google.registry.testing.DatabaseHelper.createAdminUser;
import static google.registry.testing.DatabaseHelper.insertInDb;
@@ -70,8 +71,9 @@ class ContactActionTest {
private Registrar testRegistrar;
private ConsoleApiParams consoleApiParams;
private RegistrarPoc testRegistrarPoc1;
private RegistrarPoc testRegistrarPoc2;
private RegistrarPoc adminPoc;
private RegistrarPoc techPoc;
private RegistrarPoc marketingPoc;
private static final Gson GSON = RequestModule.provideGson();
@@ -82,7 +84,7 @@ class ContactActionTest {
@BeforeEach
void beforeEach() {
testRegistrar = saveRegistrar("registrarId");
testRegistrarPoc1 =
adminPoc =
new RegistrarPoc.Builder()
.setRegistrar(testRegistrar)
.setName("Test Registrar 1")
@@ -94,19 +96,32 @@ class ContactActionTest {
.setVisibleInWhoisAsTech(false)
.setVisibleInDomainWhoisAsAbuse(false)
.build();
testRegistrarPoc2 =
testRegistrarPoc1
techPoc =
adminPoc
.asBuilder()
.setName("Test Registrar 2")
.setTypes(ImmutableSet.of(TECH))
.setVisibleInWhoisAsTech(true)
.setVisibleInWhoisAsAdmin(false)
.setEmailAddress("test.registrar2@example.com")
.setPhoneNumber("+1.1234567890")
.setFaxNumber("+1.1234567891")
.build();
marketingPoc =
adminPoc
.asBuilder()
.setName("Test Registrar 3")
.setTypes(ImmutableSet.of(MARKETING))
.setVisibleInWhoisAsAdmin(false)
.setEmailAddress("test.registrar3@example.com")
.setPhoneNumber("+1.1238675309")
.setFaxNumber("+1.1238675309")
.build();
}
@Test
void testSuccess_getContactInfo() throws IOException {
insertInDb(testRegistrarPoc1);
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.GET,
@@ -120,13 +135,13 @@ class ContactActionTest {
@Test
void testSuccess_noOp() throws IOException {
insertInDb(testRegistrarPoc1);
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1);
adminPoc);
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK);
verify(consoleApiParams.sendEmailUtils().gmailClient, never()).sendEmail(any());
@@ -134,8 +149,8 @@ class ContactActionTest {
@Test
void testSuccess_onlyContactsWithNonEmptyType() throws IOException {
testRegistrarPoc1 = testRegistrarPoc1.asBuilder().setTypes(ImmutableSet.of()).build();
insertInDb(testRegistrarPoc1);
adminPoc = adminPoc.asBuilder().setTypes(ImmutableSet.of()).build();
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.GET,
@@ -148,14 +163,14 @@ class ContactActionTest {
@Test
void testSuccess_postCreateContactInfo() throws IOException {
insertInDb(testRegistrarPoc1);
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1,
testRegistrarPoc2);
adminPoc,
techPoc);
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK);
assertThat(
@@ -168,14 +183,14 @@ class ContactActionTest {
@Test
void testSuccess_postUpdateContactInfo() throws IOException {
insertInDb(testRegistrarPoc1.asBuilder().setEmailAddress("incorrect@email.com").build());
insertInDb(techPoc.asBuilder().setEmailAddress("incorrect@email.com").build());
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1,
testRegistrarPoc2);
adminPoc,
techPoc);
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK);
HashMap<String, String> testResult = new HashMap<>();
@@ -197,8 +212,8 @@ class ContactActionTest {
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1,
testRegistrarPoc2.asBuilder().setEmailAddress("test.registrar1@example.com").build());
adminPoc,
techPoc.asBuilder().setEmailAddress("test.registrar1@example.com").build());
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(((FakeResponse) consoleApiParams.response()).getPayload())
@@ -213,13 +228,13 @@ class ContactActionTest {
@Test
void testFailure_postUpdateContactInfo_requiredContactRemoved() throws IOException {
insertInDb(testRegistrarPoc1);
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1.asBuilder().setTypes(ImmutableSet.of(ABUSE)).build());
adminPoc.asBuilder().setTypes(ImmutableSet.of(ABUSE)).build());
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(((FakeResponse) consoleApiParams.response()).getPayload())
@@ -228,20 +243,19 @@ class ContactActionTest {
loadAllOf(RegistrarPoc.class).stream()
.filter(r -> r.registrarId.equals(testRegistrar.getRegistrarId()))
.collect(toImmutableList()))
.containsExactly(testRegistrarPoc1);
.containsExactly(adminPoc);
}
@Test
void testFailure_postUpdateContactInfo_phoneNumberRemoved() throws IOException {
testRegistrarPoc1 =
testRegistrarPoc1.asBuilder().setTypes(ImmutableSet.of(ADMIN, TECH)).build();
insertInDb(testRegistrarPoc1);
adminPoc = adminPoc.asBuilder().setTypes(ImmutableSet.of(ADMIN, TECH)).build();
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1
adminPoc
.asBuilder()
.setPhoneNumber(null)
.setTypes(ImmutableSet.of(ADMIN, TECH))
@@ -254,7 +268,7 @@ class ContactActionTest {
loadAllOf(RegistrarPoc.class).stream()
.filter(r -> r.registrarId.equals(testRegistrar.getRegistrarId()))
.collect(toImmutableList()))
.containsExactly(testRegistrarPoc1);
.containsExactly(adminPoc);
}
@Test
@@ -264,11 +278,7 @@ class ContactActionTest {
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1
.asBuilder()
.setPhoneNumber(null)
.setVisibleInDomainWhoisAsAbuse(true)
.build());
adminPoc.asBuilder().setPhoneNumber(null).setVisibleInDomainWhoisAsAbuse(true).build());
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(((FakeResponse) consoleApiParams.response()).getPayload())
@@ -282,14 +292,14 @@ class ContactActionTest {
@Test
void testFailure_postUpdateContactInfo_whoisContactPhoneNumberRemoved() throws IOException {
testRegistrarPoc1 = testRegistrarPoc1.asBuilder().setVisibleInDomainWhoisAsAbuse(true).build();
insertInDb(testRegistrarPoc1);
adminPoc = adminPoc.asBuilder().setVisibleInDomainWhoisAsAbuse(true).build();
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1.asBuilder().setVisibleInDomainWhoisAsAbuse(false).build());
adminPoc.asBuilder().setVisibleInDomainWhoisAsAbuse(false).build());
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(((FakeResponse) consoleApiParams.response()).getPayload())
@@ -298,7 +308,7 @@ class ContactActionTest {
loadAllOf(RegistrarPoc.class).stream()
.filter(r -> r.registrarId.equals(testRegistrar.getRegistrarId()))
.collect(toImmutableList()))
.containsExactly(testRegistrarPoc1);
.containsExactly(adminPoc);
}
@Test
@@ -309,7 +319,7 @@ class ContactActionTest {
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1
adminPoc
.asBuilder()
.setAllowedToSetRegistryLockPassword(true)
.setRegistryLockEmailAddress("lock@example.com")
@@ -327,22 +337,19 @@ class ContactActionTest {
@Test
void testFailure_postUpdateContactInfo_cannotModifyRegistryLockEmail() throws IOException {
testRegistrarPoc1 =
testRegistrarPoc1
adminPoc =
adminPoc
.asBuilder()
.setRegistryLockEmailAddress("lock@example.com")
.setAllowedToSetRegistryLockPassword(true)
.build();
insertInDb(testRegistrarPoc1);
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1
.asBuilder()
.setRegistryLockEmailAddress("unlock@example.com")
.build());
adminPoc.asBuilder().setRegistryLockEmailAddress("unlock@example.com").build());
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(((FakeResponse) consoleApiParams.response()).getPayload())
@@ -351,25 +358,25 @@ class ContactActionTest {
loadAllOf(RegistrarPoc.class).stream()
.filter(r -> r.registrarId.equals(testRegistrar.getRegistrarId()))
.collect(toImmutableList()))
.containsExactly(testRegistrarPoc1);
.containsExactly(adminPoc);
}
@Test
void testFailure_postUpdateContactInfo_cannotSetIsAllowedToSetRegistryLockPassword()
throws IOException {
testRegistrarPoc1 =
testRegistrarPoc1
adminPoc =
adminPoc
.asBuilder()
.setRegistryLockEmailAddress("lock@example.com")
.setAllowedToSetRegistryLockPassword(false)
.build();
insertInDb(testRegistrarPoc1);
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1.asBuilder().setAllowedToSetRegistryLockPassword(true).build());
adminPoc.asBuilder().setAllowedToSetRegistryLockPassword(true).build());
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(((FakeResponse) consoleApiParams.response()).getPayload())
@@ -378,18 +385,18 @@ class ContactActionTest {
loadAllOf(RegistrarPoc.class).stream()
.filter(r -> r.registrarId.equals(testRegistrar.getRegistrarId()))
.collect(toImmutableList()))
.containsExactly(testRegistrarPoc1);
.containsExactly(adminPoc);
}
@Test
void testSuccess_sendsEmail() throws IOException, AddressException {
insertInDb(testRegistrarPoc1.asBuilder().setEmailAddress("incorrect@email.com").build());
insertInDb(techPoc.asBuilder().setEmailAddress("incorrect@email.com").build());
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1);
techPoc);
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK);
verify(consoleApiParams.sendEmailUtils().gmailClient, times(1))
@@ -404,44 +411,42 @@ class ContactActionTest {
+ "\n"
+ "contacts:\n"
+ " ADDED:\n"
+ " {name=Test Registrar 1,"
+ " emailAddress=test.registrar1@example.com, registrarId=registrarId,"
+ " registryLockEmailAddress=null, phoneNumber=+1.9999999999,"
+ " faxNumber=+1.9999999991, types=[ADMIN],"
+ " visibleInWhoisAsAdmin=true, visibleInWhoisAsTech=false,"
+ " {name=Test Registrar 2,"
+ " emailAddress=test.registrar2@example.com, registrarId=registrarId,"
+ " registryLockEmailAddress=null, phoneNumber=+1.1234567890,"
+ " faxNumber=+1.1234567891, types=[TECH],"
+ " visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=true,"
+ " visibleInDomainWhoisAsAbuse=false,"
+ " allowedToSetRegistryLockPassword=false}\n"
+ " REMOVED:\n"
+ " {name=Test Registrar 1, emailAddress=incorrect@email.com,"
+ " {name=Test Registrar 2, emailAddress=incorrect@email.com,"
+ " registrarId=registrarId, registryLockEmailAddress=null,"
+ " phoneNumber=+1.9999999999, faxNumber=+1.9999999991, types=[ADMIN],"
+ " visibleInWhoisAsAdmin=true,"
+ " visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false,"
+ " phoneNumber=+1.1234567890, faxNumber=+1.1234567891, types=[TECH],"
+ " visibleInWhoisAsAdmin=false,"
+ " visibleInWhoisAsTech=true, visibleInDomainWhoisAsAbuse=false,"
+ " allowedToSetRegistryLockPassword=false}\n"
+ " FINAL CONTENTS:\n"
+ " {name=Test Registrar 1,"
+ " emailAddress=test.registrar1@example.com, registrarId=registrarId,"
+ " registryLockEmailAddress=null, phoneNumber=+1.9999999999,"
+ " faxNumber=+1.9999999991, types=[ADMIN],"
+ " visibleInWhoisAsAdmin=true, visibleInWhoisAsTech=false,"
+ " {name=Test Registrar 2,"
+ " emailAddress=test.registrar2@example.com, registrarId=registrarId,"
+ " registryLockEmailAddress=null, phoneNumber=+1.1234567890,"
+ " faxNumber=+1.1234567891, types=[TECH],"
+ " visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=true,"
+ " visibleInDomainWhoisAsAbuse=false,"
+ " allowedToSetRegistryLockPassword=false}\n")
.setRecipients(
ImmutableList.of(
new InternetAddress("notification@test.example"),
new InternetAddress("incorrect@email.com")))
.setRecipients(ImmutableList.of(new InternetAddress("notification@test.example")))
.build());
}
@Test
void testSuccess_postDeleteContactInfo() throws IOException {
insertInDb(testRegistrarPoc1);
insertInDb(adminPoc, techPoc, marketingPoc);
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc2);
adminPoc,
techPoc);
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK);
assertThat(
@@ -449,12 +454,12 @@ class ContactActionTest {
.filter(r -> r.registrarId.equals(testRegistrar.getRegistrarId()))
.map(r -> r.getName())
.collect(toImmutableList()))
.containsExactly("Test Registrar 2");
.containsExactly("Test Registrar 1", "Test Registrar 2");
}
@Test
void testFailure_postDeleteContactInfo_missingPermission() throws IOException {
insertInDb(testRegistrarPoc1);
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.POST,
@@ -469,11 +474,27 @@ class ContactActionTest {
.build())
.build()),
testRegistrar.getRegistrarId(),
testRegistrarPoc2);
techPoc);
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_FORBIDDEN);
}
@Test
void testFailure_changesAdminEmail() throws Exception {
insertInDb(adminPoc.asBuilder().setEmailAddress("oldemail@example.com").build());
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
adminPoc);
action.run();
FakeResponse fakeResponse = (FakeResponse) consoleApiParams.response();
assertThat(fakeResponse.getStatus()).isEqualTo(400);
assertThat(fakeResponse.getPayload())
.isEqualTo("Cannot remove or change the email address of primary contacts");
}
private ContactAction createAction(
Action.Method method, AuthResult authResult, String registrarId, RegistrarPoc... contacts)
throws IOException {

View File

@@ -23,6 +23,7 @@ import google.registry.model.console.RegistrarRole;
import google.registry.server.RegistryTestServer;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junitpioneer.jupiter.RetryingTest;
@@ -50,7 +51,15 @@ import org.openqa.selenium.WebElement;
*/
// The Selenium image only supports amd64 architecture.
@EnabledIfSystemProperty(named = "os.arch", matches = "amd64")
public class ConsoleScreenshotTest extends WebDriverTestCase {
@Timeout(120)
public class ConsoleScreenshotTest {
@RegisterExtension
static final DockerWebDriverExtension webDriverProvider = new DockerWebDriverExtension();
@RegisterExtension
final WebDriverPlusScreenDifferExtension driver =
new WebDriverPlusScreenDifferExtension(webDriverProvider::getWebDriver);
@RegisterExtension
final TestServerExtension server =

View File

@@ -1,30 +0,0 @@
// Copyright 2019 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.webdriver;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Base class for tests that needs a {@link WebDriverPlusScreenDifferExtension}. */
@Timeout(120)
class WebDriverTestCase {
@RegisterExtension
static final DockerWebDriverExtension webDriverProvider = new DockerWebDriverExtension();
@RegisterExtension
final WebDriverPlusScreenDifferExtension driver =
new WebDriverPlusScreenDifferExtension(webDriverProvider::getWebDriver);
}