1
0
mirror of https://github.com/google/nomulus synced 2026-06-09 16:33:02 +00:00

Compare commits

...

27 Commits

Author SHA1 Message Date
gbrodman c6868b771b Update RDAP response profile + tech impl guide versions (#2778)
This corresponds to the Feb 2024 response profile section 1.2 and
implementation guide 1.3 respectively, now that we comply (or are, at
least closer to complying), with the Feb 2024 versions.

This should probably depend on https://github.com/google/nomulus/pull/2771
because that includes a small change included in the Feb 2024 version

This also updates the documentation to reference the proper areas of the
specifications.
2025-07-09 21:02:33 +00:00
gbrodman f34aec8b56 Add an "about" link to registrars in RDAP (#2771)
From the response profile:
2.4.6. Registrar URL - The entity with the registrar role in the RDAP response
MUST contain a links member [RFC9083]. The links object MUST contain
the elements: value, identical to the the RDAP Base URL for the
Registrar as provided in the IANA “Registrar IDs” registry (i.e.,
https://www.iana.org/assignments/registrar-ids); rel:about, and href
containing the Registrar URL. Note: in cases where the Registry Operator
acts as sponsoring Registrar (e.g., IANA Registrar ID 9999), the href shall
contain a URL from the Registry.
2025-07-08 14:54:07 +00:00
Ben McIlwain b27b077638 Increment proxy metrics by reciprocal of proxy metrics ratio (#2780)
This is necessary so that the total number of requests/responses adds up
correctly even though some fraction of them are only being recorded. It uses
stochastic rounding so that the totals add up correctly even when the reciprocal
of the ratio isn't an integer.

This is a follow-up to PR #2772.
2025-07-02 15:52:47 +00:00
Ben McIlwain 0e8cd75a58 Add the ability to configure a ratio of proxy metrics to be recorded (#2772)
This ratio defaults to 1.0 (i.e. all metrics will be recorded), but we will set
it much lower in sandbox and production, probably something closer to 0.01. This
will reduce recorded metrics volume and thus StackDriver cost, while still
retaining enough data for overall performance monitoring.

This is handled stochastically, so as to not require any coordination between
Java threads or GKE pods/clusters, as alternative approaches would (i.e. using a
counter and recording every Nth, or throttling to a max metrics qps).
2025-06-27 05:03:59 +00:00
gbrodman 2a1748ba9c Cache history values for RDAP domain requests (#2777)
In RDAP, domain queries are the most common by a factor of like 40,000
so we should optimize these as much as possible. We already have an EPP
resource / foreign key cache which does improve performance somewhat but
looking at some sample logs, it only cuts the RDAP request times by like
40% (looking at requests for the same domain a few seconds apart).

History entries don't change often, so we should cache them to make
subsequent queries faster as well. In addition, we're only caching two
fields per repo ID (modification time, registrar ID) so we can cache
more entries than we can for the EPP resource cache (which stores large
objects).
2025-06-25 19:33:36 +00:00
Weimin Yu f4889191a4 Fix prober cert renewal scripts (#2776)
Scripts needed by cron jobs wrongly removed by PR 2661.

TESTED: in crash.
2025-06-25 13:51:06 +00:00
Weimin Yu 9eddecf70f Bypass config check for caching when safe (#2773)
Pubapi actions should always use cache, regardless of the config
settings on caching.

In EppResource.java, the original `loadCached(Iterable<VKey>)`
method is renamed to `loadByCacheIfEnabled`. The original
`loadCached(Vkey)` method is renamed to `loadByCache` and always
uses cache.

In EppResourceUtils.java, the original `loadByForeignKeyCached`
method is renamed to `loadByForeignKeyByCacheIfEnabled`. A new
`loadByForeignKeyByCache` method, which always uses cache.

In ForeighKeyUtils.java, the original `loadCached` method is
renamed to `loadByCacheIfEnabled`, and a new `loadCached` method
is added which always uses cache.

Also added a `getContactsFromReplica` method in Registrar,
for use by RDAP actions.
2025-06-20 21:25:02 +00:00
gbrodman d4bcff0c31 Add password reset Java object (#2765)
A future PR will add the actions that save and use this object. That
future PR will also require loading RegistrarPoc objects given the
registrar ID, hence the change in that class.
2025-06-17 19:00:50 +00:00
Ben McIlwain 62065f88fb Remove spurious parenthesis in URS command output (#2767)
It was making the undo nomulus command look like this:

)nomulus ...
2025-06-16 20:23:48 +00:00
Pavlo Tkach c9ac9437fd Add java code for RegitrarPoc id (#2770) 2025-06-14 17:37:11 +00:00
gbrodman 1f6a09182d Add some changes related to RDAP Feb 2024 profile (#2759)
This implements two type of changes:
1. changing the link type for things like the terms of service
2. adding the request URL to each and every link with the "value" field.
   This is a bit tricky to implement because the links are generated in
various places, but we can implement it by adding it to the results
after generation.

See b/418782147 for more information
2025-06-11 20:30:15 +00:00
Weimin Yu a0eff00031 Add an aggregate module for DNS writers (#2769)
Add a new DnsWritersModule for use by the component classes.

To override the set of writers installed, we can easily overwrite this
file with a private version.
2025-06-09 14:46:54 +00:00
gbrodman 89698c6ed6 Update version of google-java-format (#2766)
This picks up a few changes including aligning the placement of quotes
in text blocks with the Google style guide.
2025-06-06 18:11:54 +00:00
gbrodman a7696c3fac Add console action test base case (#2762)
We can probably improve on this in the future if we want, but there's a
lot of boilerplate that we don't need to repeat over and over
2025-06-04 15:36:22 +00:00
Weimin Yu 7ec599f849 Fix create_cdns_tld command (#2760)
The Cloud DNS rest api is now case-sensitive about enum names (must be
lower case, counterintuitively).
2025-06-03 15:17:43 +00:00
Pavlo Tkach 70291af9ad Add RegistrarPoc id column (#2761) 2025-06-02 15:43:03 +00:00
gbrodman 5fb95f38ed Don't always require contacts in CreateDomainCommand (#2755)
If contacts are optional, they should be optional in the command too.
2025-05-15 20:22:07 +00:00
gbrodman dfe8e24761 Add registrar_id col to password reset requests (#2756)
This is just so that we can add an additional layer of security on
verification
2025-05-15 20:13:27 +00:00
Juan Celhay bd30fcc81c Remove registrar id from invoice grouping key (#2749)
* Remove registrar id from invoice grouping key

* Fix formatting issues

* Update BillingEventTests
2025-05-13 20:29:25 +00:00
gbrodman 8cecc8d3a8 Use the primary DB for DomainInfoFlow (#2750)
This avoids potential replication lag issues when requesting info on
domains that were just created.
2025-05-13 18:00:30 +00:00
Pavlo Tkach c5a39bccc5 Add Console POC reminder front-end (#2754) 2025-05-12 20:14:56 +00:00
gbrodman a90a117341 Add SQL table for password resets (#2751)
We plan on using this for EPP password resets and registry lock password
resets for now.
2025-05-08 19:16:08 +00:00
Weimin Yu b40ad54daf Hardcode beam pipelines to use GKE for tasks (#2753) 2025-05-08 17:29:30 +00:00
Pavlo Tkach b4d239c329 Add console POC reminder backend support (#2747) 2025-04-30 14:15:43 +00:00
gbrodman daa7ab3bfa 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.
2025-04-29 17:32:29 +00:00
gbrodman 56cd2ad282 Change AllocationToken behavior in non-catastrophic situations (#2730)
We're changing the way that allocation tokens work in suboptimal (i.e. incorrect) situations in the domain check, creation, and renewal process. Currently, if a token is not applicable, in any way, to any of the operations (including when a check has multiple operations requested) we return some variation of "Allocation token not valid" for all of those options. We wish to allow for a more lenient process, where if a token is "not applicable" instead of "invalid", we just pass through that part of the request as if the token were not there.

Types of errors that will remain catastrophic, where we'll basically return a token error immediately in all cases:
- nonexistent or null token
- token is assigned to a particular domain and the request isn't for that domain
- token is not valid for this registrar
- token is a single-use token that has already been redeemed
- token has a promotional schedule and it's no longer valid

Types of errors that will now be a silent pass-through, as if the user did not issue a token:
- token is not allowed for this TLD
- token has a discount, is not valid for premium names, and the domain name is premium
- token does not allow the provided EPP action

Currently, the last three types of errors cause that generic "token invalid" message but in the future, we'll pass the requests through as if the user did not pass in a token. This does allow for a default token to apply to these requests if available, meaning that it's possible that a single DomainCheckFlow with multiple check requests could use the provided token for some check(s), and a default token for others.

The flip side of this is that if the user passes in a catastrophically invalid token (the first five error messages above), we will return that result to any/all checks that they request, even if there are other issues with that request (e.g. the domain is reserved or already registered).

See b/315504612 for more details and background
2025-04-23 15:09:37 +00:00
gbrodman 0472dda860 Remove transaction duration logging (#2748)
We suspected this could be a cause of optimistic locking failures
(because long transactions would lead to optimistic locks not being
released) but this didn't end up being the case. Let's remove this to
reduce log spam.
2025-04-22 18:53:21 +00:00
219 changed files with 7044 additions and 6001 deletions
+98 -10
View File
@@ -14,30 +14,71 @@
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component';
import { MaterialModule } from './material.module';
import { BackendService } from './shared/services/backend.service';
import { AppRoutingModule } from './app-routing.module';
import { routes } from './app-routing.module';
import { AppModule } from './app.module';
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
import { RouterModule } from '@angular/router';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { UserData, UserDataService } from './shared/services/userData.service';
import { Registrar, RegistrarService } from './registrar/registrar.service';
import { MatSidenavModule } from '@angular/material/sidenav';
import { signal, WritableSignal } from '@angular/core';
describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let mockRegistrarService: {
registrar: WritableSignal<Partial<Registrar> | null | undefined>;
registrarId: WritableSignal<string>;
registrars: WritableSignal<Array<Partial<Registrar>>>;
};
let mockUserDataService: { userData: WritableSignal<Partial<UserData>> };
let mockSnackBar: jasmine.SpyObj<MatSnackBar>;
const dummyPocReminderComponent = class {}; // Dummy class for type checking
beforeEach(async () => {
mockRegistrarService = {
registrar: signal<Registrar | null | undefined>(undefined),
registrarId: signal('123'),
registrars: signal([]),
};
mockUserDataService = {
userData: signal({
globalRole: 'NONE',
}),
};
mockSnackBar = jasmine.createSpyObj('MatSnackBar', ['openFromComponent']);
await TestBed.configureTestingModule({
declarations: [AppComponent],
imports: [
MaterialModule,
BrowserAnimationsModule,
AppRoutingModule,
MatSidenavModule,
NoopAnimationsModule,
MatSnackBarModule,
AppModule,
RouterModule.forRoot(routes),
],
providers: [
BackendService,
{ provide: RegistrarService, useValue: mockRegistrarService },
{ provide: UserDataService, useValue: mockUserDataService },
{ provide: MatSnackBar, useValue: mockSnackBar },
{ provide: PocReminderComponent, useClass: dummyPocReminderComponent },
provideHttpClient(),
provideHttpClientTesting(),
],
}).compileComponents();
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should create the app', () => {
@@ -46,4 +87,51 @@ describe('AppComponent', () => {
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
describe('PoC Verification Reminder', () => {
beforeEach(() => {
jasmine.clock().install();
});
it('should open snackbar if lastPocVerificationDate is older than one year', fakeAsync(() => {
const MOCK_TODAY = new Date('2024-07-15T10:00:00.000Z');
jasmine.clock().mockDate(MOCK_TODAY);
const twoYearsAgo = new Date(MOCK_TODAY);
twoYearsAgo.setFullYear(MOCK_TODAY.getFullYear() - 2);
mockRegistrarService.registrar.set({
lastPocVerificationDate: twoYearsAgo.toISOString(),
});
fixture.detectChanges();
TestBed.flushEffects();
expect(mockSnackBar.openFromComponent).toHaveBeenCalledWith(
PocReminderComponent,
{
horizontalPosition: 'center',
verticalPosition: 'top',
duration: 1000000000,
}
);
}));
it('should NOT open snackbar if lastPocVerificationDate is within last year', fakeAsync(() => {
const MOCK_TODAY = new Date('2024-07-15T10:00:00.000Z');
jasmine.clock().mockDate(MOCK_TODAY);
const sixMonthsAgo = new Date(MOCK_TODAY);
sixMonthsAgo.setMonth(MOCK_TODAY.getMonth() - 6);
mockRegistrarService.registrar.set({
lastPocVerificationDate: sixMonthsAgo.toISOString(),
});
fixture.detectChanges();
TestBed.flushEffects();
expect(mockSnackBar.openFromComponent).not.toHaveBeenCalled();
}));
});
});
+25 -3
View File
@@ -12,13 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { AfterViewInit, Component, effect, ViewChild } from '@angular/core';
import { MatSidenav } from '@angular/material/sidenav';
import { NavigationEnd, Router } from '@angular/router';
import { RegistrarService } from './registrar/registrar.service';
import { BreakPointObserverService } from './shared/services/breakPoint.service';
import { GlobalLoaderService } from './shared/services/globalLoader.service';
import { UserDataService } from './shared/services/userData.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
@Component({
selector: 'app-root',
@@ -35,8 +37,28 @@ export class AppComponent implements AfterViewInit {
protected userDataService: UserDataService,
protected globalLoader: GlobalLoaderService,
protected breakpointObserver: BreakPointObserverService,
private router: Router
) {}
private router: Router,
private _snackBar: MatSnackBar
) {
effect(() => {
const registrar = this.registrarService.registrar();
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
oneYearAgo.setHours(0, 0, 0, 0);
if (
registrar &&
registrar.lastPocVerificationDate &&
new Date(registrar.lastPocVerificationDate) < oneYearAgo &&
this.userDataService?.userData()?.globalRole === 'NONE'
) {
this._snackBar.openFromComponent(PocReminderComponent, {
horizontalPosition: 'center',
verticalPosition: 'top',
duration: 1000000000,
});
}
});
}
ngAfterViewInit() {
this.router.events.subscribe((event) => {
+2
View File
@@ -60,6 +60,7 @@ import { TldsComponent } from './tlds/tlds.component';
import { ForceFocusDirective } from './shared/directives/forceFocus.directive';
import RdapComponent from './settings/rdap/rdap.component';
import RdapEditComponent from './settings/rdap/rdapEdit.component';
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
@NgModule({
declarations: [SelectedRegistrarWrapper],
@@ -86,6 +87,7 @@ export class SelectedRegistrarModule {}
RdapComponent,
RdapEditComponent,
ReasonDialogComponent,
PocReminderComponent,
RegistrarComponent,
RegistrarDetailsComponent,
RegistrarSelectorComponent,
@@ -57,7 +57,7 @@ export class NavigationComponent {
}
ngOnDestroy() {
this.subscription.unsubscribe();
this.subscription && this.subscription.unsubscribe();
}
getElementId(node: RouteWithIcon) {
@@ -71,6 +71,7 @@ export interface Registrar
registrarName: string;
registryLockAllowed?: boolean;
type?: string;
lastPocVerificationDate?: string;
}
@Injectable({
@@ -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',
@@ -24,7 +24,7 @@ export type contactType =
| 'LEGAL'
| 'MARKETING'
| 'TECH'
| 'RDAP';
| 'WHOIS';
type contactTypesToUserFriendlyTypes = { [type in contactType]: string };
@@ -35,7 +35,7 @@ export const contactTypeToTextMap: contactTypesToUserFriendlyTypes = {
LEGAL: 'Legal contact',
MARKETING: 'Marketing contact',
TECH: 'Technical contact',
RDAP: 'RDAP-Inquiry contact',
WHOIS: 'RDAP-Inquiry contact',
};
type UserFriendlyType = (typeof contactTypeToTextMap)[contactType];
@@ -59,7 +59,10 @@ export interface ViewReadyContact extends Contact {
export function contactTypeToViewReadyContact(c: Contact): ViewReadyContact {
return {
...c,
userFriendlyTypes: c.types?.map((cType) => contactTypeToTextMap[cType]),
userFriendlyTypes: (c.types || []).map(
(cType) => contactTypeToTextMap[cType]
),
types: c.types || [],
};
}
@@ -83,7 +86,7 @@ export class ContactService {
: contactTypeToViewReadyContact({
emailAddress: '',
name: '',
types: ['ADMIN'],
types: ['TECH'],
faxNumber: '',
phoneNumber: '',
registrarId: '',
@@ -98,19 +101,21 @@ export class ContactService {
);
}
saveContacts(contacts: ViewReadyContact[]): Observable<Contact[]> {
updateContact(contact: ViewReadyContact) {
return this.backend
.postContacts(this.registrarService.registrarId(), contacts)
.updateContact(this.registrarService.registrarId(), contact)
.pipe(switchMap((_) => this.fetchContacts()));
}
addContact(contact: ViewReadyContact) {
const newContacts = this.contacts().concat([contact]);
return this.saveContacts(newContacts);
return this.backend
.createContact(this.registrarService.registrarId(), contact)
.pipe(switchMap((_) => this.fetchContacts()));
}
deleteContact(contact: ViewReadyContact) {
const newContacts = this.contacts().filter((c) => c !== contact);
return this.saveContacts(newContacts);
return this.backend
.deleteContact(this.registrarService.registrarId(), contact)
.pipe(switchMap((_) => this.fetchContacts()));
}
}
@@ -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"
[checked]="checkboxIsChecked(contactType.key)"
(change)="checkboxOnChange($event, contactType.key)"
[disabled]="checkboxIsDisabled(contactType.key)"
>
{{ contactType.value }}
</mat-checkbox>
<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>
@@ -69,9 +69,13 @@ export class ContactDetailsComponent {
save(e: SubmitEvent) {
e.preventDefault();
if ((this.contactService.contactInEdit.types || []).length === 0) {
this._snackBar.open('Required to select contact type');
return;
}
const request = this.contactService.isContactNewView
? this.contactService.addContact(this.contactService.contactInEdit)
: this.contactService.saveContacts(this.contactService.contacts());
: this.contactService.updateContact(this.contactService.contactInEdit);
request.subscribe({
complete: () => {
this.goBack();
@@ -82,6 +86,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 +97,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 +116,8 @@ export class ContactDetailsComponent {
);
}
}
emailAddressIsDisabled() {
return this.contactService.contactInEdit.types.includes('ADMIN');
}
}
@@ -0,0 +1,14 @@
<div class="console-app__pocReminder">
<p class="">
Please take a moment to complete annual review of
<a routerLink="/settings">contacts</a>.
</p>
<span matSnackBarActions>
<button mat-button matSnackBarAction (click)="confirmReviewed()">
Confirm reviewed
</button>
<button mat-button matSnackBarAction (click)="snackBarRef.dismiss()">
Close
</button>
</span>
</div>
@@ -0,0 +1,5 @@
.console-app__pocReminder {
a {
color: white !important;
}
}
@@ -0,0 +1,53 @@
// Copyright 2025 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.
import { Component } from '@angular/core';
import { MatSnackBar, MatSnackBarRef } from '@angular/material/snack-bar';
import { RegistrarService } from '../../../registrar/registrar.service';
import { HttpErrorResponse } from '@angular/common/http';
@Component({
selector: 'app-poc-reminder',
templateUrl: './pocReminder.component.html',
styleUrls: ['./pocReminder.component.scss'],
standalone: false,
})
export class PocReminderComponent {
constructor(
public snackBarRef: MatSnackBarRef<PocReminderComponent>,
private registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {}
confirmReviewed() {
if (this.registrarService.registrar()) {
const todayMidnight = new Date();
todayMidnight.setHours(0, 0, 0, 0);
this.registrarService
// @ts-ignore - if check above won't allow empty object to be submitted
.updateRegistrar({
...this.registrarService.registrar(),
lastPocVerificationDate: todayMidnight.toISOString(),
})
.subscribe({
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error || err.message);
},
next: () => {
this.snackBarRef.dismiss();
},
});
}
}
}
@@ -70,13 +70,26 @@ export class BackendService {
.pipe(catchError((err) => this.errorCatcher<Contact[]>(err)));
}
postContacts(
registrarId: string,
contacts: Contact[]
): Observable<Contact[]> {
return this.http.post<Contact[]>(
updateContact(registrarId: string, contact: Contact): Observable<Contact> {
return this.http.put<Contact>(
`/console-api/settings/contacts?registrarId=${registrarId}`,
contacts
contact
);
}
createContact(registrarId: string, contact: Contact): Observable<Contact> {
return this.http.post<Contact>(
`/console-api/settings/contacts?registrarId=${registrarId}`,
contact
);
}
deleteContact(registrarId: string, contact: Contact): Observable<Contact> {
return this.http.delete<Contact>(
`/console-api/settings/contacts?registrarId=${registrarId}`,
{
body: JSON.stringify(contact),
}
);
}
+1
View File
@@ -61,6 +61,7 @@ def fragileTestPatterns = [
// Currently changes a global configuration parameter that for some reason
// results in timestamp inversions for other tests. TODO(mmuller): fix.
"google/registry/flows/host/HostInfoFlowTest.*",
"google/registry/beam/common/RegistryPipelineWorkerInitializerTest.*",
] + dockerIncompatibleTestPatterns
sourceSets {
@@ -172,7 +172,7 @@ public record BillingEvent(
.minusDays(1)
.toString(),
billingId(),
registrarId(),
"",
String.format("%s | TLD: %s | TERM: %d-year", action(), tld(), years()),
amount(),
currency(),
@@ -36,8 +36,6 @@ import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.flows.custom.CustomLogicFactoryModule;
import google.registry.flows.custom.CustomLogicModule;
import google.registry.flows.domain.DomainPricingLogic;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForCurrencyException;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingBase.Flag;
import google.registry.model.billing.BillingCancellation;
@@ -389,38 +387,30 @@ public class ExpandBillingRecurrencesPipeline implements Serializable {
// It is OK to always create a OneTime, even though the domain might be deleted or transferred
// later during autorenew grace period, as a cancellation will always be written out in those
// instances.
BillingEvent billingEvent = null;
try {
billingEvent =
new BillingEvent.Builder()
.setBillingTime(billingTime)
.setRegistrarId(billingRecurrence.getRegistrarId())
// Determine the cost for a one-year renewal.
.setCost(
domainPricingLogic
.getRenewPrice(
tld,
billingRecurrence.getTargetId(),
eventTime,
1,
billingRecurrence,
Optional.empty())
.getRenewCost())
.setEventTime(eventTime)
.setFlags(union(billingRecurrence.getFlags(), Flag.SYNTHETIC))
.setDomainHistory(historyEntry)
.setPeriodYears(1)
.setReason(billingRecurrence.getReason())
.setSyntheticCreationTime(endTime)
.setCancellationMatchingBillingEvent(billingRecurrence)
.setTargetId(billingRecurrence.getTargetId())
.build();
} catch (AllocationTokenInvalidForCurrencyException
| AllocationTokenInvalidForPremiumNameException e) {
// This should not be reached since we are not using an allocation token
return;
}
results.add(billingEvent);
results.add(
new BillingEvent.Builder()
.setBillingTime(billingTime)
.setRegistrarId(billingRecurrence.getRegistrarId())
// Determine the cost for a one-year renewal.
.setCost(
domainPricingLogic
.getRenewPrice(
tld,
billingRecurrence.getTargetId(),
eventTime,
1,
billingRecurrence,
Optional.empty())
.getRenewCost())
.setEventTime(eventTime)
.setFlags(union(billingRecurrence.getFlags(), Flag.SYNTHETIC))
.setDomainHistory(historyEntry)
.setPeriodYears(1)
.setReason(billingRecurrence.getReason())
.setSyntheticCreationTime(endTime)
.setCancellationMatchingBillingEvent(billingRecurrence)
.setTargetId(billingRecurrence.getTargetId())
.build());
}
results.add(
billingRecurrence
@@ -40,6 +40,8 @@ public class RegistryPipelineWorkerInitializer implements JvmInitializer {
@Override
public void beforeProcessing(PipelineOptions options) {
// TODO(b/416299900): remove next line after GAE is removed.
System.setProperty("google.registry.jetty", "true");
RegistryPipelineOptions registryOptions = options.as(RegistryPipelineOptions.class);
RegistryEnvironment environment = registryOptions.getRegistryEnvironment();
if (environment == null || environment.equals(RegistryEnvironment.UNITTEST)) {
@@ -0,0 +1,29 @@
// Copyright 2025 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.dns.writer;
import dagger.Module;
import google.registry.dns.writer.clouddns.CloudDnsWriterModule;
import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule;
/**
* Groups all {@link DnsWriter} implementations to be installed.
*
* <p>To cherry-pick the DNS writers to install, overwrite this file with your private version in
* the release process.
*/
@Module(
includes = {CloudDnsWriterModule.class, DnsUpdateWriterModule.class, VoidDnsWriterModule.class})
public class DnsWritersModule {}
@@ -185,7 +185,8 @@ public class CheckApiAction implements Runnable {
}
private boolean checkExists(String domainString, DateTime now) {
return !ForeignKeyUtils.loadCached(Domain.class, ImmutableList.of(domainString), now).isEmpty();
return !ForeignKeyUtils.loadByCache(Domain.class, ImmutableList.of(domainString), now)
.isEmpty();
}
private Optional<String> checkReserved(InternetDomainName domainName) {
@@ -14,7 +14,6 @@
package google.registry.flows.domain;
import static com.google.common.base.Strings.emptyToNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
@@ -42,6 +41,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.InternetDomainName;
import google.registry.config.RegistryConfig;
import google.registry.config.RegistryConfig.Config;
@@ -55,14 +55,7 @@ import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.custom.DomainCheckFlowCustomLogic;
import google.registry.flows.custom.DomainCheckFlowCustomLogic.BeforeResponseParameters;
import google.registry.flows.custom.DomainCheckFlowCustomLogic.BeforeResponseReturnData;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
import google.registry.flows.domain.token.AllocationTokenDomainCheckResults;
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForCommandException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException;
import google.registry.model.EppResource;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.billing.BillingRecurrence;
@@ -87,7 +80,6 @@ import google.registry.model.tld.Tld;
import google.registry.model.tld.Tld.TldState;
import google.registry.model.tld.label.ReservationType;
import google.registry.persistence.VKey;
import google.registry.pricing.PricingEngineProxy;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import java.util.Collection;
@@ -131,6 +123,8 @@ import org.joda.time.DateTime;
@ReportingSpec(ActivityReportField.DOMAIN_CHECK)
public final class DomainCheckFlow implements TransactionalFlow {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final String STANDARD_FEE_RESPONSE_CLASS = "STANDARD";
private static final String STANDARD_PROMOTION_FEE_RESPONSE_CLASS = "STANDARD PROMOTION";
@@ -146,7 +140,6 @@ public final class DomainCheckFlow implements TransactionalFlow {
@Inject @Superuser boolean isSuperuser;
@Inject Clock clock;
@Inject EppResponse.Builder responseBuilder;
@Inject AllocationTokenFlowUtils allocationTokenFlowUtils;
@Inject DomainCheckFlowCustomLogic flowCustomLogic;
@Inject DomainPricingLogic pricingLogic;
@@ -195,36 +188,15 @@ public final class DomainCheckFlow implements TransactionalFlow {
existingDomains.size() == parsedDomains.size()
? ImmutableSet.of()
: getBsaBlockedDomains(parsedDomains.values(), now);
Optional<AllocationTokenExtension> allocationTokenExtension =
eppInput.getSingleExtension(AllocationTokenExtension.class);
Optional<AllocationTokenDomainCheckResults> tokenDomainCheckResults =
allocationTokenExtension.map(
tokenExtension ->
allocationTokenFlowUtils.checkDomainsWithToken(
ImmutableList.copyOf(parsedDomains.values()),
tokenExtension.getAllocationToken(),
registrarId,
now));
ImmutableList.Builder<DomainCheck> checksBuilder = new ImmutableList.Builder<>();
ImmutableSet.Builder<String> availableDomains = new ImmutableSet.Builder<>();
ImmutableMap<String, TldState> tldStates =
Maps.toMap(seenTlds, tld -> Tld.get(tld).getTldState(now));
ImmutableMap<InternetDomainName, String> domainCheckResults =
tokenDomainCheckResults
.map(AllocationTokenDomainCheckResults::domainCheckResults)
.orElse(ImmutableMap.of());
Optional<AllocationToken> allocationToken =
tokenDomainCheckResults.flatMap(AllocationTokenDomainCheckResults::token);
for (String domainName : domainNames) {
Optional<String> message =
getMessageForCheck(
parsedDomains.get(domainName),
existingDomains,
bsaBlockedDomainNames,
domainCheckResults,
tldStates,
allocationToken);
domainName, existingDomains, bsaBlockedDomainNames, tldStates, parsedDomains, now);
boolean isAvailable = message.isEmpty();
checksBuilder.add(DomainCheck.create(isAvailable, domainName, message.orElse(null)));
if (isAvailable) {
@@ -237,11 +209,7 @@ public final class DomainCheckFlow implements TransactionalFlow {
.setDomainChecks(checksBuilder.build())
.setResponseExtensions(
getResponseExtensions(
parsedDomains,
existingDomains,
availableDomains.build(),
now,
allocationToken))
parsedDomains, existingDomains, availableDomains.build(), now))
.setAsOfDate(now)
.build());
return responseBuilder
@@ -251,10 +219,39 @@ public final class DomainCheckFlow implements TransactionalFlow {
}
private Optional<String> getMessageForCheck(
String domainName,
ImmutableMap<String, VKey<Domain>> existingDomains,
ImmutableSet<InternetDomainName> bsaBlockedDomainNames,
ImmutableMap<String, TldState> tldStates,
ImmutableMap<String, InternetDomainName> parsedDomains,
DateTime now) {
InternetDomainName idn = parsedDomains.get(domainName);
Optional<AllocationToken> token;
try {
// Which token we use may vary based on the domain -- a provided token may be invalid for
// some domains, or there may be DEFAULT PROMO tokens only applicable on some domains
token =
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
registrarId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class),
Tld.get(idn.parent().toString()),
domainName,
FeeQueryCommandExtensionItem.CommandName.CREATE);
} catch (AllocationTokenFlowUtils.NonexistentAllocationTokenException
| AllocationTokenFlowUtils.AllocationTokenInvalidException e) {
// The provided token was catastrophically invalid in some way
logger.atInfo().withCause(e).log("Cannot load/use allocation token.");
return Optional.of(e.getMessage());
}
return getMessageForCheckWithToken(
idn, existingDomains, bsaBlockedDomainNames, tldStates, token);
}
private Optional<String> getMessageForCheckWithToken(
InternetDomainName domainName,
ImmutableMap<String, VKey<Domain>> existingDomains,
ImmutableSet<InternetDomainName> bsaBlockedDomains,
ImmutableMap<InternetDomainName, String> tokenCheckResults,
ImmutableMap<String, TldState> tldStates,
Optional<AllocationToken> allocationToken) {
if (existingDomains.containsKey(domainName.toString())) {
@@ -271,11 +268,6 @@ public final class DomainCheckFlow implements TransactionalFlow {
}
}
}
Optional<String> tokenResult =
Optional.ofNullable(emptyToNull(tokenCheckResults.get(domainName)));
if (tokenResult.isPresent()) {
return tokenResult;
}
if (isRegisterBsaCreate(domainName, allocationToken)
|| !bsaBlockedDomains.contains(domainName)) {
return Optional.empty();
@@ -290,8 +282,7 @@ public final class DomainCheckFlow implements TransactionalFlow {
ImmutableMap<String, InternetDomainName> domainNames,
ImmutableMap<String, VKey<Domain>> existingDomains,
ImmutableSet<String> availableDomains,
DateTime now,
Optional<AllocationToken> allocationToken)
DateTime now)
throws EppException {
Optional<FeeCheckCommandExtension> feeCheckOpt =
eppInput.getSingleExtension(FeeCheckCommandExtension.class);
@@ -309,84 +300,24 @@ public final class DomainCheckFlow implements TransactionalFlow {
RegistryConfig.getTieredPricingPromotionRegistrarIds().contains(registrarId);
for (FeeCheckCommandExtensionItem feeCheckItem : feeCheck.getItems()) {
for (String domainName : getDomainNamesToCheckForFee(feeCheckItem, domainNames.keySet())) {
Optional<AllocationToken> defaultToken =
DomainFlowUtils.checkForDefaultToken(
Tld.get(InternetDomainName.from(domainName).parent().toString()),
domainName,
feeCheckItem.getCommandName(),
registrarId,
now);
FeeCheckResponseExtensionItem.Builder<?> builder = feeCheckItem.createResponseBuilder();
Optional<Domain> domain = Optional.ofNullable(domainObjs.get(domainName));
Tld tld = Tld.get(domainNames.get(domainName).parent().toString());
Optional<AllocationToken> token;
try {
if (allocationToken.isPresent()) {
AllocationTokenFlowUtils.validateToken(
InternetDomainName.from(domainName),
allocationToken.get(),
feeCheckItem.getCommandName(),
registrarId,
PricingEngineProxy.isDomainPremium(domainName, now),
now);
}
handleFeeRequest(
feeCheckItem,
builder,
domainNames.get(domainName),
domain,
feeCheck.getCurrency(),
now,
pricingLogic,
allocationToken.isPresent() ? allocationToken : defaultToken,
availableDomains.contains(domainName),
recurrences.getOrDefault(domainName, null));
// In the case of a registrar that is running a tiered pricing promotion, we issue two
// responses for the CREATE fee check command: one (the default response) with the
// non-promotional price, and one (an extra STANDARD PROMO response) with the actual
// promotional price.
if (defaultToken.isPresent()
&& shouldUseTieredPricingPromotion
&& feeCheckItem
.getCommandName()
.equals(FeeQueryCommandExtensionItem.CommandName.CREATE)) {
// First, set the promotional (real) price under the STANDARD PROMO class
builder
.setClass(STANDARD_PROMOTION_FEE_RESPONSE_CLASS)
.setCommand(
FeeQueryCommandExtensionItem.CommandName.CUSTOM,
feeCheckItem.getPhase(),
feeCheckItem.getSubphase());
// Next, get the non-promotional price and set it as the standard response to the CREATE
// fee check command
FeeCheckResponseExtensionItem.Builder<?> nonPromotionalBuilder =
feeCheckItem.createResponseBuilder();
handleFeeRequest(
feeCheckItem,
nonPromotionalBuilder,
domainNames.get(domainName),
domain,
feeCheck.getCurrency(),
now,
pricingLogic,
allocationToken,
availableDomains.contains(domainName),
recurrences.getOrDefault(domainName, null));
responseItems.add(
nonPromotionalBuilder
.setClass(STANDARD_FEE_RESPONSE_CLASS)
.setDomainNameIfSupported(domainName)
.build());
}
responseItems.add(builder.setDomainNameIfSupported(domainName).build());
} catch (AllocationTokenInvalidForPremiumNameException
| AllocationTokenNotValidForCommandException
| AllocationTokenNotValidForDomainException
| AllocationTokenNotValidForRegistrarException
| AllocationTokenNotValidForTldException
| AllocationTokenNotInPromotionException e) {
// Allocation token is either not an active token or it is not valid for the EPP command,
// registrar, domain, or TLD.
Tld tld = Tld.get(InternetDomainName.from(domainName).parent().toString());
// The precise token to use for this fee request may vary based on the domain or even the
// precise command issued (some tokens may be valid only for certain actions)
token =
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
registrarId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class),
tld,
domainName,
feeCheckItem.getCommandName());
} catch (AllocationTokenFlowUtils.NonexistentAllocationTokenException
| AllocationTokenFlowUtils.AllocationTokenInvalidException e) {
// The provided token was catastrophically invalid in some way
responseItems.add(
builder
.setDomainNameIfSupported(domainName)
@@ -398,7 +329,60 @@ public final class DomainCheckFlow implements TransactionalFlow {
.setCurrencyIfSupported(tld.getCurrency())
.setClass("token-not-supported")
.build());
continue;
}
handleFeeRequest(
feeCheckItem,
builder,
domainNames.get(domainName),
domain,
feeCheck.getCurrency(),
now,
pricingLogic,
token,
availableDomains.contains(domainName),
recurrences.getOrDefault(domainName, null));
// In the case of a registrar that is running a tiered pricing promotion, we issue two
// responses for the CREATE fee check command: one (the default response) with the
// non-promotional price, and one (an extra STANDARD PROMO response) with the actual
// promotional price.
if (token
.map(t -> t.getTokenType().equals(AllocationToken.TokenType.DEFAULT_PROMO))
.orElse(false)
&& shouldUseTieredPricingPromotion
&& feeCheckItem
.getCommandName()
.equals(FeeQueryCommandExtensionItem.CommandName.CREATE)) {
// First, set the promotional (real) price under the STANDARD PROMO class
builder
.setClass(STANDARD_PROMOTION_FEE_RESPONSE_CLASS)
.setCommand(
FeeQueryCommandExtensionItem.CommandName.CUSTOM,
feeCheckItem.getPhase(),
feeCheckItem.getSubphase());
// Next, get the non-promotional price and set it as the standard response to the CREATE
// fee check command
FeeCheckResponseExtensionItem.Builder<?> nonPromotionalBuilder =
feeCheckItem.createResponseBuilder();
handleFeeRequest(
feeCheckItem,
nonPromotionalBuilder,
domainNames.get(domainName),
domain,
feeCheck.getCurrency(),
now,
pricingLogic,
Optional.empty(),
availableDomains.contains(domainName),
recurrences.getOrDefault(domainName, null));
responseItems.add(
nonPromotionalBuilder
.setClass(STANDARD_FEE_RESPONSE_CLASS)
.setDomainNameIfSupported(domainName)
.build());
}
responseItems.add(builder.setDomainNameIfSupported(domainName).build());
}
}
return ImmutableList.of(feeCheck.createResponse(responseItems.build()));
@@ -448,7 +432,7 @@ public final class DomainCheckFlow implements TransactionalFlow {
.filter(existingDomains::containsKey)
.collect(toImmutableMap(d -> d, existingDomains::get));
ImmutableMap<VKey<? extends EppResource>, EppResource> loadedDomains =
EppResource.loadCached(ImmutableList.copyOf(existingDomainsToLoad.values()));
EppResource.loadByCacheIfEnabled(ImmutableList.copyOf(existingDomainsToLoad.values()));
return ImmutableMap.copyOf(
Maps.transformEntries(existingDomainsToLoad, (k, v) -> (Domain) loadedDomains.get(v)));
}
@@ -133,11 +133,8 @@ import org.joda.time.Duration;
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException}
* @error {@link AllocationTokenFlowUtils.NonexistentAllocationTokenException}
* @error {@link google.registry.flows.exceptions.OnlyToolCanPassMetadataException}
* @error {@link ResourceAlreadyExistsForThisClientException}
* @error {@link ResourceCreateContentionException}
@@ -205,7 +202,6 @@ import org.joda.time.Duration;
* @error {@link DomainFlowUtils.UnexpectedClaimsNoticeException}
* @error {@link DomainFlowUtils.UnsupportedFeeAttributeException}
* @error {@link DomainFlowUtils.UnsupportedMarkTypeException}
* @error {@link DomainPricingLogic.AllocationTokenInvalidForPremiumNameException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_CREATE)
public final class DomainCreateFlow implements MutatingFlow {
@@ -221,7 +217,6 @@ public final class DomainCreateFlow implements MutatingFlow {
@Inject @Superuser boolean isSuperuser;
@Inject DomainHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
@Inject AllocationTokenFlowUtils allocationTokenFlowUtils;
@Inject DomainCreateFlowCustomLogic flowCustomLogic;
@Inject DomainFlowTmchUtils tmchUtils;
@Inject DomainPricingLogic pricingLogic;
@@ -264,25 +259,20 @@ public final class DomainCreateFlow implements MutatingFlow {
}
boolean isSunriseCreate = hasSignedMarks && (tldState == START_DATE_SUNRISE);
Optional<AllocationToken> allocationToken =
allocationTokenFlowUtils.verifyAllocationTokenCreateIfPresent(
command,
tld,
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
registrarId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class));
boolean defaultTokenUsed = false;
if (allocationToken.isEmpty()) {
allocationToken =
DomainFlowUtils.checkForDefaultToken(
tld, command.getDomainName(), CommandName.CREATE, registrarId, now);
if (allocationToken.isPresent()) {
defaultTokenUsed = true;
}
}
eppInput.getSingleExtension(AllocationTokenExtension.class),
tld,
command.getDomainName(),
CommandName.CREATE);
boolean defaultTokenUsed =
allocationToken.map(t -> t.getTokenType().equals(TokenType.DEFAULT_PROMO)).orElse(false);
boolean isAnchorTenant =
isAnchorTenant(
domainName, allocationToken, eppInput.getSingleExtension(MetadataExtension.class));
verifyAnchorTenantValidPeriod(isAnchorTenant, years);
// Superusers can create reserved domains, force creations on domains that require a claims
// notice without specifying a claims key, ignore the registry phase, and override blocks on
// registering premium domains.
@@ -416,7 +406,7 @@ public final class DomainCreateFlow implements MutatingFlow {
entitiesToSave.add(domain, domainHistory);
if (allocationToken.isPresent() && allocationToken.get().getTokenType().isOneTimeUse()) {
entitiesToSave.add(
allocationTokenFlowUtils.redeemToken(
AllocationTokenFlowUtils.redeemToken(
allocationToken.get(), domainHistory.getHistoryEntryId()));
}
if (domain.shouldPublishToDns()) {
@@ -42,7 +42,6 @@ import static google.registry.model.tld.label.ReservationType.RESERVED_FOR_ANCHO
import static google.registry.model.tld.label.ReservationType.RESERVED_FOR_SPECIFIC_USE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.pricing.PricingEngineProxy.isDomainPremium;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
import static google.registry.util.CollectionUtils.nullToEmpty;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.isAtOrAfter;
@@ -67,7 +66,6 @@ import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
import google.registry.flows.EppException.AssociationProhibitsOperationException;
import google.registry.flows.EppException.AuthorizationErrorException;
import google.registry.flows.EppException.CommandUseErrorException;
import google.registry.flows.EppException.ObjectDoesNotExistException;
@@ -77,8 +75,6 @@ import google.registry.flows.EppException.ParameterValueSyntaxErrorException;
import google.registry.flows.EppException.RequiredParameterMissingException;
import google.registry.flows.EppException.StatusProhibitsOperationException;
import google.registry.flows.EppException.UnimplementedOptionException;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.model.EppResource;
import google.registry.model.billing.BillingBase.Flag;
@@ -101,7 +97,6 @@ import google.registry.model.domain.fee.BaseFee.FeeType;
import google.registry.model.domain.fee.Credit;
import google.registry.model.domain.fee.Fee;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import google.registry.model.domain.fee.FeeQueryResponseExtensionItem;
import google.registry.model.domain.fee.FeeTransformCommandExtension;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
@@ -422,7 +417,7 @@ public class DomainFlowUtils {
contacts.stream().map(DesignatedContact::getContactKey).forEach(keysToLoad::add);
registrant.ifPresent(keysToLoad::add);
keysToLoad.addAll(nameservers);
verifyNotInPendingDelete(EppResource.loadCached(keysToLoad.build()).values());
verifyNotInPendingDelete(EppResource.loadByCacheIfEnabled(keysToLoad.build()).values());
}
private static void verifyNotInPendingDelete(Iterable<EppResource> resources)
@@ -1233,52 +1228,6 @@ public class DomainFlowUtils {
.getResultList();
}
/**
* Checks if there is a valid default token to be used for a domain create command.
*
* <p>If there is more than one valid default token for the registration, only the first valid
* token found on the TLD's default token list will be returned.
*/
public static Optional<AllocationToken> checkForDefaultToken(
Tld tld, String domainName, CommandName commandName, String registrarId, DateTime now)
throws EppException {
if (isNullOrEmpty(tld.getDefaultPromoTokens())) {
return Optional.empty();
}
Map<VKey<AllocationToken>, Optional<AllocationToken>> tokens =
AllocationToken.getAll(tld.getDefaultPromoTokens());
ImmutableList<Optional<AllocationToken>> tokenList =
tld.getDefaultPromoTokens().stream()
.map(tokens::get)
.filter(Optional::isPresent)
.collect(toImmutableList());
checkState(
!isNullOrEmpty(tokenList),
"Failure while loading default TLD promotions from the database");
// Check if any of the tokens are valid for this domain registration
for (Optional<AllocationToken> token : tokenList) {
try {
AllocationTokenFlowUtils.validateToken(
InternetDomainName.from(domainName),
token.get(),
commandName,
registrarId,
isDomainPremium(domainName, now),
now);
} catch (AssociationProhibitsOperationException
| StatusProhibitsOperationException
| AllocationTokenInvalidForPremiumNameException e) {
// Allocation token was not valid for this registration, continue to check the next token in
// the list
continue;
}
// Only use the first valid token in the list
return token;
}
// No valid default token found
return Optional.empty();
}
/** Resource linked to this domain does not exist. */
static class LinkedResourcesDoNotExistException extends ObjectDoesNotExistException {
public LinkedResourcesDoNotExistException(Class<?> type, ImmutableSet<String> resourceIds) {
@@ -31,6 +31,7 @@ import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.MutatingFlow;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.custom.DomainInfoFlowCustomLogic;
@@ -53,6 +54,8 @@ import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.eppoutput.EppResponse.ResponseExtension;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.persistence.IsolationLevel;
import google.registry.persistence.PersistenceModule;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import java.util.Optional;
@@ -62,8 +65,12 @@ import org.joda.time.DateTime;
* An EPP flow that returns information about a domain.
*
* <p>The registrar that owns the domain, and any registrar presenting a valid authInfo for the
* domain, will get a rich result with all of the domain's fields. All other requests will be
* answered with a minimal result containing only basic information about the domain.
* domain, will get a rich result with all the domain's fields. All other requests will be answered
* with a minimal result containing only basic information about the domain.
*
* <p>This implements {@link MutatingFlow} instead of {@link TransactionalFlow} as a workaround so
* that the common workflow of "create domain, then immediately get domain info" does not run into
* replication lag issues where the info command claims the domain does not exist.
*
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link google.registry.flows.FlowUtils.UnknownCurrencyEppException}
@@ -76,7 +83,8 @@ import org.joda.time.DateTime;
* @error {@link DomainFlowUtils.TransfersAreAlwaysForOneYearException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_INFO)
public final class DomainInfoFlow implements TransactionalFlow {
@IsolationLevel(PersistenceModule.TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ)
public final class DomainInfoFlow implements MutatingFlow {
@Inject ExtensionManager extensionManager;
@Inject ResourceCommand resourceCommand;
@@ -16,14 +16,13 @@ package google.registry.flows.domain;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.flows.domain.DomainFlowUtils.zeroInCurrency;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.validateTokenForPossiblePremiumName;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.discountTokenInvalidForPremiumName;
import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import com.google.common.net.InternetDomainName;
import google.registry.config.RegistryConfig;
import google.registry.flows.EppException;
import google.registry.flows.EppException.CommandUseErrorException;
import google.registry.flows.custom.DomainPricingCustomLogic;
import google.registry.flows.custom.DomainPricingCustomLogic.CreatePriceParameters;
import google.registry.flows.custom.DomainPricingCustomLogic.RenewPriceParameters;
@@ -129,9 +128,7 @@ public final class DomainPricingLogic {
DateTime dateTime,
int years,
@Nullable BillingRecurrence billingRecurrence,
Optional<AllocationToken> allocationToken)
throws AllocationTokenInvalidForCurrencyException,
AllocationTokenInvalidForPremiumNameException {
Optional<AllocationToken> allocationToken) {
checkArgument(years > 0, "Number of years must be positive");
Money renewCost;
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
@@ -260,8 +257,7 @@ public final class DomainPricingLogic {
/** Returns the domain create cost with allocation-token-related discounts applied. */
private Money getDomainCreateCostWithDiscount(
DomainPrices domainPrices, int years, Optional<AllocationToken> allocationToken, Tld tld)
throws EppException {
DomainPrices domainPrices, int years, Optional<AllocationToken> allocationToken, Tld tld) {
return getDomainCostWithDiscount(
domainPrices.isPremium(),
years,
@@ -277,9 +273,7 @@ public final class DomainPricingLogic {
DomainPrices domainPrices,
DateTime dateTime,
int years,
Optional<AllocationToken> allocationToken)
throws AllocationTokenInvalidForCurrencyException,
AllocationTokenInvalidForPremiumNameException {
Optional<AllocationToken> allocationToken) {
// Short-circuit if the user sent an anchor-tenant or otherwise NONPREMIUM-renewal token
if (allocationToken.isPresent()) {
AllocationToken token = allocationToken.get();
@@ -315,44 +309,41 @@ public final class DomainPricingLogic {
Optional<AllocationToken> allocationToken,
Money firstYearCost,
Optional<Money> subsequentYearCost,
Tld tld)
throws AllocationTokenInvalidForCurrencyException,
AllocationTokenInvalidForPremiumNameException {
Tld tld) {
checkArgument(years > 0, "Registration years to get cost for must be positive.");
validateTokenForPossiblePremiumName(allocationToken, isPremium);
Money totalDomainFlowCost =
firstYearCost.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(years - 1));
if (allocationToken.isEmpty()) {
return totalDomainFlowCost;
}
AllocationToken token = allocationToken.get();
if (discountTokenInvalidForPremiumName(token, isPremium)) {
return totalDomainFlowCost;
}
if (!token.getTokenBehavior().equals(TokenBehavior.DEFAULT)) {
return totalDomainFlowCost;
}
// Apply the allocation token discount, if applicable.
if (allocationToken.isPresent()
&& allocationToken.get().getTokenBehavior().equals(TokenBehavior.DEFAULT)) {
if (allocationToken.get().getDiscountPrice().isPresent()) {
if (!tld.getCurrency()
.equals(allocationToken.get().getDiscountPrice().get().getCurrencyUnit())) {
throw new AllocationTokenInvalidForCurrencyException();
}
int nonDiscountedYears = Math.max(0, years - allocationToken.get().getDiscountYears());
totalDomainFlowCost =
allocationToken
.get()
.getDiscountPrice()
.get()
.multipliedBy(allocationToken.get().getDiscountYears())
.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(nonDiscountedYears));
} else {
// Assumes token has discount fraction set.
int discountedYears = Math.min(years, allocationToken.get().getDiscountYears());
if (token.getDiscountPrice().isPresent()
&& tld.getCurrency().equals(token.getDiscountPrice().get().getCurrencyUnit())) {
int nonDiscountedYears = Math.max(0, years - token.getDiscountYears());
totalDomainFlowCost =
token
.getDiscountPrice()
.get()
.multipliedBy(token.getDiscountYears())
.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(nonDiscountedYears));
} else if (token.getDiscountFraction() > 0) {
int discountedYears = Math.min(years, token.getDiscountYears());
if (discountedYears > 0) {
var discount =
firstYearCost
.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(discountedYears - 1))
.multipliedBy(
allocationToken.get().getDiscountFraction(), RoundingMode.HALF_EVEN);
var discount =
firstYearCost
.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(discountedYears - 1))
.multipliedBy(token.getDiscountFraction(), RoundingMode.HALF_EVEN);
totalDomainFlowCost = totalDomainFlowCost.minus(discount);
}
}
}
return totalDomainFlowCost;
}
@@ -376,18 +367,4 @@ public final class DomainPricingLogic {
: domainPrices.getRenewCost();
return DomainPrices.create(isPremium, createCost, renewCost);
}
/** An allocation token was provided that is invalid for premium domains. */
public static class AllocationTokenInvalidForPremiumNameException
extends CommandUseErrorException {
public AllocationTokenInvalidForPremiumNameException() {
super("Token not valid for premium name");
}
}
public static class AllocationTokenInvalidForCurrencyException extends CommandUseErrorException {
public AllocationTokenInvalidForCurrencyException() {
super("Token and domain currencies do not match.");
}
}
}
@@ -31,7 +31,7 @@ import static google.registry.flows.domain.DomainFlowUtils.validateRegistrationP
import static google.registry.flows.domain.DomainFlowUtils.verifyRegistrarIsActive;
import static google.registry.flows.domain.DomainFlowUtils.verifyUnitIsYears;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.maybeApplyBulkPricingRemovalToken;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.verifyTokenAllowedOnDomain;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.verifyBulkTokenAllowedOnDomain;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_RENEW;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.leapSafeAddYears;
@@ -124,15 +124,12 @@ import org.joda.time.Duration;
* @error {@link RemoveBulkPricingTokenOnNonBulkPricingDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException}
* @error {@link AllocationTokenFlowUtils.NonexistentAllocationTokenException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_RENEW)
@@ -154,7 +151,6 @@ public final class DomainRenewFlow implements MutatingFlow {
@Inject @Superuser boolean isSuperuser;
@Inject DomainHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
@Inject AllocationTokenFlowUtils allocationTokenFlowUtils;
@Inject DomainRenewFlowCustomLogic flowCustomLogic;
@Inject DomainPricingLogic pricingLogic;
@Inject DomainRenewFlow() {}
@@ -174,22 +170,17 @@ public final class DomainRenewFlow implements MutatingFlow {
String tldStr = existingDomain.getTld();
Tld tld = Tld.get(tldStr);
Optional<AllocationToken> allocationToken =
allocationTokenFlowUtils.verifyAllocationTokenIfPresent(
existingDomain,
tld,
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
registrarId,
now,
CommandName.RENEW,
eppInput.getSingleExtension(AllocationTokenExtension.class));
boolean defaultTokenUsed = false;
if (allocationToken.isEmpty()) {
allocationToken =
DomainFlowUtils.checkForDefaultToken(
tld, existingDomain.getDomainName(), CommandName.RENEW, registrarId, now);
if (allocationToken.isPresent()) {
defaultTokenUsed = true;
}
}
eppInput.getSingleExtension(AllocationTokenExtension.class),
tld,
existingDomain.getDomainName(),
CommandName.RENEW);
boolean defaultTokenUsed =
allocationToken
.map(t -> t.getTokenType().equals(AllocationToken.TokenType.DEFAULT_PROMO))
.orElse(false);
verifyRenewAllowed(authInfo, existingDomain, command, allocationToken);
// If client passed an applicable static token this updates the domain
@@ -259,7 +250,7 @@ public final class DomainRenewFlow implements MutatingFlow {
newDomain, domainHistory, explicitRenewEvent, newAutorenewEvent, newAutorenewPollMessage);
if (allocationToken.isPresent() && allocationToken.get().getTokenType().isOneTimeUse()) {
entitiesToSave.add(
allocationTokenFlowUtils.redeemToken(
AllocationTokenFlowUtils.redeemToken(
allocationToken.get(), domainHistory.getHistoryEntryId()));
}
EntityChanges entityChanges =
@@ -327,7 +318,7 @@ public final class DomainRenewFlow implements MutatingFlow {
}
verifyUnitIsYears(command.getPeriod());
// We only allow __REMOVE_BULK_PRICING__ token on bulk pricing domains for now
verifyTokenAllowedOnDomain(existingDomain, allocationToken);
verifyBulkTokenAllowedOnDomain(existingDomain, allocationToken);
// If the date they specify doesn't match the expiration, fail. (This is an idempotence check).
if (!command.getCurrentExpirationDate().equals(
existingDomain.getRegistrationExpirationTime().toLocalDate())) {
@@ -54,7 +54,6 @@ import google.registry.model.billing.BillingRecurrence;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.token.AllocationTokenExtension;
@@ -94,15 +93,12 @@ import org.joda.time.DateTime;
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException}
* @error {@link AllocationTokenFlowUtils.NonexistentAllocationTokenException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_TRANSFER_APPROVE)
@@ -116,7 +112,6 @@ public final class DomainTransferApproveFlow implements MutatingFlow {
@Inject DomainHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
@Inject DomainPricingLogic pricingLogic;
@Inject AllocationTokenFlowUtils allocationTokenFlowUtils;
@Inject EppInput eppInput;
@Inject DomainTransferApproveFlow() {}
@@ -132,13 +127,8 @@ public final class DomainTransferApproveFlow implements MutatingFlow {
extensionManager.validate();
DateTime now = tm().getTransactionTime();
Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now);
allocationTokenFlowUtils.verifyAllocationTokenIfPresent(
existingDomain,
Tld.get(existingDomain.getTld()),
registrarId,
now,
CommandName.TRANSFER,
eppInput.getSingleExtension(AllocationTokenExtension.class));
AllocationTokenFlowUtils.loadAllocationTokenFromExtension(
registrarId, targetId, now, eppInput.getSingleExtension(AllocationTokenExtension.class));
verifyOptionalAuthInfo(authInfo, existingDomain);
verifyHasPendingTransfer(existingDomain);
verifyResourceOwnership(registrarId, existingDomain);
@@ -58,7 +58,6 @@ import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand.Transfer;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.Period;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import google.registry.model.domain.fee.FeeTransferCommandExtension;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
import google.registry.model.domain.metadata.MetadataExtension;
@@ -123,15 +122,12 @@ import org.joda.time.DateTime;
* @error {@link DomainFlowUtils.UnsupportedFeeAttributeException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException}
* @error {@link AllocationTokenFlowUtils.NonexistentAllocationTokenException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_TRANSFER_REQUEST)
@@ -154,7 +150,6 @@ public final class DomainTransferRequestFlow implements MutatingFlow {
@Inject AsyncTaskEnqueuer asyncTaskEnqueuer;
@Inject EppResponse.Builder responseBuilder;
@Inject DomainPricingLogic pricingLogic;
@Inject AllocationTokenFlowUtils allocationTokenFlowUtils;
@Inject DomainTransferRequestFlow() {}
@@ -170,12 +165,10 @@ public final class DomainTransferRequestFlow implements MutatingFlow {
extensionManager.validate();
DateTime now = tm().getTransactionTime();
Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now);
allocationTokenFlowUtils.verifyAllocationTokenIfPresent(
existingDomain,
Tld.get(existingDomain.getTld()),
AllocationTokenFlowUtils.loadAllocationTokenFromExtension(
gainingClientId,
targetId,
now,
CommandName.TRANSFER,
eppInput.getSingleExtension(AllocationTokenExtension.class));
Optional<DomainTransferRequestSuperuserExtension> superuserExtension =
eppInput.getSingleExtension(DomainTransferRequestSuperuserExtension.class);
@@ -15,218 +15,97 @@
package google.registry.flows.domain.token;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.pricing.PricingEngineProxy.isDomainPremium;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.ImmutableList;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
import google.registry.flows.EppException.AssociationProhibitsOperationException;
import google.registry.flows.EppException.AuthorizationErrorException;
import google.registry.flows.EppException.StatusProhibitsOperationException;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
import google.registry.model.billing.BillingBase;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenBehavior;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.domain.token.AllocationTokenExtension;
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.model.tld.Tld;
import google.registry.persistence.VKey;
import jakarta.inject.Inject;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.joda.time.DateTime;
/** Utility functions for dealing with {@link AllocationToken}s in domain flows. */
public class AllocationTokenFlowUtils {
@Inject
public AllocationTokenFlowUtils() {}
/**
* Checks if the allocation token applies to the given domain names, used for domain checks.
*
* @return A map of domain names to domain check error response messages. If a message is present
* for a a given domain then it does not validate with this allocation token; domains that do
* validate have blank messages (i.e. no error).
*/
public AllocationTokenDomainCheckResults checkDomainsWithToken(
List<InternetDomainName> domainNames, String token, String registrarId, DateTime now) {
// If the token is completely invalid, return the error message for all domain names
AllocationToken tokenEntity;
try {
tokenEntity = loadToken(token);
} catch (EppException e) {
return new AllocationTokenDomainCheckResults(
Optional.empty(), Maps.toMap(domainNames, ignored -> e.getMessage()));
}
// If the token is only invalid for some domain names (e.g. an invalid TLD), include those error
// results for only those domain names
ImmutableMap.Builder<InternetDomainName, String> resultsBuilder = new ImmutableMap.Builder<>();
for (InternetDomainName domainName : domainNames) {
try {
validateToken(
domainName,
tokenEntity,
CommandName.CREATE,
registrarId,
isDomainPremium(domainName.toString(), now),
now);
resultsBuilder.put(domainName, "");
} catch (EppException e) {
resultsBuilder.put(domainName, e.getMessage());
}
}
return new AllocationTokenDomainCheckResults(Optional.of(tokenEntity), resultsBuilder.build());
}
private AllocationTokenFlowUtils() {}
/** Redeems a SINGLE_USE {@link AllocationToken}, returning the redeemed copy. */
public AllocationToken redeemToken(AllocationToken token, HistoryEntryId redemptionHistoryId) {
public static AllocationToken redeemToken(
AllocationToken token, HistoryEntryId redemptionHistoryId) {
checkArgument(
token.getTokenType().isOneTimeUse(), "Only SINGLE_USE tokens can be marked as redeemed");
return token.asBuilder().setRedemptionHistoryId(redemptionHistoryId).build();
}
/**
* Validates a given token. The token could be invalid if it has allowed client IDs or TLDs that
* do not include this client ID / TLD, or if the token has a promotion that is not currently
* running, or the token is not valid for a premium name when necessary.
*
* @throws EppException if the token is invalid in any way
*/
public static void validateToken(
InternetDomainName domainName,
AllocationToken token,
CommandName commandName,
String registrarId,
boolean isPremium,
DateTime now)
throws EppException {
// Only tokens with default behavior require validation
if (!TokenBehavior.DEFAULT.equals(token.getTokenBehavior())) {
return;
}
validateTokenForPossiblePremiumName(Optional.of(token), isPremium);
if (!token.getAllowedEppActions().isEmpty()
&& !token.getAllowedEppActions().contains(commandName)) {
throw new AllocationTokenNotValidForCommandException();
}
if (!token.getAllowedRegistrarIds().isEmpty()
&& !token.getAllowedRegistrarIds().contains(registrarId)) {
throw new AllocationTokenNotValidForRegistrarException();
}
if (!token.getAllowedTlds().isEmpty()
&& !token.getAllowedTlds().contains(domainName.parent().toString())) {
throw new AllocationTokenNotValidForTldException();
}
if (token.getDomainName().isPresent()
&& !token.getDomainName().get().equals(domainName.toString())) {
throw new AllocationTokenNotValidForDomainException();
}
// Tokens without status transitions will just have a single-entry NOT_STARTED map, so only
// check the status transitions map if it's non-trivial.
if (token.getTokenStatusTransitions().size() > 1
&& !TokenStatus.VALID.equals(token.getTokenStatusTransitions().getValueAtTime(now))) {
throw new AllocationTokenNotInPromotionException();
}
}
/** Validates that the given token is valid for a premium name if the name is premium. */
public static void validateTokenForPossiblePremiumName(
Optional<AllocationToken> token, boolean isPremium)
throws AllocationTokenInvalidForPremiumNameException {
if (token.isPresent()
&& (token.get().getDiscountFraction() != 0.0 || token.get().getDiscountPrice().isPresent())
/** Don't apply discounts on premium domains if the token isn't configured that way. */
public static boolean discountTokenInvalidForPremiumName(
AllocationToken token, boolean isPremium) {
return (token.getDiscountFraction() != 0.0 || token.getDiscountPrice().isPresent())
&& isPremium
&& !token.get().shouldDiscountPremiums()) {
throw new AllocationTokenInvalidForPremiumNameException();
}
&& !token.shouldDiscountPremiums();
}
/** Loads a given token and validates that it is not redeemed */
private static AllocationToken loadToken(String token) throws EppException {
if (Strings.isNullOrEmpty(token)) {
// We load the token directly from the input XML. If it's null or empty we should throw
// an InvalidAllocationTokenException before the database load attempt fails.
// See https://tools.ietf.org/html/draft-ietf-regext-allocation-token-04#section-2.1
throw new InvalidAllocationTokenException();
}
Optional<AllocationToken> maybeTokenEntity = AllocationToken.maybeGetStaticTokenInstance(token);
if (maybeTokenEntity.isPresent()) {
return maybeTokenEntity.get();
}
// TODO(b/368069206): `reTransact` needed by tests only.
maybeTokenEntity =
tm().reTransact(() -> tm().loadByKeyIfPresent(VKey.create(AllocationToken.class, token)));
if (maybeTokenEntity.isEmpty()) {
throw new InvalidAllocationTokenException();
}
if (maybeTokenEntity.get().isRedeemed()) {
throw new AlreadyRedeemedAllocationTokenException();
}
return maybeTokenEntity.get();
}
/** Verifies and returns the allocation token if one is specified, otherwise does nothing. */
public Optional<AllocationToken> verifyAllocationTokenCreateIfPresent(
DomainCommand.Create command,
Tld tld,
/** Loads and verifies the allocation token if one is specified, otherwise does nothing. */
public static Optional<AllocationToken> loadAllocationTokenFromExtension(
String registrarId,
String domainName,
DateTime now,
Optional<AllocationTokenExtension> extension)
throws EppException {
throws NonexistentAllocationTokenException, AllocationTokenInvalidException {
if (extension.isEmpty()) {
return Optional.empty();
}
AllocationToken tokenEntity = loadToken(extension.get().getAllocationToken());
validateToken(
InternetDomainName.from(command.getDomainName()),
tokenEntity,
CommandName.CREATE,
registrarId,
isDomainPremium(command.getDomainName(), now),
now);
return Optional.of(tokenEntity);
return Optional.of(
loadAndValidateToken(extension.get().getAllocationToken(), registrarId, domainName, now));
}
/** Verifies and returns the allocation token if one is specified, otherwise does nothing. */
public Optional<AllocationToken> verifyAllocationTokenIfPresent(
Domain existingDomain,
Tld tld,
/**
* Loads the relevant token, if present, for the given extension + request.
*
* <p>This may be the allocation token provided in the request, if it is present and valid for the
* request. Otherwise, it may be a default allocation token if one is present and valid for the
* request.
*/
public static Optional<AllocationToken> loadTokenFromExtensionOrGetDefault(
String registrarId,
DateTime now,
CommandName commandName,
Optional<AllocationTokenExtension> extension)
throws EppException {
if (extension.isEmpty()) {
return Optional.empty();
Optional<AllocationTokenExtension> extension,
Tld tld,
String domainName,
CommandName commandName)
throws NonexistentAllocationTokenException, AllocationTokenInvalidException {
Optional<AllocationToken> fromExtension =
loadAllocationTokenFromExtension(registrarId, domainName, now, extension);
if (fromExtension.isPresent()
&& tokenIsValidAgainstDomain(
InternetDomainName.from(domainName), fromExtension.get(), commandName, now)) {
return fromExtension;
}
AllocationToken tokenEntity = loadToken(extension.get().getAllocationToken());
validateToken(
InternetDomainName.from(existingDomain.getDomainName()),
tokenEntity,
commandName,
registrarId,
isDomainPremium(existingDomain.getDomainName(), now),
now);
return Optional.of(tokenEntity);
return checkForDefaultToken(tld, domainName, commandName, registrarId, now);
}
public static void verifyTokenAllowedOnDomain(
/** Verifies that the given domain can have a bulk pricing token removed from it. */
public static void verifyBulkTokenAllowedOnDomain(
Domain domain, Optional<AllocationToken> allocationToken) throws EppException {
boolean domainHasBulkToken = domain.getCurrentBulkToken().isPresent();
boolean hasRemoveBulkPricingToken =
allocationToken.isPresent()
@@ -239,6 +118,11 @@ public class AllocationTokenFlowUtils {
}
}
/**
* Removes the bulk pricing token from the provided domain, if applicable.
*
* @param allocationToken the (possibly) REMOVE_BULK_PRICING token provided by the client.
*/
public static Domain maybeApplyBulkPricingRemovalToken(
Domain domain, Optional<AllocationToken> allocationToken) {
if (allocationToken.isEmpty()
@@ -249,7 +133,7 @@ public class AllocationTokenFlowUtils {
BillingRecurrence newBillingRecurrence =
tm().loadByKey(domain.getAutorenewBillingEvent())
.asBuilder()
.setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT)
.setRenewalPriceBehavior(BillingBase.RenewalPriceBehavior.DEFAULT)
.setRenewalPrice(null)
.build();
@@ -267,35 +151,139 @@ public class AllocationTokenFlowUtils {
.build();
}
/**
* Checks if the given token is valid for the given request.
*
* <p>Note that if the token is not valid, that is not a catastrophic error -- we may move on to
* trying a different token or skip token usage entirely.
*/
@VisibleForTesting
static boolean tokenIsValidAgainstDomain(
InternetDomainName domainName, AllocationToken token, CommandName commandName, DateTime now) {
if (discountTokenInvalidForPremiumName(token, isDomainPremium(domainName.toString(), now))) {
return false;
}
if (!token.getAllowedEppActions().isEmpty()
&& !token.getAllowedEppActions().contains(commandName)) {
return false;
}
if (!token.getAllowedTlds().isEmpty()
&& !token.getAllowedTlds().contains(domainName.parent().toString())) {
return false;
}
return token.getDomainName().isEmpty()
|| token.getDomainName().get().equals(domainName.toString());
}
/**
* Checks if there is a valid default token to be used for a domain create command.
*
* <p>If there is more than one valid default token for the registration, only the first valid
* token found on the TLD's default token list will be returned.
*/
private static Optional<AllocationToken> checkForDefaultToken(
Tld tld, String domainName, CommandName commandName, String registrarId, DateTime now) {
ImmutableList<VKey<AllocationToken>> tokensFromTld = tld.getDefaultPromoTokens();
if (isNullOrEmpty(tokensFromTld)) {
return Optional.empty();
}
Map<VKey<AllocationToken>, Optional<AllocationToken>> tokens =
AllocationToken.getAll(tokensFromTld);
checkState(
!isNullOrEmpty(tokens), "Failure while loading default TLD tokens from the database");
// Iterate over the list to maintain token ordering (since we return the first valid token)
ImmutableList<AllocationToken> tokenList =
tokensFromTld.stream()
.map(tokens::get)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(toImmutableList());
// Check if any of the tokens are valid for this domain registration
for (AllocationToken token : tokenList) {
try {
validateTokenEntity(token, registrarId, domainName, now);
} catch (AllocationTokenInvalidException e) {
// Token is not valid for this registrar, etc. -- continue trying tokens
continue;
}
if (tokenIsValidAgainstDomain(InternetDomainName.from(domainName), token, commandName, now)) {
return Optional.of(token);
}
}
// No valid default token found
return Optional.empty();
}
/** Loads a given token and validates it against the registrar, time, etc */
private static AllocationToken loadAndValidateToken(
String token, String registrarId, String domainName, DateTime now)
throws NonexistentAllocationTokenException, AllocationTokenInvalidException {
if (Strings.isNullOrEmpty(token)) {
// We load the token directly from the input XML. If it's null or empty we should throw
// an NonexistentAllocationTokenException before the database load attempt fails.
// See https://tools.ietf.org/html/draft-ietf-regext-allocation-token-04#section-2.1
throw new NonexistentAllocationTokenException();
}
Optional<AllocationToken> maybeTokenEntity = AllocationToken.maybeGetStaticTokenInstance(token);
if (maybeTokenEntity.isPresent()) {
return maybeTokenEntity.get();
}
maybeTokenEntity = AllocationToken.get(VKey.create(AllocationToken.class, token));
if (maybeTokenEntity.isEmpty()) {
throw new NonexistentAllocationTokenException();
}
AllocationToken tokenEntity = maybeTokenEntity.get();
validateTokenEntity(tokenEntity, registrarId, domainName, now);
return tokenEntity;
}
private static void validateTokenEntity(
AllocationToken token, String registrarId, String domainName, DateTime now)
throws AllocationTokenInvalidException {
if (token.isRedeemed()) {
throw new AlreadyRedeemedAllocationTokenException();
}
if (!token.getAllowedRegistrarIds().isEmpty()
&& !token.getAllowedRegistrarIds().contains(registrarId)) {
throw new AllocationTokenNotValidForRegistrarException();
}
// Tokens without status transitions will just have a single-entry NOT_STARTED map, so only
// check the status transitions map if it's non-trivial.
if (token.getTokenStatusTransitions().size() > 1
&& !AllocationToken.TokenStatus.VALID.equals(
token.getTokenStatusTransitions().getValueAtTime(now))) {
throw new AllocationTokenNotInPromotionException();
}
if (token.getDomainName().isPresent() && !token.getDomainName().get().equals(domainName)) {
throw new AllocationTokenNotValidForDomainException();
}
}
// Note: exception messages should be <= 32 characters long for domain check results
/** The allocation token exists but is not valid, e.g. the wrong registrar. */
public abstract static class AllocationTokenInvalidException
extends StatusProhibitsOperationException {
AllocationTokenInvalidException(String message) {
super(message);
}
}
/** The allocation token is not currently valid. */
public static class AllocationTokenNotInPromotionException
extends StatusProhibitsOperationException {
extends AllocationTokenInvalidException {
AllocationTokenNotInPromotionException() {
super("Alloc token not in promo period");
}
}
/** The allocation token is not valid for this TLD. */
public static class AllocationTokenNotValidForTldException
extends AssociationProhibitsOperationException {
AllocationTokenNotValidForTldException() {
super("Alloc token invalid for TLD");
}
}
/** The allocation token is not valid for this domain. */
public static class AllocationTokenNotValidForDomainException
extends AssociationProhibitsOperationException {
AllocationTokenNotValidForDomainException() {
super("Alloc token invalid for domain");
}
}
/** The allocation token is not valid for this registrar. */
public static class AllocationTokenNotValidForRegistrarException
extends AssociationProhibitsOperationException {
extends AllocationTokenInvalidException {
AllocationTokenNotValidForRegistrarException() {
super("Alloc token invalid for client");
}
@@ -303,23 +291,23 @@ public class AllocationTokenFlowUtils {
/** The allocation token was already redeemed. */
public static class AlreadyRedeemedAllocationTokenException
extends AssociationProhibitsOperationException {
extends AllocationTokenInvalidException {
AlreadyRedeemedAllocationTokenException() {
super("Alloc token was already redeemed");
}
}
/** The allocation token is not valid for this EPP command. */
public static class AllocationTokenNotValidForCommandException
extends AssociationProhibitsOperationException {
AllocationTokenNotValidForCommandException() {
super("Allocation token not valid for the EPP command");
/** The allocation token is not valid for this domain. */
public static class AllocationTokenNotValidForDomainException
extends AllocationTokenInvalidException {
AllocationTokenNotValidForDomainException() {
super("Alloc token invalid for domain");
}
}
}
/** The allocation token is invalid. */
public static class InvalidAllocationTokenException extends AuthorizationErrorException {
InvalidAllocationTokenException() {
public static class NonexistentAllocationTokenException extends AuthorizationErrorException {
NonexistentAllocationTokenException() {
super("The allocation token is invalid");
}
}
@@ -404,7 +404,7 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B
* <p>Don't use this unless you really need it for performance reasons, and be sure that you are
* OK with the trade-offs in loss of transactional consistency.
*/
public static ImmutableMap<VKey<? extends EppResource>, EppResource> loadCached(
public static ImmutableMap<VKey<? extends EppResource>, EppResource> loadByCacheIfEnabled(
Iterable<VKey<? extends EppResource>> keys) {
if (!RegistryConfig.isEppResourceCachingEnabled()) {
return tm().reTransact(() -> tm().loadByKeys(keys));
@@ -413,15 +413,12 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B
}
/**
* Loads a given EppResource by its key using the cache (if enabled).
* Loads a given EppResource by its key using the cache.
*
* <p>Don't use this unless you really need it for performance reasons, and be sure that you are
* OK with the trade-offs in loss of transactional consistency.
* <p>This method ignores the `isEppResourceCachingEnabled` config setting. It is reserved for use
* cases that can tolerate slightly stale data, e.g., RDAP queries.
*/
public static <T extends EppResource> T loadCached(VKey<T> key) {
if (!RegistryConfig.isEppResourceCachingEnabled()) {
return tm().reTransact(() -> tm().loadByKey(key));
}
public static <T extends EppResource> T loadByCache(VKey<T> key) {
// Safe to cast because loading a Key<T> returns an entity of type T.
@SuppressWarnings("unchecked")
T resource = (T) cacheEppResources.get(key);
@@ -16,6 +16,7 @@ package google.registry.model;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.DateTimeUtils.isAtOrAfter;
@@ -40,6 +41,7 @@ import google.registry.model.transfer.DomainTransferData;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.TransactionManager;
import jakarta.persistence.Query;
import java.util.Collection;
import java.util.Comparator;
@@ -109,12 +111,12 @@ public final class EppResourceUtils {
*/
public static <T extends EppResource> Optional<T> loadByForeignKey(
Class<T> clazz, String foreignKey, DateTime now) {
return loadByForeignKeyHelper(clazz, foreignKey, now, false);
return loadByForeignKeyHelper(tm(), clazz, foreignKey, now, false);
}
/**
* Loads the last created version of an {@link EppResource} from the database by foreign key,
* using a cache.
* using a cache, if caching is enabled in config settings.
*
* <p>Returns null if no resource with this foreign key was ever created, or if the most recently
* created resource was deleted before time "now".
@@ -134,20 +136,36 @@ public final class EppResourceUtils {
* @param foreignKey id to match
* @param now the current logical time to project resources at
*/
public static <T extends EppResource> Optional<T> loadByForeignKeyCached(
public static <T extends EppResource> Optional<T> loadByForeignKeyByCacheIfEnabled(
Class<T> clazz, String foreignKey, DateTime now) {
return loadByForeignKeyHelper(
clazz, foreignKey, now, RegistryConfig.isEppResourceCachingEnabled());
tm(), clazz, foreignKey, now, RegistryConfig.isEppResourceCachingEnabled());
}
/**
* Loads the last created version of an {@link EppResource} from the replica database by foreign
* key, using a cache.
*
* <p>This method ignores the config setting for caching, and is reserved for use cases that can
* tolerate slightly stale data.
*/
public static <T extends EppResource> Optional<T> loadByForeignKeyByCache(
Class<T> clazz, String foreignKey, DateTime now) {
return loadByForeignKeyHelper(replicaTm(), clazz, foreignKey, now, true);
}
private static <T extends EppResource> Optional<T> loadByForeignKeyHelper(
Class<T> clazz, String foreignKey, DateTime now, boolean useCache) {
TransactionManager txnManager,
Class<T> clazz,
String foreignKey,
DateTime now,
boolean useCache) {
checkArgument(
ForeignKeyedEppResource.class.isAssignableFrom(clazz),
"loadByForeignKey may only be called for foreign keyed EPP resources");
VKey<T> key =
useCache
? ForeignKeyUtils.loadCached(clazz, ImmutableList.of(foreignKey), now).get(foreignKey)
? ForeignKeyUtils.loadByCache(clazz, ImmutableList.of(foreignKey), now).get(foreignKey)
: ForeignKeyUtils.load(clazz, foreignKey, now);
// The returned key is null if the resource is hard deleted or soft deleted by the given time.
if (key == null) {
@@ -155,10 +173,10 @@ public final class EppResourceUtils {
}
T resource =
useCache
? EppResource.loadCached(key)
? EppResource.loadByCache(key)
// This transaction is buried very deeply inside many outer nested calls, hence merits
// the use of reTransact() for now pending a substantial refactoring.
: tm().reTransact(() -> tm().loadByKeyIfPresent(key).orElse(null));
: txnManager.reTransact(() -> txnManager.loadByKeyIfPresent(key).orElse(null));
if (resource == null || isAtOrAfter(now, resource.getDeletionTime())) {
return Optional.empty();
}
@@ -204,11 +204,25 @@ public final class ForeignKeyUtils {
* <p>Don't use the cached version of this method unless you really need it for performance
* reasons, and are OK with the trade-offs in loss of transactional consistency.
*/
public static <E extends EppResource> ImmutableMap<String, VKey<E>> loadCached(
public static <E extends EppResource> ImmutableMap<String, VKey<E>> loadByCacheIfEnabled(
Class<E> clazz, Collection<String> foreignKeys, final DateTime now) {
if (!RegistryConfig.isEppResourceCachingEnabled()) {
return load(clazz, foreignKeys, now);
}
return loadByCache(clazz, foreignKeys, now);
}
/**
* Load a list of {@link VKey} to {@link EppResource} instances by class and foreign key strings
* that are active at or after the specified moment in time, using the cache.
*
* <p>The returned map will omit any keys for which the {@link EppResource} doesn't exist or has
* been soft-deleted.
*
* <p>This method is reserved for use cases that can tolerate slightly stale data.
*/
public static <E extends EppResource> ImmutableMap<String, VKey<E>> loadByCache(
Class<E> clazz, Collection<String> foreignKeys, final DateTime now) {
return foreignKeyCache
.getAll(foreignKeys.stream().map(fk -> VKey.create(clazz, fk)).collect(toImmutableList()))
.entrySet()
@@ -0,0 +1,150 @@
// Copyright 2025 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.model.console;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
import google.registry.persistence.WithVKey;
import jakarta.persistence.AttributeOverride;
import jakarta.persistence.AttributeOverrides;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import java.util.Optional;
import java.util.UUID;
import org.joda.time.DateTime;
/**
* Represents a password reset request of some type.
*
* <p>Password reset requests must be performed within an hour of the time that they were requested,
* as well as requiring that the requester and the fulfiller have the proper respective permissions.
*/
@Entity
@WithVKey(String.class)
public class PasswordResetRequest extends ImmutableObject implements Buildable {
public enum Type {
EPP,
REGISTRY_LOCK
}
@Id private String verificationCode;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
Type type;
@AttributeOverrides({
@AttributeOverride(
name = "creationTime",
column = @Column(name = "requestTime", nullable = false))
})
CreateAutoTimestamp requestTime = CreateAutoTimestamp.create(null);
@Column(nullable = false)
String requester;
@Column DateTime fulfillmentTime;
@Column(nullable = false)
String destinationEmail;
@Column(nullable = false)
String registrarId;
public String getVerificationCode() {
return verificationCode;
}
public Type getType() {
return type;
}
public DateTime getRequestTime() {
return requestTime.getTimestamp();
}
public String getRequester() {
return requester;
}
public Optional<DateTime> getFulfillmentTime() {
return Optional.ofNullable(fulfillmentTime);
}
public String getDestinationEmail() {
return destinationEmail;
}
public String getRegistrarId() {
return registrarId;
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
}
/** Builder for constructing immutable {@link PasswordResetRequest} objects. */
public static class Builder extends Buildable.Builder<PasswordResetRequest> {
public Builder() {}
private Builder(PasswordResetRequest instance) {
super(instance);
}
@Override
public PasswordResetRequest build() {
checkArgumentNotNull(getInstance().type, "Type must be specified");
checkArgumentNotNull(getInstance().requester, "Requester must be specified");
checkArgumentNotNull(getInstance().destinationEmail, "Destination email must be specified");
checkArgumentNotNull(getInstance().registrarId, "Registrar ID must be specified");
getInstance().verificationCode = UUID.randomUUID().toString();
return super.build();
}
public Builder setType(Type type) {
getInstance().type = type;
return this;
}
public Builder setRequester(String requester) {
getInstance().requester = requester;
return this;
}
public Builder setDestinationEmail(String destinationEmail) {
getInstance().destinationEmail = destinationEmail;
return this;
}
public Builder setRegistrarId(String registrarId) {
getInstance().registrarId = registrarId;
return this;
}
public Builder setFulfillmentTime(DateTime fulfillmentTime) {
getInstance().fulfillmentTime = fulfillmentTime;
return this;
}
}
}
@@ -441,7 +441,8 @@ public class DomainCommand {
private static <T extends EppResource> ImmutableMap<String, VKey<T>> loadByForeignKeysCached(
final Set<String> foreignKeys, final Class<T> clazz, final DateTime now)
throws InvalidReferencesException {
ImmutableMap<String, VKey<T>> fks = ForeignKeyUtils.loadCached(clazz, foreignKeys, now);
ImmutableMap<String, VKey<T>> fks =
ForeignKeyUtils.loadByCacheIfEnabled(clazz, foreignKeys, now);
if (!fks.keySet().equals(foreignKeys)) {
throw new InvalidReferencesException(
clazz, ImmutableSet.copyOf(difference(foreignKeys, fks.keySet())));
@@ -27,6 +27,7 @@ import static com.google.common.io.BaseEncoding.base64;
import static google.registry.config.RegistryConfig.getDefaultRegistrarWhoisServer;
import static google.registry.model.CacheUtils.memoizeWithShortExpiration;
import static google.registry.model.tld.Tlds.assertTldsExist;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableSortedCopy;
@@ -62,6 +63,7 @@ import google.registry.model.tld.Tld.TldType;
import google.registry.persistence.VKey;
import google.registry.persistence.converter.CidrBlockListUserType;
import google.registry.persistence.converter.CurrencyToStringMapUserType;
import google.registry.persistence.transaction.TransactionManager;
import google.registry.util.CidrAddressBlock;
import google.registry.util.PasswordUtils;
import jakarta.mail.internet.AddressException;
@@ -403,6 +405,9 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
*/
DateTime lastExpiringFailoverCertNotificationSentDate = START_OF_TIME;
/** The time that the POCs have been reviewed last. */
@Expose DateTime lastPocVerificationDate = START_OF_TIME;
/** Telephone support passcode (5-digit numeric) */
String phonePasscode;
@@ -461,6 +466,10 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
return registrarName;
}
public DateTime getLastPocVerificationDate() {
return lastPocVerificationDate;
}
public Type getType() {
return type;
}
@@ -569,7 +578,20 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
* address.
*/
public ImmutableSortedSet<RegistrarPoc> getContacts() {
return getContactPocs().stream()
return getContactPocs(tm()).stream()
.filter(Objects::nonNull)
.collect(toImmutableSortedSet(CONTACT_EMAIL_COMPARATOR));
}
/**
* Returns a list of all {@link RegistrarPoc} objects for this registrar sorted by their email
* address.
*
* <p>This method queries the replica database. It is reserved for use cases that can tolerate
* slightly stale data.
*/
public ImmutableSortedSet<RegistrarPoc> getContactsFromReplica() {
return getContactPocs(replicaTm()).stream()
.filter(Objects::nonNull)
.collect(toImmutableSortedSet(CONTACT_EMAIL_COMPARATOR));
}
@@ -579,7 +601,7 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
* their email address.
*/
public ImmutableSortedSet<RegistrarPoc> getContactsOfType(final RegistrarPoc.Type type) {
return getContactPocs().stream()
return getContactPocs(tm()).stream()
.filter(Objects::nonNull)
.filter((@Nullable RegistrarPoc contact) -> contact.getTypes().contains(type))
.collect(toImmutableSortedSet(CONTACT_EMAIL_COMPARATOR));
@@ -593,13 +615,8 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
return getContacts().stream().filter(RegistrarPoc::getVisibleInDomainWhoisAsAbuse).findFirst();
}
private ImmutableSet<RegistrarPoc> getContactPocs() {
return tm().transact(
() ->
tm().query("FROM RegistrarPoc WHERE registrarId = :registrarId", RegistrarPoc.class)
.setParameter("registrarId", registrarId)
.getResultStream()
.collect(toImmutableSet()));
private ImmutableList<RegistrarPoc> getContactPocs(TransactionManager txnManager) {
return txnManager.transact(() -> RegistrarPoc.loadForRegistrar(registrarId));
}
@Override
@@ -614,6 +631,7 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
.putString(
"lastExpiringFailoverCertNotificationSentDate",
lastExpiringFailoverCertNotificationSentDate)
.putString("lastPocVerificationDate", lastPocVerificationDate)
.put("registrarName", registrarName)
.put("type", type)
.put("state", state)
@@ -802,6 +820,12 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
return thisCastToDerived();
}
public B setLastPocVerificationDate(DateTime now) {
checkArgumentNotNull(now, "Registrar lastPocVerificationDate cannot be null");
getInstance().lastPocVerificationDate = now;
return thisCastToDerived();
}
private static String calculateHash(String clientCertificate) {
if (clientCertificate == null) {
return null;
@@ -27,6 +27,7 @@ import static google.registry.util.PasswordUtils.hashPassword;
import static java.util.stream.Collectors.joining;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.gson.annotations.Expose;
@@ -36,6 +37,7 @@ import google.registry.model.JsonMapBuilder;
import google.registry.model.Jsonifiable;
import google.registry.model.UnsafeSerializable;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.QueryComposer;
import google.registry.util.PasswordUtils;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@@ -93,6 +95,10 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
}
}
@Expose
@Column(insertable = false, updatable = false)
protected Long id;
/** The name of the contact. */
@Expose String name;
@@ -179,6 +185,10 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
tm().putAll(contacts);
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
@@ -295,6 +305,7 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
.put("visibleInDomainWhoisAsAbuse", visibleInDomainWhoisAsAbuse)
.put("allowedToSetRegistryLockPassword", allowedToSetRegistryLockPassword)
.put("registryLockAllowed", isRegistryLockAllowed())
.put("id", getId())
.build();
}
@@ -423,6 +434,12 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
}
}
public static ImmutableList<RegistrarPoc> loadForRegistrar(String registrarId) {
return tm().createQueryComposer(RegistrarPoc.class)
.where("registrarId", QueryComposer.Comparator.EQ, registrarId)
.list();
}
/** Class to represent the composite primary key for {@link RegistrarPoc} entity. */
@VisibleForTesting
public static class RegistrarPocId extends ImmutableObject implements Serializable {
@@ -38,10 +38,8 @@ import google.registry.dns.PublishDnsUpdatesAction;
import google.registry.dns.ReadDnsRefreshRequestsAction;
import google.registry.dns.RefreshDnsAction;
import google.registry.dns.RefreshDnsOnHostRenameAction;
import google.registry.dns.writer.VoidDnsWriterModule;
import google.registry.dns.writer.clouddns.CloudDnsWriterModule;
import google.registry.dns.writer.DnsWritersModule;
import google.registry.dns.writer.dnsupdate.DnsUpdateConfigModule;
import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule;
import google.registry.export.ExportDomainListsAction;
import google.registry.export.ExportPremiumTermsAction;
import google.registry.export.ExportReservedTermsAction;
@@ -140,14 +138,13 @@ import google.registry.whois.WhoisModule;
BatchModule.class,
BillingModule.class,
CheckApiModule.class,
CloudDnsWriterModule.class,
ConsoleModule.class,
CronModule.class,
CustomLogicModule.class,
DnsCountQueryCoordinatorModule.class,
DnsModule.class,
DnsUpdateConfigModule.class,
DnsUpdateWriterModule.class,
DnsWritersModule.class,
EppTlsModule.class,
EppToolModule.class,
IcannReportingModule.class,
@@ -160,7 +157,6 @@ import google.registry.whois.WhoisModule;
Spec11Module.class,
TmchModule.class,
ToolsServerModule.class,
VoidDnsWriterModule.class,
WhiteboxModule.class,
WhoisModule.class,
})
@@ -22,7 +22,6 @@ import google.registry.bigquery.BigqueryModule;
import google.registry.config.CloudTasksUtilsModule;
import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.dns.writer.VoidDnsWriterModule;
import google.registry.export.DriveModule;
import google.registry.export.sheet.SheetsServiceModule;
import google.registry.flows.ServerTridProviderModule;
@@ -73,7 +72,6 @@ import jakarta.inject.Singleton;
SheetsServiceModule.class,
StackdriverModule.class,
UrlConnectionServiceModule.class,
VoidDnsWriterModule.class,
UtilsModule.class
})
interface BackendComponent {
@@ -34,10 +34,8 @@ import google.registry.dns.PublishDnsUpdatesAction;
import google.registry.dns.ReadDnsRefreshRequestsAction;
import google.registry.dns.RefreshDnsAction;
import google.registry.dns.RefreshDnsOnHostRenameAction;
import google.registry.dns.writer.VoidDnsWriterModule;
import google.registry.dns.writer.clouddns.CloudDnsWriterModule;
import google.registry.dns.writer.DnsWritersModule;
import google.registry.dns.writer.dnsupdate.DnsUpdateConfigModule;
import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule;
import google.registry.export.ExportDomainListsAction;
import google.registry.export.ExportPremiumTermsAction;
import google.registry.export.ExportReservedTermsAction;
@@ -82,13 +80,12 @@ import google.registry.tmch.TmchSmdrlAction;
modules = {
BatchModule.class,
BillingModule.class,
CloudDnsWriterModule.class,
CronModule.class,
CustomLogicModule.class,
DnsCountQueryCoordinatorModule.class,
DnsModule.class,
DnsUpdateConfigModule.class,
DnsUpdateWriterModule.class,
DnsWritersModule.class,
IcannReportingModule.class,
RdeModule.class,
ReportingModule.class,
@@ -96,7 +93,6 @@ import google.registry.tmch.TmchSmdrlAction;
SheetModule.class,
Spec11Module.class,
TmchModule.class,
VoidDnsWriterModule.class,
WhiteboxModule.class,
})
public interface BackendRequestComponent {
@@ -276,10 +276,6 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
T result = work.call();
txn.commit();
long duration = clock.nowUtc().getMillis() - txnInfo.transactionTime.getMillis();
if (duration >= 100) {
logger.atInfo().log("Transaction duration: %d milliseconds", duration);
}
return result;
} catch (Throwable e) {
// Catch a Throwable here so even Errors would lead to a rollback.
@@ -336,7 +336,7 @@ abstract class AbstractJsonableObject implements Jsonable {
// According to RFC 9083 section 3, the syntax of dates and times is defined in RFC3339.
//
// According to RFC3339, we should use ISO8601, which is what DateTime.toString does!
return new JsonPrimitive(((DateTime) object).toString());
return new JsonPrimitive(object.toString());
}
if (object == null) {
return JsonNull.INSTANCE;
@@ -24,10 +24,14 @@ import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.EppResource;
import google.registry.model.registrar.Registrar;
@@ -41,6 +45,7 @@ import google.registry.request.HttpException;
import google.registry.request.Parameter;
import google.registry.request.RequestMethod;
import google.registry.request.RequestPath;
import google.registry.request.RequestUrl;
import google.registry.request.Response;
import google.registry.util.Clock;
import jakarta.inject.Inject;
@@ -50,7 +55,7 @@ import java.util.Optional;
import org.joda.time.DateTime;
/**
* Base RDAP (new WHOIS) action for all requests.
* Base RDAP action for all requests.
*
* @see <a href="https://tools.ietf.org/html/rfc9082">RFC 9082: Registration Data Access Protocol
* (RDAP) Query Format</a>
@@ -75,6 +80,7 @@ public abstract class RdapActionBase implements Runnable {
@Inject Response response;
@Inject @RequestMethod Action.Method requestMethod;
@Inject @RequestPath String requestPath;
@Inject @RequestUrl String requestUrl;
@Inject RdapAuthorization rdapAuthorization;
@Inject RdapJsonFormatter rdapJsonFormatter;
@Inject @Parameter("includeDeleted") Optional<Boolean> includeDeletedParam;
@@ -132,7 +138,7 @@ public abstract class RdapActionBase implements Runnable {
// RFC7480 4.2 - servers receiving an RDAP request return an entity with a Content-Type header
// containing the RDAP-specific JSON media type.
response.setContentType(RESPONSE_MEDIA_TYPE);
// RDAP Technical Implementation Guide 1.13 - when responding to RDAP valid requests, we MUST
// RDAP Technical Implementation Guide 1.14 - when responding to RDAP valid requests, we MUST
// include the Access-Control-Allow-Origin, which MUST be "*" unless otherwise specified.
response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
try {
@@ -198,7 +204,9 @@ public abstract class RdapActionBase implements Runnable {
TopLevelReplyObject topLevelObject =
TopLevelReplyObject.create(replyObject, rdapJsonFormatter.createTosNotice());
Gson gson = formatOutputParam.orElse(false) ? FORMATTED_OUTPUT_GSON : GSON;
response.setPayload(gson.toJson(topLevelObject.toJson()));
JsonObject jsonResult = topLevelObject.toJson();
addLinkValuesRecursively(jsonResult);
response.setPayload(gson.toJson(jsonResult));
}
/**
@@ -264,4 +272,34 @@ public abstract class RdapActionBase implements Runnable {
return rdapJsonFormatter.getRequestTime();
}
/**
* Adds a request-referencing "value" to each link object.
*
* <p>This is the "context URI" as described in RFC 8288. Basically, this contains a reference to
* the request URL that generated this RDAP response.
*
* <p>This is required per the RDAP February 2024 response profile sections 2.6.3 and 2.10, and
* the technical implementation guide sections 3.2 and 3.3.2.
*
* <p>We must do this here (instead of where the links are generated) because many of the links
* (e.g. terms of service) are static constants, and thus cannot by default know what the request
* URL was.
*/
private void addLinkValuesRecursively(JsonElement jsonElement) {
if (jsonElement instanceof JsonArray jsonArray) {
jsonArray.forEach(this::addLinkValuesRecursively);
} else if (jsonElement instanceof JsonObject jsonObject) {
if (jsonObject.get("links") instanceof JsonArray linksArray) {
addLinkValues(linksArray);
}
jsonObject.entrySet().forEach(entry -> addLinkValuesRecursively(entry.getValue()));
}
}
private void addLinkValues(JsonArray linksArray) {
Streams.stream(linksArray)
.map(JsonElement::getAsJsonObject)
.filter(o -> !o.has("value"))
.forEach(o -> o.addProperty("value", requestUrl));
}
}
@@ -26,7 +26,7 @@ import google.registry.request.auth.Auth;
import jakarta.inject.Inject;
/**
* RDAP (new WHOIS) action for RDAP autonomous system number requests.
* RDAP action for RDAP autonomous system number requests.
*
* <p>This feature is not implemented because it's only necessary for <i>address</i> registries like
* ARIN, not domain registries.
@@ -41,14 +41,13 @@ final class RdapDataStructures {
// Conformance to RFC 9083
jsonArray.add("rdap_level_0");
// Conformance to the RDAP Response Profile V2.1
// Conformance to the RDAP Response Profile V2.2 (February 2024)
// (see section 1.2)
jsonArray.add("icann_rdap_response_profile_1");
// Conformance to the RDAP Technical Implementation Guide V2.2 (February 2024)
// (see section 1.3)
jsonArray.add("icann_rdap_response_profile_0");
// Conformance to the RDAP Technical Implementation Guide V2.1
// (see section 1.14)
jsonArray.add("icann_rdap_technical_implementation_guide_0");
jsonArray.add("icann_rdap_technical_implementation_guide_1");
return jsonArray;
}
}
@@ -15,7 +15,7 @@
package google.registry.rdap;
import static google.registry.flows.domain.DomainFlowUtils.validateDomainName;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
@@ -36,7 +36,7 @@ import google.registry.request.auth.Auth;
import jakarta.inject.Inject;
import java.util.Optional;
/** RDAP (new WHOIS) action for domain requests. */
/** RDAP action for domain requests. */
@Action(
service = GaeService.PUBAPI,
path = "/rdap/domain/",
@@ -65,7 +65,7 @@ public class RdapDomainAction extends RdapActionBase {
}
// The query string is not used; the RDAP syntax is /rdap/domain/mydomain.com.
Optional<Domain> domain =
loadByForeignKeyCached(
loadByForeignKeyByCache(
Domain.class,
pathSearchString,
shouldIncludeDeleted() ? START_OF_TIME : rdapJsonFormatter.getRequestTime());
@@ -15,7 +15,7 @@
package google.registry.rdap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
@@ -51,6 +51,7 @@ import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.util.NonFinalForTesting;
import jakarta.inject.Inject;
import jakarta.persistence.Query;
import jakarta.persistence.criteria.CriteriaBuilder;
import java.net.InetAddress;
import java.util.Comparator;
@@ -60,17 +61,15 @@ import java.util.stream.Stream;
import org.hibernate.Hibernate;
/**
* RDAP (new WHOIS) action for domain search requests.
* RDAP action for domain search requests.
*
* <p>All commands and responses conform to the RDAP spec as defined in RFCs 7480 through 7485.
* <p>All commands and responses conform to the RDAP spec as defined in STD 95 and its RFCs.
*
* @see <a href="http://tools.ietf.org/html/rfc9082">RFC 9082: Registration Data Access Protocol
* (RDAP) Query Format</a>
* @see <a href="http://tools.ietf.org/html/rfc9083">RFC 9083: JSON Responses for the Registration
* Data Access Protocol (RDAP)</a>
*/
// TODO: This isn't required by the RDAP Technical Implementation Guide, and hence should be
// deleted, at least until it's actually required.
@Action(
service = GaeService.PUBAPI,
path = "/rdap/domains",
@@ -184,7 +183,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
private DomainSearchResponse searchByDomainNameWithoutWildcard(
final RdapSearchPattern partialStringQuery) {
Optional<Domain> domain =
loadByForeignKeyCached(
loadByForeignKeyByCache(
Domain.class, partialStringQuery.getInitialString(), getRequestTime());
return makeSearchResults(
shouldBeVisible(domain) ? ImmutableList.of(domain.get()) : ImmutableList.of());
@@ -339,7 +338,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
Optional<String> desiredRegistrar = getDesiredRegistrar();
if (desiredRegistrar.isPresent()) {
Optional<Host> host =
loadByForeignKeyCached(
loadByForeignKeyByCache(
Host.class,
partialStringQuery.getInitialString(),
shouldIncludeDeleted() ? START_OF_TIME : getRequestTime());
@@ -364,7 +363,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
// through the subordinate hosts. This is more efficient, and lets us permit wildcard searches
// with no initial string.
Domain domain =
loadByForeignKeyCached(
loadByForeignKeyByCache(
Domain.class,
partialStringQuery.getSuffix(),
shouldIncludeDeleted() ? START_OF_TIME : getRequestTime())
@@ -381,7 +380,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
if (partialStringQuery.matches(fqhn)) {
if (desiredRegistrar.isPresent()) {
Optional<Host> host =
loadByForeignKeyCached(
loadByForeignKeyByCache(
Host.class, fqhn, shouldIncludeDeleted() ? START_OF_TIME : getRequestTime());
if (host.isPresent()
&& desiredRegistrar
@@ -442,7 +441,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
replicaTm()
.transact(
() -> {
jakarta.persistence.Query query =
Query query =
replicaTm()
.getEntityManager()
.createNativeQuery(queryBuilder.toString())
@@ -37,14 +37,12 @@ import jakarta.inject.Inject;
import java.util.Optional;
/**
* RDAP (new WHOIS) action for entity (contact and registrar) requests. the ICANN operational
* profile dictates that the "handle" for registrars is to be the IANA registrar ID:
* RDAP action for entity (contact and registrar) requests. the ICANN operational profile dictates
* that the "handle" for registrars is to be the IANA registrar ID:
*
* <p>2.8.3. Registries MUST support lookup for entities with the registrar role within other
* objects using the handle (as described in 3.1.5 of RFC 9082). The handle of the entity with the
* registrar role MUST be equal to IANA Registrar ID. The entity with the registrar role in the RDAP
* response MUST contain a publicIDs member to identify the IANA Registrar ID from the IANAs
* Registrar ID registry. The type value of the publicID object MUST be equal to IANA Registrar ID.
* <p>2.4.1.Registry RDAP servers MUST support Registrar object lookup using an entity path request
* for entities with the registrar role using the handle (as described in 3.1.5 of RFC9082) where
* the handle of the entity with the registrar role is be [sic] equal to the IANA Registrar ID.
*/
@Action(
service = GaeService.PUBAPI,
@@ -104,7 +102,7 @@ public class RdapEntityAction extends RdapActionBase {
// query, it MUST reply with 404 response code.
//
// Note we don't do RFC7480 5.3 - returning a different code if we wish to say "this info
// exists but we don't want to show it to you", because we DON'T wish to say that.
// exists, but we don't want to show it to you", because we DON'T wish to say that.
throw new NotFoundException(pathSearchString + " not found");
}
}
@@ -49,9 +49,9 @@ import java.util.List;
import java.util.Optional;
/**
* RDAP (new WHOIS) action for entity (contact and registrar) search requests.
* RDAP action for entity (contact and registrar) search requests.
*
* <p>All commands and responses conform to the RDAP spec as defined in RFCs 7480 through 7485.
* <p>All commands and responses conform to the RDAP spec as defined in STD 95 and its RFCs.
*
* <p>The RDAP specification lumps contacts and registrars together and calls them "entities", which
* is confusing for us, because "entity" means something else in SQL. But here, when we use the
@@ -76,8 +76,6 @@ import java.util.Optional;
* @see <a href="http://tools.ietf.org/html/rfc9083">RFC 9083: JSON Responses for the Registration
* Data Access Protocol (RDAP)</a>
*/
// TODO: This isn't required by the RDAP Technical Implementation Guide, and hence should be
// deleted, at least until it's actually required.
@Action(
service = GaeService.PUBAPI,
path = "/rdap/entities",
@@ -28,7 +28,7 @@ import google.registry.request.auth.Auth;
import jakarta.inject.Inject;
import java.util.Optional;
/** RDAP (new WHOIS) action for help requests. */
/** RDAP action for help requests. */
@Action(
service = GaeService.PUBAPI,
path = RdapHelpAction.PATH,
@@ -22,42 +22,34 @@ import google.registry.rdap.RdapDataStructures.Remark;
/**
* This file contains boilerplate required by the ICANN RDAP Profile.
*
* @see <a href="https://www.icann.org/resources/pages/rdap-operational-profile-2016-07-26-en">RDAP
* Operational Profile for gTLD Registries and Registrars</a>
* @see <a
* href="https://itp.cdn.icann.org/en/files/registry-operators/rdap-response-profile-21feb24-en.pdf">
* RDAP Response Profile</a>
*/
public class RdapIcannStandardInformation {
/** Required by ICANN RDAP Profile section 1.4.10. */
private static final Notice CONFORMANCE_NOTICE =
Notice.builder()
.setDescription(
"This response conforms to the RDAP Operational Profile for gTLD Registries and"
+ " Registrars version 1.0")
.build();
/** Required by ICANN RDAP Profile section 1.5.18. */
/** Required by RDAP Response Profile section 2.6.3. */
private static final Notice DOMAIN_STATUS_CODES_NOTICE =
Notice.builder()
.setTitle("Status Codes")
.setDescription(
"For more information on domain status codes, please visit"
+ " https://icann.org/epp")
"For more information on domain status codes, please visit https://icann.org/epp")
.addLink(
Link.builder()
.setRel("alternate")
.setRel("glossary")
.setHref("https://icann.org/epp")
.setType("text/html")
.build())
.build();
/** Required by ICANN RDAP Response Profile section 2.11. */
/** Required by RDAP Response Profile section 2.10. */
private static final Notice INACCURACY_COMPLAINT_FORM_NOTICE =
Notice.builder()
.setTitle("RDDS Inaccuracy Complaint Form")
.setDescription("URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf")
.addLink(
Link.builder()
.setRel("alternate")
.setRel("help")
.setHref("https://icann.org/wicf")
.setType("text/html")
.build())
@@ -79,28 +71,16 @@ public class RdapIcannStandardInformation {
/** Boilerplate notices required by domain responses. */
static final ImmutableList<Notice> DOMAIN_BOILERPLATE_NOTICES =
ImmutableList.of(
CONFORMANCE_NOTICE,
// RDAP Response Profile 2.6.3
DOMAIN_STATUS_CODES_NOTICE,
// RDAP Response Profile 2.11
// RDAP Response Profile 2.10
INACCURACY_COMPLAINT_FORM_NOTICE);
/** Boilerplate notice for when a domain is blocked by BSA. */
static final ImmutableList<Notice> DOMAIN_BLOCKED_BY_BSA_BOILERPLATE_NOTICES =
ImmutableList.of(DOMAIN_BLOCKED_BY_BSA_NOTICE);
/** Boilerplate remarks required by nameserver and entity responses. */
static final ImmutableList<Notice> NAMESERVER_AND_ENTITY_BOILERPLATE_NOTICES =
ImmutableList.of(CONFORMANCE_NOTICE);
/**
* Required by ICANN RDAP Profile section 1.4.9, as corrected by Gustavo Lozano of ICANN.
*
* <p>Also mentioned in the RDAP Technical Implementation Guide 3.6.
*
* @see <a href="http://mm.icann.org/pipermail/gtld-tech/2016-October/000822.html">Questions about
* the ICANN RDAP Profile</a>
*/
/** Required by the RDAP Technical Implementation Guide 3.6. */
static final Remark SUMMARY_DATA_REMARK =
Remark.builder()
.setTitle("Incomplete Data")
@@ -109,14 +89,7 @@ public class RdapIcannStandardInformation {
.setType(Remark.Type.OBJECT_TRUNCATED_UNEXPLAINABLE)
.build();
/**
* Required by ICANN RDAP Profile section 1.4.8, as corrected by Gustavo Lozano of ICANN.
*
* <p>Also mentioned in the RDAP Technical Implementation Guide 3.5.
*
* @see <a href="http://mm.icann.org/pipermail/gtld-tech/2016-October/000822.html">Questions about
* the ICANN RDAP Profile</a>
*/
/** Required by the RDAP Technical Implementation Guide 3.5. */
static final Notice TRUNCATED_RESULT_SET_NOTICE =
Notice.builder()
.setTitle("Search Policy")
@@ -148,7 +121,9 @@ public class RdapIcannStandardInformation {
/**
* Included when requester is not logged in as the owner of the contact being returned.
*
* <p>Format required by ICANN RDAP Response Profile 15feb19 section 2.7.4.3.
* <p>>Note: if we were keeping this around, we'd want/need to implement the <a
* href="https://datatracker.ietf.org/doc/rfc9537/">official RDAP redaction spec</a> for contacts.
* We are getting rid of contacts in 2025 though so this should be unnecessary.
*/
static final Remark CONTACT_PERSONAL_DATA_HIDDEN_DATA_REMARK =
Remark.builder()
@@ -169,10 +144,9 @@ public class RdapIcannStandardInformation {
/**
* Included in ALL contact responses, even if the user is authorized.
*
* <p>Format required by ICANN RDAP Response Profile 15feb19 section 2.7.5.3.
*
* <p>NOTE that unlike other redacted fields, there's no allowance to give the email to authorized
* users or allow for registrar consent.
* <p>>Note: if we were keeping this around, we'd want/need to implement the <a
* href="https://datatracker.ietf.org/doc/rfc9537/">official RDAP redaction spec</a> for contacts.
* We are getting rid of contacts in 2025 though so this should be unnecessary.
*/
static final Remark CONTACT_EMAIL_REDACTED_FOR_DOMAIN =
Remark.builder()
@@ -26,7 +26,7 @@ import google.registry.request.auth.Auth;
import jakarta.inject.Inject;
/**
* RDAP (new WHOIS) action for RDAP IP address requests.
* RDAP action for RDAP IP address requests.
*
* <p>This feature is not implemented because it's only necessary for <i>address</i> registries like
* ARIN, not domain registries.
@@ -23,20 +23,21 @@ import static google.registry.model.EppResourceUtils.isLinked;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.util.CollectionUtils.union;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.InetAddresses;
import com.google.gson.JsonArray;
import google.registry.config.RegistryConfig;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.CacheUtils;
import google.registry.model.EppResource;
import google.registry.model.adapters.EnumToAttributeAdapter.EppEnum;
import google.registry.model.contact.Contact;
@@ -73,13 +74,11 @@ import google.registry.rdap.RdapObjectClasses.VcardArray;
import google.registry.request.RequestServerName;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import jakarta.persistence.Entity;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.URI;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
@@ -103,6 +102,16 @@ public class RdapJsonFormatter {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@VisibleForTesting
record HistoryTimeAndRegistrar(DateTime modificationTime, String registrarId) {}
private static final LoadingCache<String, ImmutableMap<EventAction, HistoryTimeAndRegistrar>>
DOMAIN_HISTORIES_BY_REPO_ID =
CacheUtils.newCacheBuilder(RegistryConfig.getEppResourceCachingDuration())
// Cache more than the EPP resource cache because we're only caching small objects
.maximumSize(RegistryConfig.getEppResourceMaxCachedEntries() * 4L)
.build(repoId -> getLastHistoryByType(repoId, Domain.class));
private DateTime requestTime = null;
@Inject
@@ -212,7 +221,7 @@ public class RdapJsonFormatter {
* Map of EPP event values to the RDAP equivalents.
*
* <p>Only has entries for optional events, either stated as optional in the RDAP Response Profile
* 15feb19, or not mentioned at all but thought to be useful anyway.
* section 2.3.2, or not mentioned at all but thought to be useful anyway.
*
* <p>Any required event should be added elsewhere, preferably without using HistoryEntries (so
* that we don't need to load HistoryEntries for "summary" responses).
@@ -271,7 +280,7 @@ public class RdapJsonFormatter {
URI htmlUri = htmlBaseURI.resolve(rdapTosStaticUrl);
noticeBuilder.addLink(
Link.builder()
.setRel("alternate")
.setRel("terms-of-service")
.setHref(htmlUri.toString())
.setType("text/html")
.build());
@@ -283,8 +292,8 @@ public class RdapJsonFormatter {
* Creates a JSON object for a {@link Domain}.
*
* <p>NOTE that domain searches aren't in the spec yet - they're in the RFC 9082 that describes
* the query format, but they aren't in the RDAP Technical Implementation Guide 15feb19, meaning
* we don't have to implement them yet and the RDAP Response Profile doesn't apply to them.
* the query format, but they aren't in the RDAP Technical Implementation Guide, meaning we don't
* have to implement them yet and the RDAP Response Profile doesn't apply to them.
*
* <p>We're implementing domain searches anyway, BUT we won't have the response for searches
* conform to the RDAP Response Profile.
@@ -298,9 +307,9 @@ public class RdapJsonFormatter {
if (outputDataType != OutputDataType.FULL) {
builder.remarksBuilder().add(RdapIcannStandardInformation.SUMMARY_DATA_REMARK);
}
// RDAP Response Profile 15feb19 section 2.1 discusses the domain name.
// RDAP Response Profile section 2.1 discusses the domain name.
builder.setLdhName(domain.getDomainName());
// RDAP Response Profile 15feb19 section 2.2:
// RDAP Response Profile section 2.2:
// The domain handle MUST be the ROID
builder.setHandle(domain.getRepoId());
// If this is a summary (search result) - we'll return now. Since there's no requirement for
@@ -308,9 +317,9 @@ public class RdapJsonFormatter {
if (outputDataType == OutputDataType.SUMMARY) {
return builder.build();
}
// RDAP Response Profile 15feb19 section 2.3.1:
// RDAP Response Profile section 2.3.1:
// The domain object in the RDAP response MUST contain the following events:
// [registration, expiration, last update of RDAP database]
// [registration, expiration]
builder
.eventsBuilder()
.add(
@@ -324,14 +333,18 @@ public class RdapJsonFormatter {
.setEventAction(EventAction.EXPIRATION)
.setEventDate(domain.getRegistrationExpirationTime())
.build(),
// RDAP response profile section 1.5:
// The topmost object in the RDAP response MUST contain an event of "eventAction" type
// "last update of RDAP database" with a value equal to the timestamp when the RDAP
// database was last updated
Event.builder()
.setEventAction(EventAction.LAST_UPDATE_OF_RDAP_DATABASE)
.setEventDate(getRequestTime())
.build());
// RDAP Response Profile 15feb19 section 2.3.2 discusses optional events. We add some of those
// RDAP Response Profile section 2.3.2 discusses optional events. We add some of those
// here. We also add a few others we find interesting.
builder.eventsBuilder().addAll(makeOptionalEvents(domain));
// RDAP Response Profile 15feb19 section 2.4.1:
// RDAP Response Profile section 2.4.1:
// The domain object in the RDAP response MUST contain an entity with the Registrar role.
//
// See {@link createRdapRegistrarEntity} for details of section 2.4 conformance
@@ -369,8 +382,6 @@ public class RdapJsonFormatter {
// RDAP Response Profile 2.6.3, must have a notice about statuses. That is in {@link
// RdapIcannStandardInformation#domainBoilerplateNotices}
// Kick off the database loads of the nameservers that we will need, so it can load
// asynchronously while we load and process the contacts.
ImmutableSet<Host> loadedHosts =
replicaTm()
.transact(
@@ -415,12 +426,12 @@ public class RdapJsonFormatter {
}
// Add the nameservers to the data; the load was kicked off above for efficiency.
// RDAP Response Profile 2.9: we MUST have the nameservers
// RDAP Response Profile 2.8: we MUST have the nameservers
for (Host host : HOST_RESOURCE_ORDERING.immutableSortedCopy(loadedHosts)) {
builder.nameserversBuilder().add(createRdapNameserver(host, OutputDataType.INTERNAL));
}
// RDAP Response Profile 2.10 - MUST contain a secureDns member including at least a
// RDAP Response Profile 2.9 - MUST contain a secureDns member including at least a
// delegationSigned element. Other elements (e.g. dsData) MUST be included if the domain name is
// signed and the elements are stored in the Registry
//
@@ -445,13 +456,13 @@ public class RdapJsonFormatter {
builder.remarksBuilder().add(RdapIcannStandardInformation.SUMMARY_DATA_REMARK);
}
// We need the ldhName: RDAP Response Profile 2.9.1, 4.1
// We need the ldhName: RDAP Response Profile 2.8.1, 4.1
builder.setLdhName(host.getHostName());
// Handle is optional, but if given it MUST be the ROID.
// We will set it always as it's important as a "self link"
builder.setHandle(host.getRepoId());
// Status is optional for internal Nameservers - RDAP Response Profile 2.9.2
// Status is optional for internal Nameservers - RDAP Response Profile 2.8.2
// It isn't mentioned at all anywhere else. So we can just not put it at all?
//
// To be safe, we'll put it on the "FULL" version anyway
@@ -483,7 +494,7 @@ public class RdapJsonFormatter {
// For query responses - we MUST have all the ip addresses: RDAP Response Profile 4.2.
//
// However, it is optional for internal responses: RDAP Response Profile 2.9.2
// However, it is optional for internal responses: RDAP Response Profile 2.8.2
if (outputDataType != OutputDataType.INTERNAL) {
for (InetAddress inetAddress : host.getInetAddresses()) {
if (inetAddress instanceof Inet4Address) {
@@ -501,7 +512,7 @@ public class RdapJsonFormatter {
builder.entitiesBuilder().add(createRdapRegistrarEntity(registrar, OutputDataType.INTERNAL));
}
if (outputDataType != OutputDataType.INTERNAL) {
// Rdap Response Profile 4.4, must have "last update of RDAP database" response. But this is
// Rdap Response Profile 1.5, must have "last update of RDAP database" response. But this is
// only for direct query responses and not for internal objects.
builder.setLastUpdateOfRdapDatabaseEvent(
Event.builder()
@@ -526,10 +537,7 @@ public class RdapJsonFormatter {
Contact contact, Iterable<RdapEntity.Role> roles, OutputDataType outputDataType) {
RdapContactEntity.Builder contactBuilder = RdapContactEntity.builder();
// RDAP Response Profile 2.7.1, 2.7.3 - we MUST have the contacts. 2.7.4 discusses censoring of
// fields we don't want to show (as opposed to not having contacts at all) because of GDPR etc.
//
// 2.8 allows for unredacted output for authorized people.
// RDAP Response Profile 2.7.1, 2.7.3 - we MUST have the contacts
boolean isAuthorized =
rdapAuthorization.isAuthorizedForRegistrar(contact.getCurrentSponsorRegistrarId());
@@ -569,7 +577,7 @@ public class RdapJsonFormatter {
.add(RdapIcannStandardInformation.CONTACT_EMAIL_REDACTED_FOR_DOMAIN);
if (outputDataType != OutputDataType.INTERNAL) {
// Rdap Response Profile 2.7.6 must have "last update of RDAP database" response. But this is
// Rdap Response Profile 1.5 must have "last update of RDAP database" response. But this is
// only for direct query responses and not for internal objects. I'm not sure why it's in that
// section at all...
contactBuilder.setLastUpdateOfRdapDatabaseEvent(
@@ -647,8 +655,8 @@ public class RdapJsonFormatter {
* Creates a JSON object for a {@link Registrar}.
*
* <p>This object can be INTERNAL to the Domain and Nameserver responses, with requirements
* discussed in the RDAP Response Profile 15feb19 sections 2.4 (internal to Domain) and 4.3
* (internal to Namesever)
* discussed in the RDAP Response Profile sections 2.4 (internal to Domain) and 4.3 (internal to
* Namesever)
*
* @param registrar the registrar object from which the RDAP response
* @param outputDataType whether to generate FULL, SUMMARY, or INTERNAL data.
@@ -712,6 +720,15 @@ public class RdapJsonFormatter {
builder.linksBuilder().add(makeSelfLink("entity", ianaIdentifier.toString()));
}
// RDAP Response Profile 2.4.6: must have a links entry pointing to the registrar URL, with a
// rel:about and a value containing the registrar RDAP base URL (if present)
if (registrar.getUrl() != null) {
Link.Builder registrarLinkBuilder =
Link.builder().setHref(registrar.getUrl()).setRel("about").setType("text/html");
registrar.getRdapBaseUrls().stream().findFirst().ifPresent(registrarLinkBuilder::setValue);
builder.linksBuilder().add(registrarLinkBuilder.build());
}
// There's no mention of the registrar STATUS in the RDAP Response Profile, so we'll only add it
// for FULL response
// We could probably not add it at all, but it could be useful for us internally
@@ -737,10 +754,9 @@ public class RdapJsonFormatter {
//
// Write the minimum, meaning only ABUSE for INTERNAL registrars, nothing for SUMMARY and
// everything for FULL.
//
if (outputDataType != OutputDataType.SUMMARY) {
ImmutableList<RdapContactEntity> registrarContacts =
registrar.getContacts().stream()
registrar.getContactsFromReplica().stream()
.map(RdapJsonFormatter::makeRdapJsonForRegistrarContact)
.filter(Optional::isPresent)
.map(Optional::get)
@@ -758,7 +774,7 @@ public class RdapJsonFormatter {
builder.entitiesBuilder().addAll(registrarContacts);
}
// Rdap Response Profile 3.3, must have "last update of RDAP database" response. But this is
// Rdap Response Profile 1.5, must have "last update of RDAP database" response. But this is
// only for direct query responses and not for internal objects.
if (outputDataType != OutputDataType.INTERNAL) {
builder.setLastUpdateOfRdapDatabaseEvent(
@@ -860,8 +876,18 @@ public class RdapJsonFormatter {
}
@VisibleForTesting
ImmutableMap<EventAction, HistoryEntry> getLastHistoryEntryByType(EppResource resource) {
HashMap<EventAction, HistoryEntry> lastEntryOfType = Maps.newHashMap();
static ImmutableMap<EventAction, HistoryTimeAndRegistrar> getLastHistoryByType(
EppResource eppResource) {
if (eppResource instanceof Domain) {
return DOMAIN_HISTORIES_BY_REPO_ID.get(eppResource.getRepoId());
}
return getLastHistoryByType(eppResource.getRepoId(), eppResource.getClass());
}
private static ImmutableMap<EventAction, HistoryTimeAndRegistrar> getLastHistoryByType(
String repoId, Class<? extends EppResource> resourceType) {
ImmutableMap.Builder<EventAction, HistoryTimeAndRegistrar> lastEntryOfType =
new ImmutableMap.Builder<>();
// Events (such as transfer, but also create) can appear multiple times. We only want the last
// time they appeared.
//
@@ -873,49 +899,48 @@ public class RdapJsonFormatter {
// 2.3.2.3 An event of *eventAction* type *transfer*, with the last date and time that the
// domain was transferred. The event of *eventAction* type *transfer* MUST be omitted if the
// domain name has not been transferred since it was created.
VKey<? extends EppResource> resourceVkey = resource.createVKey();
Class<? extends HistoryEntry> historyClass =
HistoryEntryDao.getHistoryClassFromParent(resourceVkey.getKind());
String entityName = historyClass.getAnnotation(Entity.class).name();
if (Strings.isNullOrEmpty(entityName)) {
entityName = historyClass.getSimpleName();
}
String entityName = HistoryEntryDao.getHistoryClassFromParent(resourceType).getSimpleName();
String jpql =
GET_LAST_HISTORY_BY_TYPE_JPQL_TEMPLATE
.replace("%entityName%", entityName)
.replace("%repoIdValue%", resourceVkey.getKey().toString());
Iterable<HistoryEntry> historyEntries =
replicaTm()
.transact(
() ->
replicaTm()
.getEntityManager()
.createQuery(jpql, HistoryEntry.class)
.getResultList());
for (HistoryEntry historyEntry : historyEntries) {
EventAction rdapEventAction =
HISTORY_ENTRY_TYPE_TO_RDAP_EVENT_ACTION_MAP.get(historyEntry.getType());
// Only save the historyEntries if this is a type we care about.
if (rdapEventAction == null) {
continue;
}
lastEntryOfType.put(rdapEventAction, historyEntry);
}
return ImmutableMap.copyOf(lastEntryOfType);
.replace("%repoIdValue%", repoId);
replicaTm()
.transact(
() ->
replicaTm()
.getEntityManager()
.createQuery(jpql, HistoryEntry.class)
.getResultStream()
.forEach(
historyEntry -> {
EventAction rdapEventAction =
HISTORY_ENTRY_TYPE_TO_RDAP_EVENT_ACTION_MAP.get(
historyEntry.getType());
// Only save the entries if this is a type we care about.
if (rdapEventAction != null) {
lastEntryOfType.put(
rdapEventAction,
new HistoryTimeAndRegistrar(
historyEntry.getModificationTime(),
historyEntry.getRegistrarId()));
}
}));
return lastEntryOfType.buildKeepingLast();
}
/**
* Creates the list of optional events to list in domain, nameserver, or contact replies.
*
* <p>Only has entries for optional events that won't be shown in "SUMMARY" versions of these
* objects. These are either stated as optional in the RDAP Response Profile 15feb19, or not
* mentioned at all but thought to be useful anyway.
* objects. These are either stated as optional in the RDAP Response Profile, or not mentioned at
* all but thought to be useful anyway.
*
* <p>Any required event should be added elsewhere, preferably without using HistoryEntries (so
* that we don't need to load HistoryEntries for "summary" responses).
*/
private ImmutableList<Event> makeOptionalEvents(EppResource resource) {
ImmutableMap<EventAction, HistoryEntry> lastEntryOfType = getLastHistoryEntryByType(resource);
ImmutableMap<EventAction, HistoryTimeAndRegistrar> lastHistoryOfType =
getLastHistoryByType(resource);
ImmutableList.Builder<Event> eventsBuilder = new ImmutableList.Builder<>();
DateTime creationTime = resource.getCreationTime();
DateTime lastChangeTime =
@@ -923,12 +948,12 @@ public class RdapJsonFormatter {
// The order of the elements is stable - it's the order in which the enum elements are defined
// in EventAction
for (EventAction rdapEventAction : EventAction.values()) {
HistoryEntry historyEntry = lastEntryOfType.get(rdapEventAction);
HistoryTimeAndRegistrar historyTimeAndRegistrar = lastHistoryOfType.get(rdapEventAction);
// Check if there was any entry of this type
if (historyEntry == null) {
if (historyTimeAndRegistrar == null) {
continue;
}
DateTime modificationTime = historyEntry.getModificationTime();
DateTime modificationTime = historyTimeAndRegistrar.modificationTime();
// We will ignore all events that happened before the "creation time", since these events are
// from a "previous incarnation of the domain" (for a domain that was owned by someone,
// deleted, and then bought by someone else)
@@ -938,7 +963,7 @@ public class RdapJsonFormatter {
eventsBuilder.add(
Event.builder()
.setEventAction(rdapEventAction)
.setEventActor(historyEntry.getRegistrarId())
.setEventActor(historyTimeAndRegistrar.registrarId())
.setEventDate(modificationTime)
.build());
// The last change time might not be the lastEppUpdateTime, since some changes happen without
@@ -947,29 +972,24 @@ public class RdapJsonFormatter {
lastChangeTime = modificationTime;
}
}
// RDAP Response Profile 15feb19 section 2.3.2.2:
// RDAP Response Profile section 2.3.2.2:
// The event of eventAction type last changed MUST be omitted if the domain name has not been
// updated since it was created
if (lastChangeTime.isAfter(creationTime)) {
eventsBuilder.add(makeEvent(EventAction.LAST_CHANGED, null, lastChangeTime));
// Creates an RDAP event object as defined by RFC 9083
eventsBuilder.add(
Event.builder()
.setEventAction(EventAction.LAST_CHANGED)
.setEventDate(lastChangeTime)
.build());
}
return eventsBuilder.build();
}
/** Creates an RDAP event object as defined by RFC 9083. */
private static Event makeEvent(
EventAction eventAction, @Nullable String eventActor, DateTime eventDate) {
Event.Builder builder = Event.builder().setEventAction(eventAction).setEventDate(eventDate);
if (eventActor != null) {
builder.setEventActor(eventActor);
}
return builder.build();
}
/**
* Creates a vCard address entry: array of strings specifying the components of the address.
*
* <p>Rdap Response Profile 3.1.1: MUST contain the following fields: Street, City, Country Rdap
* <p>RDAP Response Profile 3.1.1: MUST contain the following fields: Street, City, Country Rdap
* Response Profile 3.1.2: optional fields: State/Province, Postal Code, Fax Number
*
* @see <a href="https://tools.ietf.org/html/rfc7095">RFC 7095: jCard: The JSON Format for
@@ -15,7 +15,7 @@
package google.registry.rdap;
import static google.registry.flows.host.HostFlowUtils.validateHostName;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
@@ -33,7 +33,7 @@ import google.registry.request.auth.Auth;
import jakarta.inject.Inject;
import java.util.Optional;
/** RDAP (new WHOIS) action for nameserver requests. */
/** RDAP action for nameserver requests. */
@Action(
service = GaeService.PUBAPI,
path = "/rdap/nameserver/",
@@ -48,7 +48,7 @@ public class RdapNameserverAction extends RdapActionBase {
@Override
public RdapNameserver getJsonObjectForResource(String pathSearchString, boolean isHeadRequest) {
// RDAP Technical Implementation Guide 2.2.1 - we must support A-label (Punycode) and U-label
// RDAP Technical Implementation Guide 2.1.1 - we must support A-label (Punycode) and U-label
// (Unicode) formats. canonicalizeName will transform Unicode to Punycode so we support both.
pathSearchString = canonicalizeName(pathSearchString);
// The RDAP syntax is /rdap/nameserver/ns1.mydomain.com.
@@ -63,7 +63,7 @@ public class RdapNameserverAction extends RdapActionBase {
// If there are no undeleted nameservers with the given name, the foreign key should point to
// the most recently deleted one.
Optional<Host> host =
loadByForeignKeyCached(
loadByForeignKeyByCache(
Host.class,
pathSearchString,
shouldIncludeDeleted() ? START_OF_TIME : getRequestTime());
@@ -14,7 +14,7 @@
package google.registry.rdap;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
@@ -47,9 +47,9 @@ import java.util.List;
import java.util.Optional;
/**
* RDAP (new WHOIS) action for nameserver search requests.
* RDAP action for nameserver search requests.
*
* <p>All commands and responses conform to the RDAP spec as defined in RFCs 7480 through 7485.
* <p>All commands and responses conform to the RDAP spec as defined in STD 95 and its RFCs.
*
* @see <a href="http://tools.ietf.org/html/rfc9082">RFC 9082: Registration Data Access Protocol
* (RDAP) Query Format</a>
@@ -159,7 +159,8 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
.setIncompletenessWarningType(IncompletenessWarningType.COMPLETE);
Optional<Host> host =
loadByForeignKeyCached(Host.class, partialStringQuery.getInitialString(), getRequestTime());
loadByForeignKeyByCache(
Host.class, partialStringQuery.getInitialString(), getRequestTime());
metricInformationBuilder.setNumHostsRetrieved(host.isPresent() ? 1 : 0);
@@ -175,7 +176,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
private NameserverSearchResponse searchByNameUsingSuperordinateDomain(
RdapSearchPattern partialStringQuery) {
Optional<Domain> domain =
loadByForeignKeyCached(Domain.class, partialStringQuery.getSuffix(), getRequestTime());
loadByForeignKeyByCache(Domain.class, partialStringQuery.getSuffix(), getRequestTime());
if (domain.isEmpty()) {
// Don't allow wildcards with suffixes which are not domains we manage. That would risk a
// table scan in many easily foreseeable cases. The user might ask for ns*.zombo.com,
@@ -193,7 +194,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
// We can't just check that the host name starts with the initial query string, because
// then the query ns.exam*.example.com would match against nameserver ns.example.com.
if (partialStringQuery.matches(fqhn)) {
Optional<Host> host = loadByForeignKeyCached(Host.class, fqhn, getRequestTime());
Optional<Host> host = loadByForeignKeyByCache(Host.class, fqhn, getRequestTime());
if (shouldBeVisible(host)) {
hostList.add(host.get());
if (hostList.size() > rdapResultSetMaxSize) {
@@ -45,11 +45,7 @@ import java.util.Optional;
/** Object Classes defined in RFC 9083 section 5. */
final class RdapObjectClasses {
/**
* Temporary implementation of VCards.
*
* <p>Will create a better implementation soon.
*/
/** Rough implementation of VCards. */
@RestrictJsonNames({})
@AutoValue
public abstract static class Vcard implements Jsonable {
@@ -140,8 +136,8 @@ final class RdapObjectClasses {
public enum BoilerplateType {
DOMAIN(RdapIcannStandardInformation.DOMAIN_BOILERPLATE_NOTICES),
DOMAIN_BLOCKED_BY_BSA(RdapIcannStandardInformation.DOMAIN_BLOCKED_BY_BSA_BOILERPLATE_NOTICES),
NAMESERVER(RdapIcannStandardInformation.NAMESERVER_AND_ENTITY_BOILERPLATE_NOTICES),
ENTITY(RdapIcannStandardInformation.NAMESERVER_AND_ENTITY_BOILERPLATE_NOTICES),
NAMESERVER(ImmutableList.of()),
ENTITY(ImmutableList.of()),
OTHER(ImmutableList.of());
@SuppressWarnings("ImmutableEnumChecker") // immutable lists are, in fact, immutable
@@ -173,8 +169,8 @@ final class RdapObjectClasses {
* The Top Level JSON reply, Adds the required top-level boilerplate to a ReplyPayloadBase.
*
* <p>RFC 9083 specifies that the top-level object should include an entry indicating the
* conformance level. ICANN RDAP spec for 15feb19 mandates several additional entries, in sections
* 2.6.3, 2.11 of the Response Profile and 3.3, 3.5, of the Technical Implementation Guide.
* conformance level. The RDAP spec mandates several additional entries, in sections 2.6.3, 2.10
* of the Response Profile and 3.3, 3.5, of the Technical Implementation Guide.
*/
@AutoValue
@RestrictJsonNames({})
@@ -353,7 +349,7 @@ final class RdapObjectClasses {
*
* <p>Takes care of the name and unicode field.
*
* <p>See RDAP Response Profile 15feb19 sections 2.1 and 4.1.
* <p>See RDAP Response Profile sections 2.1 and 4.1.
*
* <p>Note the ldhName field is only required for non-IDN names or IDN names when the query was an
* A-label. It is optional for IDN names when the query was a U-label. Because we don't want to
@@ -471,7 +467,7 @@ final class RdapObjectClasses {
}
/**
* an integer representing the signature lifetime in seconds to be used when creating the RRSIG
* An integer representing the signature lifetime in seconds to be used when creating the RRSIG
* DS record in the parent zone [RFC5910].
*
* <p>Note that although it isn't given as optional in RFC 9083, in RFC5910 it's mentioned as
@@ -31,7 +31,6 @@ import google.registry.request.HttpException.BadRequestException;
import google.registry.request.HttpException.UnprocessableEntityException;
import google.registry.request.Parameter;
import google.registry.request.ParameterMap;
import google.registry.request.RequestUrl;
import jakarta.inject.Inject;
import jakarta.persistence.criteria.CriteriaBuilder;
import java.io.UnsupportedEncodingException;
@@ -45,7 +44,7 @@ import java.util.Objects;
import java.util.Optional;
/**
* Base RDAP (new WHOIS) action for domain, nameserver and entity search requests.
* Base RDAP action for domain, nameserver and entity search requests.
*
* @see <a href="https://tools.ietf.org/html/rfc9082">RFC 9082: Registration Data Access Protocol
* (RDAP) Query Format</a>
@@ -54,7 +53,6 @@ public abstract class RdapSearchActionBase extends RdapActionBase {
private static final int RESULT_SET_SIZE_SCALING_FACTOR = 30;
@Inject @RequestUrl String requestUrl;
@Inject @ParameterMap ImmutableListMultimap<String, String> parameterMap;
@Inject @Parameter("cursor") Optional<String> cursorTokenParam;
@Inject @Parameter("registrar") Optional<String> registrarParam;
@@ -157,7 +155,6 @@ public abstract class RdapSearchActionBase extends RdapActionBase {
*/
<T extends EppResource> RdapResultSet<T> getMatchingResources(
CriteriaQueryBuilder<T> builder, boolean checkForVisibility, int querySizeLimit) {
replicaTm().assertInTransaction();
Optional<String> desiredRegistrar = getDesiredRegistrar();
if (desiredRegistrar.isPresent()) {
builder =
@@ -39,8 +39,8 @@ public final class RdapSearchPattern {
/**
* Pattern for allowed LDH searches.
*
* <p>Based on RFC 9082 4.1. Must contains only alphanumeric plus dots and hyphens. A single
* whildcard asterix is allowed - but if exists must be the last character of a domain name label
* <p>Based on RFC 9082 4.1. Must contain only alphanumeric plus dots and hyphens. A single
* wildcard asterix is allowed - but if exists must be the last character of a domain name label
* (so exam* and exam*.com are allowed, but exam*le.com isn't allowd)
*
* <p>The prefix is in group(1), and the suffix without the dot (if it exists) is in group(4). If
@@ -123,7 +123,7 @@ public final class RdapSearchPattern {
* Creates a SearchPattern using the provided domain search pattern in LDH format.
*
* <p>The domain search pattern can have a single wildcard asterix that can match 0 or more
* charecters. If such an asterix exists - it must be at the end of a domain label.
* characters. If such an asterix exists - it must be at the end of a domain label.
*
* @param searchQuery the string containing the partial match pattern
* @throws UnprocessableEntityException if {@code pattern} does not meet the requirements of RFC
@@ -150,7 +150,7 @@ public final class RdapSearchPattern {
* Creates a SearchPattern using the provided domain search pattern in LDH or Unicode format.
*
* <p>The domain search pattern can have a single wildcard asterix that can match 0 or more
* charecters. If such an asterix exists - it must be at the end of a domain label.
* characters. If such an asterix exists - it must be at the end of a domain label.
*
* <p>In theory, according to RFC 9082 4.1 - we should make some checks about partial matching in
* unicode queries. We don't, but we might want to just disable partial matches for unicode inputs
@@ -37,7 +37,7 @@ public final class RdapUtils {
*
* <p>Used for RDAP Technical Implementation Guide 2.4.2 - search of registrar by the fn element.
*
* <p>For convenience, we use case insensitive search.
* <p>For convenience, we use case-insensitive search.
*/
static Optional<Registrar> getRegistrarByName(String registrarName) {
return Streams.stream(Registrar.loadAllCached())
@@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.model.registrar.Registrar;
import google.registry.persistence.PersistenceModule;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.HttpException.InternalServerErrorException;
@@ -36,7 +37,7 @@ import jakarta.inject.Inject;
import java.io.IOException;
import java.io.StringReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URI;
import java.security.GeneralSecurityException;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
@@ -72,7 +73,7 @@ public final class UpdateRegistrarRdapBaseUrlsAction implements Runnable {
public void run() {
try {
ImmutableMap<String, String> ianaIdsToUrls = getIanaIdsToUrls();
tm().transact(() -> processAllRegistrars(ianaIdsToUrls));
processAllRegistrars(ianaIdsToUrls);
} catch (Exception e) {
throw new InternalServerErrorException("Error when retrieving RDAP base URL CSV file", e);
}
@@ -80,7 +81,14 @@ public final class UpdateRegistrarRdapBaseUrlsAction implements Runnable {
private static void processAllRegistrars(ImmutableMap<String, String> ianaIdsToUrls) {
int nonUpdatedRegistrars = 0;
for (Registrar registrar : Registrar.loadAll()) {
// Split into multiple transactions to avoid load-save-reload conflicts. Re-building a registrar
// requires a full (cached) load of all registrars to avoid IANA ID conflicts, so if multiple
// registrars are modified in the same transaction, the second build call will fail.
Iterable<Registrar> registrars =
tm().transact(
PersistenceModule.TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ,
Registrar::loadAll);
for (Registrar registrar : registrars) {
// Only update REAL registrars
if (registrar.getType() != Registrar.Type.REAL) {
continue;
@@ -100,7 +108,12 @@ public final class UpdateRegistrarRdapBaseUrlsAction implements Runnable {
"Updating RDAP base URLs for registrar %s from %s to %s",
registrar.getRegistrarId(), registrar.getRdapBaseUrls(), baseUrls);
}
tm().put(registrar.asBuilder().setRdapBaseUrls(baseUrls).build());
tm().transact(
() -> {
// Reload inside a transaction to avoid race conditions
Registrar reloadedRegistrar = tm().loadByEntity(registrar);
tm().put(reloadedRegistrar.asBuilder().setRdapBaseUrls(baseUrls).build());
});
}
}
logger.atInfo().log("No change in RDAP base URLs for %d registrars", nonUpdatedRegistrars);
@@ -108,9 +121,9 @@ public final class UpdateRegistrarRdapBaseUrlsAction implements Runnable {
private ImmutableMap<String, String> getIanaIdsToUrls()
throws IOException, GeneralSecurityException {
CSVParser csv;
HttpURLConnection connection = urlConnectionService.createConnection(new URL(RDAP_IDS_URL));
// Explictly set the accepted encoding, as we know Brotli causes us problems when talking to
HttpURLConnection connection =
urlConnectionService.createConnection(URI.create(RDAP_IDS_URL).toURL());
// Explicitly set the accepted encoding, as we know Brotli causes us problems when talking to
// ICANN.
connection.setRequestProperty(ACCEPT_ENCODING, "gzip");
String csvString;
@@ -128,11 +141,11 @@ public final class UpdateRegistrarRdapBaseUrlsAction implements Runnable {
} finally {
connection.disconnect();
}
csv =
CSVParser csv =
CSVFormat.Builder.create(CSVFormat.DEFAULT)
.setHeader()
.setSkipHeaderRecord(true)
.build()
.get()
.parse(new StringReader(csvString));
ImmutableMap.Builder<String, String> result = new ImmutableMap.Builder<>();
for (CSVRecord record : csv) {
@@ -72,11 +72,11 @@ final class CreateCdnsTld extends ConfirmingCommand {
.setDescription(description)
.setNameServerSet(
RegistryToolEnvironment.get() == RegistryToolEnvironment.PRODUCTION
? "cloud-dns-registry"
: "cloud-dns-registry-test")
? "cloud-dns-registry"
: "cloud-dns-registry-test")
.setDnsName(dnsName)
.setName((name != null) ? name : dnsName)
.setDnssecConfig(new ManagedZoneDnsSecConfig().setNonExistence("NSEC").setState("ON"));
.setDnssecConfig(new ManagedZoneDnsSecConfig().setNonExistence("nsec").setState("on"));
}
@Override
@@ -16,6 +16,7 @@ package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static org.joda.time.DateTimeZone.UTC;
@@ -23,6 +24,7 @@ import static org.joda.time.DateTimeZone.UTC;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.template.soy.data.SoyMapData;
import google.registry.model.common.FeatureFlag;
import google.registry.model.pricing.PremiumPricingEngine.DomainPrices;
import google.registry.tools.soy.DomainCreateSoyInfo;
import google.registry.util.StringGenerator;
@@ -58,9 +60,15 @@ final class CreateDomainCommand extends CreateOrUpdateDomainCommand {
@Override
protected void initMutatingEppToolCommand() {
checkArgumentNotNull(registrant, "Registrant must be specified");
checkArgument(!admins.isEmpty(), "At least one admin must be specified");
checkArgument(!techs.isEmpty(), "At least one tech must be specified");
tm().transact(
() -> {
if (!FeatureFlag.isActiveNowOrElse(
FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL, false)) {
checkArgumentNotNull(registrant, "Registrant must be specified");
checkArgument(!admins.isEmpty(), "At least one admin must be specified");
checkArgument(!techs.isEmpty(), "At least one tech must be specified");
}
});
if (isNullOrEmpty(password)) {
password = passwordGenerator.createString(PASSWORD_LENGTH);
}
@@ -16,7 +16,7 @@ package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_ACTIONS;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCacheIfEnabled;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
@@ -326,7 +326,7 @@ public final class DomainLockUtils {
private Domain getDomain(String domainName, String registrarId, DateTime now) {
Domain domain =
loadByForeignKeyCached(Domain.class, domainName, now)
loadByForeignKeyByCacheIfEnabled(Domain.class, domainName, now)
.orElseThrow(() -> new IllegalArgumentException("Domain doesn't exist"));
// The user must have specified either the correct registrar ID or the admin registrar ID
checkArgument(
@@ -23,9 +23,7 @@ import google.registry.config.CloudTasksUtilsModule;
import google.registry.config.CredentialModule.LocalCredentialJson;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.dns.writer.VoidDnsWriterModule;
import google.registry.dns.writer.clouddns.CloudDnsWriterModule;
import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule;
import google.registry.dns.writer.DnsWritersModule;
import google.registry.keyring.KeyringModule;
import google.registry.keyring.api.KeyModule;
import google.registry.model.ModelModule;
@@ -56,9 +54,8 @@ import javax.annotation.Nullable;
BatchModule.class,
BigqueryModule.class,
ConfigModule.class,
CloudDnsWriterModule.class,
CloudTasksUtilsModule.class,
DnsUpdateWriterModule.class,
DnsWritersModule.class,
GsonModule.class,
KeyModule.class,
KeyringModule.class,
@@ -71,7 +68,6 @@ import javax.annotation.Nullable;
SecretManagerModule.class,
UrlConnectionServiceModule.class,
UtilsModule.class,
VoidDnsWriterModule.class,
NonCachingWhoisModule.class
})
interface RegistryToolComponent {
@@ -251,11 +251,12 @@ final class UniformRapidSuspensionCommand extends MutatingEppToolCommand {
if (undo) {
return "";
}
StringBuilder undoBuilder = new StringBuilder("UNDO COMMAND:\n\n)")
.append("nomulus -e ")
.append(RegistryToolEnvironment.get())
.append(" uniform_rapid_suspension --undo --domain_name ")
.append(domainName);
StringBuilder undoBuilder =
new StringBuilder("UNDO COMMAND:\n\n")
.append("nomulus -e ")
.append(RegistryToolEnvironment.get())
.append(" uniform_rapid_suspension --undo --domain_name ")
.append(domainName);
if (!existingNameservers.isEmpty()) {
undoBuilder.append(" --hosts ").append(Joiner.on(',').join(existingNameservers));
}
@@ -55,7 +55,7 @@ public class ConsoleDomainGetAction extends ConsoleApiAction {
Optional<Domain> possibleDomain =
tm().transact(
() ->
EppResourceUtils.loadByForeignKeyCached(
EppResourceUtils.loadByForeignKeyByCacheIfEnabled(
Domain.class, paramDomain, tm().getTransactionTime()));
if (possibleDomain.isEmpty()) {
consoleApiParams.response().setStatus(SC_NOT_FOUND);
@@ -19,7 +19,6 @@ import static google.registry.request.RequestParameters.extractOptionalIntParame
import static google.registry.request.RequestParameters.extractOptionalParameter;
import static google.registry.request.RequestParameters.extractRequiredParameter;
import com.google.common.collect.ImmutableSet;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import dagger.Module;
@@ -192,10 +191,10 @@ public final class ConsoleModule {
}
@Provides
@Parameter("contacts")
public static Optional<ImmutableSet<RegistrarPoc>> provideContacts(
@Parameter("contact")
public static Optional<RegistrarPoc> provideContacts(
Gson gson, @OptionalJsonPayload Optional<JsonElement> payload) {
return payload.map(s -> ImmutableSet.copyOf(gson.fromJson(s, RegistrarPoc[].class)));
return payload.map(s -> gson.fromJson(s, RegistrarPoc.class));
}
@Provides
@@ -17,6 +17,7 @@ package google.registry.ui.server.console;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import static org.apache.http.HttpStatus.SC_OK;
@@ -37,6 +38,7 @@ import google.registry.util.RegistryEnvironment;
import jakarta.inject.Inject;
import java.util.Optional;
import java.util.stream.Collectors;
import org.joda.time.DateTime;
@Action(
service = GaeService.DEFAULT,
@@ -88,17 +90,35 @@ public class ConsoleUpdateRegistrarAction extends ConsoleApiAction {
}
}
Registrar updatedRegistrar =
DateTime now = tm().getTransactionTime();
DateTime newLastPocVerificationDate =
registrarParam.getLastPocVerificationDate() == null
? START_OF_TIME
: registrarParam.getLastPocVerificationDate();
checkArgument(
newLastPocVerificationDate.isBefore(now),
"Invalid value of LastPocVerificationDate - value is in the future");
var updatedRegistrarBuilder =
existingRegistrar
.get()
.asBuilder()
.setAllowedTlds(
registrarParam.getAllowedTlds().stream()
.map(DomainNameUtils::canonicalizeHostname)
.collect(Collectors.toSet()))
.setRegistryLockAllowed(registrarParam.isRegistryLockAllowed())
.build();
.setLastPocVerificationDate(newLastPocVerificationDate);
if (user.getUserRoles()
.hasGlobalPermission(ConsolePermission.EDIT_REGISTRAR_DETAILS)) {
updatedRegistrarBuilder =
updatedRegistrarBuilder
.setAllowedTlds(
registrarParam.getAllowedTlds().stream()
.map(DomainNameUtils::canonicalizeHostname)
.collect(Collectors.toSet()))
.setRegistryLockAllowed(registrarParam.isRegistryLockAllowed())
.setLastPocVerificationDate(newLastPocVerificationDate);
}
var updatedRegistrar = updatedRegistrarBuilder.build();
tm().put(updatedRegistrar);
finishAndPersistConsoleUpdateHistory(
new ConsoleUpdateHistory.Builder()
@@ -15,12 +15,13 @@
package google.registry.ui.server.console.settings;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Sets.difference;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.DELETE;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static google.registry.request.Action.Method.PUT;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.collect.HashMultimap;
@@ -33,66 +34,122 @@ import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPoc.Type;
import google.registry.persistence.transaction.QueryComposer.Comparator;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.Action.GkeService;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.ui.forms.FormException;
import google.registry.ui.server.RegistrarFormFields;
import google.registry.ui.server.console.ConsoleApiAction;
import google.registry.ui.server.console.ConsoleApiParams;
import jakarta.inject.Inject;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
@Action(
service = GaeService.DEFAULT,
gkeService = GkeService.CONSOLE,
path = ContactAction.PATH,
method = {GET, POST},
method = {GET, POST, DELETE, PUT},
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class ContactAction extends ConsoleApiAction {
static final String PATH = "/console-api/settings/contacts";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final Optional<ImmutableSet<RegistrarPoc>> contacts;
private final Optional<RegistrarPoc> contact;
private final String registrarId;
@Inject
public ContactAction(
ConsoleApiParams consoleApiParams,
@Parameter("registrarId") String registrarId,
@Parameter("contacts") Optional<ImmutableSet<RegistrarPoc>> contacts) {
@Parameter("contact") Optional<RegistrarPoc> contact) {
super(consoleApiParams);
this.registrarId = registrarId;
this.contacts = contacts;
this.contact = contact;
}
@Override
protected void getHandler(User user) {
checkPermission(user, registrarId, ConsolePermission.VIEW_REGISTRAR_DETAILS);
ImmutableList<RegistrarPoc> am =
tm().transact(
() ->
tm()
.createQueryComposer(RegistrarPoc.class)
.where("registrarId", Comparator.EQ, registrarId)
.stream()
.filter(r -> !r.getTypes().isEmpty())
.collect(toImmutableList()));
ImmutableList<RegistrarPoc> contacts =
tm().transact(() -> RegistrarPoc.loadForRegistrar(registrarId));
consoleApiParams.response().setStatus(SC_OK);
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(am));
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(contacts));
}
@Override
protected void deleteHandler(User user) {
updateContacts(
user,
(registrar, oldContacts) ->
oldContacts.stream()
.filter(
oldContact ->
!oldContact.getEmailAddress().equals(contact.get().getEmailAddress()))
.collect(toImmutableSet()));
}
@Override
protected void postHandler(User user) {
updateContacts(
user,
(registrar, oldContacts) -> {
RegistrarPoc newContact = contact.get();
return ImmutableSet.<RegistrarPoc>builder()
.addAll(oldContacts)
.add(
new RegistrarPoc()
.asBuilder()
.setTypes(newContact.getTypes())
.setVisibleInWhoisAsTech(newContact.getVisibleInWhoisAsTech())
.setVisibleInWhoisAsAdmin(newContact.getVisibleInWhoisAsAdmin())
.setVisibleInDomainWhoisAsAbuse(newContact.getVisibleInDomainWhoisAsAbuse())
.setFaxNumber(newContact.getFaxNumber())
.setName(newContact.getName())
.setEmailAddress(newContact.getEmailAddress())
.setPhoneNumber(newContact.getPhoneNumber())
.setRegistrar(registrar)
.build())
.build();
});
}
@Override
protected void putHandler(User user) {
updateContacts(
user,
(registrar, oldContacts) -> {
RegistrarPoc updatedContact = contact.get();
return oldContacts.stream()
.map(
oldContact ->
oldContact.getId().equals(updatedContact.getId())
? oldContact
.asBuilder()
.setTypes(updatedContact.getTypes())
.setVisibleInWhoisAsTech(updatedContact.getVisibleInWhoisAsTech())
.setVisibleInWhoisAsAdmin(updatedContact.getVisibleInWhoisAsAdmin())
.setVisibleInDomainWhoisAsAbuse(
updatedContact.getVisibleInDomainWhoisAsAbuse())
.setFaxNumber(updatedContact.getFaxNumber())
.setName(updatedContact.getName())
.setEmailAddress(updatedContact.getEmailAddress())
.setPhoneNumber(updatedContact.getPhoneNumber())
.build()
: oldContact)
.collect(toImmutableSet());
});
}
private void updateContacts(
User user,
BiFunction<Registrar, ImmutableSet<RegistrarPoc>, ImmutableSet<RegistrarPoc>>
contactsUpdater) {
checkPermission(user, registrarId, ConsolePermission.EDIT_REGISTRAR_DETAILS);
checkArgument(contacts.isPresent(), "Contacts parameter is not present");
checkArgument(contact.isPresent(), "Contact parameter is not present");
Registrar registrar =
Registrar.loadByRegistrarId(registrarId)
.orElseThrow(
@@ -101,20 +158,10 @@ public class ContactAction extends ConsoleApiAction {
String.format("Unknown registrar %s", registrarId)));
ImmutableSet<RegistrarPoc> oldContacts = registrar.getContacts();
ImmutableSet<RegistrarPoc> updatedContacts =
RegistrarFormFields.getRegistrarContactBuilders(
oldContacts,
Collections.singletonMap(
"contacts",
contacts.get().stream()
.map(RegistrarPoc::toJsonMap)
.collect(toImmutableList())))
.stream()
.map(builder -> builder.setRegistrar(registrar).build())
.collect(toImmutableSet());
ImmutableSet<RegistrarPoc> newContacts = contactsUpdater.apply(registrar, oldContacts);
try {
checkContactRequirements(oldContacts, updatedContacts);
checkContactRequirements(oldContacts, newContacts);
} catch (FormException e) {
logger.atWarning().withCause(e).log(
"Error processing contacts post request for registrar: %s", registrarId);
@@ -123,14 +170,13 @@ public class ContactAction extends ConsoleApiAction {
tm().transact(
() -> {
RegistrarPoc.updateContacts(registrar, updatedContacts);
RegistrarPoc.updateContacts(registrar, newContacts);
Registrar updatedRegistrar =
registrar.asBuilder().setContactsRequireSyncing(true).build();
tm().put(updatedRegistrar);
sendExternalUpdatesIfNecessary(
EmailInfo.create(registrar, updatedRegistrar, oldContacts, updatedContacts));
EmailInfo.create(registrar, updatedRegistrar, oldContacts, newContacts));
});
consoleApiParams.response().setStatus(SC_OK);
}
@@ -169,6 +215,8 @@ public class ContactAction extends ConsoleApiAction {
throw new ContactRequirementException(t);
}
}
enforcePrimaryContactRestrictions(oldContactsByType, newContactsByType);
ensurePhoneNumberNotRemovedForContactTypes(oldContactsByType, newContactsByType, Type.TECH);
Optional<RegistrarPoc> domainWhoisAbuseContact =
getDomainWhoisVisibleAbuseContact(updatedContacts);
@@ -187,6 +235,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
@@ -17,7 +17,7 @@ package google.registry.whois;
import static com.google.common.base.Preconditions.checkNotNull;
import static google.registry.flows.domain.DomainFlowUtils.isBlockedByBsa;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.model.tld.Tlds.findTldForName;
import static google.registry.model.tld.Tlds.getTlds;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
@@ -94,7 +94,7 @@ public class DomainLookupCommand implements WhoisCommand {
private Optional<WhoisResponse> getResponse(InternetDomainName domainName, DateTime now) {
Optional<Domain> domainResource =
cached
? loadByForeignKeyCached(Domain.class, domainName.toString(), now)
? loadByForeignKeyByCache(Domain.class, domainName.toString(), now)
: loadByForeignKey(Domain.class, domainName.toString(), now);
return domainResource.map(
domain -> new DomainWhoisResponse(domain, fullOutput, whoisRedactedEmailText, now));
@@ -81,7 +81,7 @@ final class DomainWhoisResponse extends WhoisResponseImpl {
domain.getCurrentSponsorRegistrarId());
Registrar registrar = registrarOptional.get();
Optional<RegistrarPoc> abuseContact =
registrar.getContacts().stream()
registrar.getContactsFromReplica().stream()
.filter(RegistrarPoc::getVisibleInDomainWhoisAsAbuse)
.findFirst();
return WhoisResponseResults.create(
@@ -154,7 +154,7 @@ final class DomainWhoisResponse extends WhoisResponseImpl {
// If we refer to a contact that doesn't exist, that's a bug. It means referential integrity
// has somehow been broken. We skip the rest of this contact, but log it to hopefully bring it
// someone's attention.
Contact contact1 = EppResource.loadCached(contact.get());
Contact contact1 = EppResource.loadByCache(contact.get());
if (contact1 == null) {
logger.atSevere().log(
"(BUG) Broken reference found from domain %s to contact %s.",
@@ -16,7 +16,7 @@ package google.registry.whois;
import static com.google.common.base.Preconditions.checkNotNull;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.model.tld.Tlds.findTldForName;
import static google.registry.model.tld.Tlds.getTlds;
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
@@ -57,7 +57,7 @@ public class NameserverLookupByHostCommand implements WhoisCommand {
private Optional<WhoisResponse> getResponse(InternetDomainName hostName, DateTime now) {
Optional<Host> host =
cached
? loadByForeignKeyCached(Host.class, hostName.toString(), now)
? loadByForeignKeyByCache(Host.class, hostName.toString(), now)
: loadByForeignKey(Host.class, hostName.toString(), now);
return host.map(h -> new NameserverWhoisResponse(h, now));
}
@@ -44,7 +44,7 @@ class RegistrarWhoisResponse extends WhoisResponseImpl {
@Override
public WhoisResponseResults getResponse(boolean preferUnicode, String disclaimer) {
Set<RegistrarPoc> contacts = registrar.getContacts();
Set<RegistrarPoc> contacts = registrar.getContactsFromReplica();
String plaintext =
new RegistrarEmitter()
.emitField("Registrar", registrar.getRegistrarName())
@@ -47,13 +47,14 @@
<class>google.registry.model.billing.BillingRecurrence</class>
<class>google.registry.model.common.Cursor</class>
<class>google.registry.model.common.DnsRefreshRequest</class>
<class>google.registry.model.common.FeatureFlag</class>
<class>google.registry.model.console.ConsoleUpdateHistory</class>
<class>google.registry.model.console.PasswordResetRequest</class>
<class>google.registry.model.console.User</class>
<class>google.registry.model.contact.ContactHistory</class>
<class>google.registry.model.contact.Contact</class>
<class>google.registry.model.domain.Domain</class>
<class>google.registry.model.domain.DomainHistory</class>
<class>google.registry.model.common.FeatureFlag</class>
<class>google.registry.model.domain.GracePeriod</class>
<class>google.registry.model.domain.GracePeriod$GracePeriodHistory</class>
<class>google.registry.model.domain.secdns.DomainDsData</class>
@@ -20,9 +20,9 @@
{@param domain: string}
{@param period: int}
{@param nameservers: list<string>}
{@param registrant: string}
{@param admins: list<string>}
{@param techs: list<string>}
{@param? registrant: string|null}
{@param? admins: list<string>|null}
{@param? techs: list<string>|null}
{@param password: string}
{@param? currency: string|null}
{@param? price: string|null}
@@ -45,13 +45,19 @@
{/for}
</domain:ns>
{/if}
<domain:registrant>{$registrant}</domain:registrant>
{for $admin in $admins}
<domain:contact type="admin">{$admin}</domain:contact>
{/for}
{for $tech in $techs}
<domain:contact type="tech">{$tech}</domain:contact>
{/for}
{if $registrant != null}
<domain:registrant>{$registrant}</domain:registrant>
{/if}
{if $admins != null}
{for $admin in $admins}
<domain:contact type="admin">{$admin}</domain:contact>
{/for}
{/if}
{if $techs != null}
{for $tech in $techs}
<domain:contact type="tech">{$tech}</domain:contact>
{/for}
{/if}
<domain:authInfo>
<domain:pw>{$password}</domain:pw>
</domain:authInfo>
@@ -85,7 +85,7 @@ class BillingEventTest {
assertThat(invoiceKey.startDate()).isEqualTo("2017-10-01");
assertThat(invoiceKey.endDate()).isEqualTo("2022-09-30");
assertThat(invoiceKey.productAccountKey()).isEqualTo("12345-CRRHELLO");
assertThat(invoiceKey.usageGroupingKey()).isEqualTo("myRegistrar");
assertThat(invoiceKey.usageGroupingKey()).isEqualTo("");
assertThat(invoiceKey.description()).isEqualTo("RENEW | TLD: test | TERM: 5-year");
assertThat(invoiceKey.unitPrice()).isEqualTo(20.5);
assertThat(invoiceKey.unitPriceCurrency()).isEqualTo("USD");
@@ -106,7 +106,7 @@ class BillingEventTest {
assertThat(invoiceKey.toCsv(3L))
.isEqualTo(
"2017-10-01,2022-09-30,12345-CRRHELLO,61.50,USD,10125,1,PURCHASE,"
+ "myRegistrar,3,RENEW | TLD: test | TERM: 5-year,20.50,USD,");
+ ",3,RENEW | TLD: test | TERM: 5-year,20.50,USD,");
}
@Test
@@ -116,7 +116,7 @@ class BillingEventTest {
assertThat(invoiceKey.toCsv(3L))
.isEqualTo(
"2017-10-01,,12345-CRRHELLO,61.50,USD,10125,1,PURCHASE,"
+ "myRegistrar,3,RENEW | TLD: test | TERM: 0-year,20.50,USD,");
+ ",3,RENEW | TLD: test | TERM: 0-year,20.50,USD,");
}
@Test
@@ -199,6 +199,36 @@ class InvoicingPipelineTest {
0,
"USD",
20.0,
""),
google.registry.beam.billing.BillingEvent.create(
15,
DateTime.parse("2017-10-02T00:00:00.0Z"),
DateTime.parse("2017-10-04T00:00:00.0Z"),
"theRegistrarCopy",
"234",
"",
"test",
"CREATE",
"mydomainfromanotherclient.test",
"REPO-ID",
5,
"JPY",
70.0,
""),
google.registry.beam.billing.BillingEvent.create(
16,
DateTime.parse("2017-10-04T00:00:00Z"),
DateTime.parse("2017-10-04T00:00:00Z"),
"theRegistrarCopy",
"234",
"",
"test",
"RENEW",
"mydomain2fromanotherclient.test",
"REPO-ID",
3,
"USD",
20.5,
""));
private static final ImmutableMap<String, ImmutableList<String>> EXPECTED_DETAILED_REPORT_MAP =
@@ -224,18 +254,26 @@ class InvoicingPipelineTest {
"invoice_details_2017-10_anotherRegistrar_test.csv",
ImmutableList.of(
"5,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00 UTC,anotherRegistrar,789,,"
+ "test,CREATE,mydomain5.test,REPO-ID,1,USD,0.00,SUNRISE ANCHOR_TENANT"));
+ "test,CREATE,mydomain5.test,REPO-ID,1,USD,0.00,SUNRISE ANCHOR_TENANT"),
"invoice_details_2017-10_theRegistrarCopy_test.csv",
ImmutableList.of(
"15,2017-10-02 00:00:00 UTC,2017-10-04 00:00:00"
+ " UTC,theRegistrarCopy,234,,test,CREATE,mydomainfromanotherclient.test,REPO-ID,5,JPY,70.00,",
"16,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00"
+ " UTC,theRegistrarCopy,234,,test,RENEW,mydomain2fromanotherclient.test,REPO-ID,3,USD,20.50,"));
private static final ImmutableList<String> EXPECTED_INVOICE_OUTPUT =
ImmutableList.of(
"2017-10-01,2020-09-30,234,41.00,USD,10125,1,PURCHASE,theRegistrar,2,"
"2017-10-01,2020-09-30,234,61.50,USD,10125,1,PURCHASE,,3,"
+ "RENEW | TLD: test | TERM: 3-year,20.50,USD,",
"2017-10-01,2022-09-30,234,70.00,JPY,10125,1,PURCHASE,theRegistrar,1,"
"2017-10-01,2022-09-30,234,70.00,JPY,10125,1,PURCHASE,,1,"
+ "CREATE | TLD: hello | TERM: 5-year,70.00,JPY,",
"2017-10-01,,234,20.00,USD,10125,1,PURCHASE,theRegistrar,1,"
"2017-10-01,,234,20.00,USD,10125,1,PURCHASE,,1,"
+ "SERVER_STATUS | TLD: test | TERM: 0-year,20.00,USD,",
"2017-10-01,2018-09-30,456,20.50,USD,10125,1,PURCHASE,bestdomains,1,"
+ "RENEW | TLD: test | TERM: 1-year,20.50,USD,116688");
"2017-10-01,2018-09-30,456,20.50,USD,10125,1,PURCHASE,,1,"
+ "RENEW | TLD: test | TERM: 1-year,20.50,USD,116688",
"2017-10-01,2022-09-30,234,70.00,JPY,10125,1,PURCHASE,,1,CREATE | TLD: test | TERM:"
+ " 5-year,70.00,JPY,");
private final InvoicingPipelineOptions options =
PipelineOptionsFactory.create().as(InvoicingPipelineOptions.class);
@@ -355,21 +393,21 @@ class InvoicingPipelineTest {
.isEqualTo(
"""
SELECT b, r FROM BillingEvent b
JOIN Registrar r ON b.clientId = r.registrarId
JOIN Domain d ON b.domainRepoId = d.repoId
JOIN Tld t ON t.tldStr = d.tld
LEFT JOIN BillingCancellation c ON b.id = c.billingEvent
LEFT JOIN BillingCancellation cr ON b.cancellationMatchingBillingEvent = cr.billingRecurrence
WHERE r.billingAccountMap IS NOT NULL
AND r.type = 'REAL'
AND t.invoicingEnabled IS TRUE
AND CAST(b.billingTime AS timestamp)
BETWEEN CAST('2017-10-01T00:00:00Z' AS timestamp)
AND CAST('2017-11-01T00:00:00Z' AS timestamp)
AND c.id IS NULL
AND cr.id IS NULL
""");
SELECT b, r FROM BillingEvent b
JOIN Registrar r ON b.clientId = r.registrarId
JOIN Domain d ON b.domainRepoId = d.repoId
JOIN Tld t ON t.tldStr = d.tld
LEFT JOIN BillingCancellation c ON b.id = c.billingEvent
LEFT JOIN BillingCancellation cr ON b.cancellationMatchingBillingEvent = cr.billingRecurrence
WHERE r.billingAccountMap IS NOT NULL
AND r.type = 'REAL'
AND t.invoicingEnabled IS TRUE
AND CAST(b.billingTime AS timestamp)
BETWEEN CAST('2017-10-01T00:00:00Z' AS timestamp)
AND CAST('2017-11-01T00:00:00Z' AS timestamp)
AND c.id IS NULL
AND cr.id IS NULL
""");
}
/** Returns the text contents of a file under the beamBucket/results directory. */
@@ -391,6 +429,13 @@ class InvoicingPipelineTest {
.setBillingAccountMap(ImmutableMap.of(JPY, "234", USD, "234"))
.build();
persistResource(registrar1);
Registrar registrar11 = persistNewRegistrar("theRegistrarCopy");
registrar11 =
registrar11
.asBuilder()
.setBillingAccountMap(ImmutableMap.of(JPY, "234", USD, "234"))
.build();
persistResource(registrar11);
Registrar registrar2 = persistNewRegistrar("bestdomains");
registrar2 =
registrar2
@@ -547,6 +592,21 @@ class InvoicingPipelineTest {
.setDomainHistory(domainHistoryRecurrence)
.build();
persistResource(cancellationRecurrence);
// Domains created for registrar with = key but != client id.
Domain domain14 = persistActiveDomain("mydomainfromanotherclient.test");
Domain domain15 = persistActiveDomain("mydomain2fromanotherclient.test");
persistBillingEvent(
15,
domain14,
registrar11,
Reason.CREATE,
5,
Money.ofMajor(JPY, 70),
DateTime.parse("2017-10-04T00:00:00.0Z"),
DateTime.parse("2017-10-02T00:00:00.0Z"));
persistBillingEvent(16, domain15, registrar11, Reason.RENEW, 3, Money.of(USD, 20.5));
}
private static DomainHistory persistDomainHistory(Domain domain, Registrar registrar) {
@@ -0,0 +1,36 @@
// Copyright 2025 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.common;
import static com.google.common.truth.Truth.assertThat;
import google.registry.util.RegistryEnvironment;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.junit.jupiter.api.Test;
public class RegistryPipelineWorkerInitializerTest {
@Test
void test() {
RegistryPipelineOptions options =
PipelineOptionsFactory.fromArgs(
"--registryEnvironment=ALPHA", "--isolationOverride=TRANSACTION_SERIALIZABLE")
.withValidation()
.as(RegistryPipelineOptions.class);
new RegistryPipelineWorkerInitializer().beforeProcessing(options);
assertThat(RegistryEnvironment.isOnJetty()).isTrue();
System.clearProperty("google.registry.jetty");
}
}
@@ -58,7 +58,7 @@ public class FlowModuleTest {
@Test
void givenNonMutatingFlow_thenReplicaTmIsUsed() throws EppException {
String eppInputXmlFilename = "domain_info.xml";
String eppInputXmlFilename = "domain_check.xml";
FlowModule flowModule =
new FlowModule.Builder().setEppInput(getEppInput(eppInputXmlFilename)).build();
JpaTransactionManager tm =
@@ -211,8 +211,8 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
doCheckTest(
create(true, "example1.tld", null),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Reserved; alloc. token required"));
create(false, "reserved.tld", "Alloc token invalid for domain"),
create(false, "specificuse.tld", "Alloc token invalid for domain"));
}
@Test
@@ -230,8 +230,8 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
doCheckTest(
create(false, "example1.tld", "Blocked by a GlobalBlock service"),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Reserved; alloc. token required"));
create(false, "reserved.tld", "Alloc token invalid for domain"),
create(false, "specificuse.tld", "Alloc token invalid for domain"));
}
@Test
@@ -257,17 +257,6 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
create(true, "example3.tld", null));
}
@Test
void testSuccess_oneExists_allocationTokenIsInvalid() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
persistActiveDomain("example1.tld");
doCheckTest(
create(false, "example1.tld", "In use"),
create(false, "example2.tld", "The allocation token is invalid"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Reserved; alloc. token required"));
}
@Test
void testSuccess_oneExists_allocationTokenIsValid() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
@@ -300,24 +289,6 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
runFlowAssertResponse(loadFile("domain_check_allocationtoken_fee_anchor_response.xml"));
}
@Test
void testSuccess_oneExists_allocationTokenIsRedeemed() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
Domain domain = persistActiveDomain("example1.tld");
HistoryEntryId historyEntryId = new HistoryEntryId(domain.getRepoId(), 1L);
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRedemptionHistoryId(historyEntryId)
.build());
doCheckTest(
create(false, "example1.tld", "In use"),
create(false, "example2.tld", "Alloc token was already redeemed"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Reserved; alloc. token required"));
}
@Test
void testSuccess_oneExists_allocationTokenForReservedDomain() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
@@ -329,9 +300,9 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
.setTokenType(SINGLE_USE)
.build());
doCheckTest(
create(false, "example1.tld", "In use"),
create(false, "example1.tld", "Alloc token invalid for domain"),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "reserved.tld", "Alloc token invalid for domain"),
create(true, "specificuse.tld", null));
}
@@ -350,23 +321,6 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
runFlowAssertResponse(loadFile("domain_check_allocationtoken_fee_specificuse_response.xml"));
}
@Test
void testSuccess_oneExists_allocationTokenForWrongDomain() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
persistActiveDomain("example1.tld");
persistResource(
new AllocationToken.Builder()
.setDomainName("someotherdomain.tld")
.setToken("abc123")
.setTokenType(SINGLE_USE)
.build());
doCheckTest(
create(false, "example1.tld", "In use"),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Reserved; alloc. token required"));
}
@Test
void testSuccess_notOutOfDateToken_forSpecificDomain() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
@@ -385,44 +339,10 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
doCheckTest(
create(false, "example1.tld", "Alloc token invalid for domain"),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "reserved.tld", "Alloc token invalid for domain"),
create(true, "specificuse.tld", null));
}
@Test
void testSuccess_outOfDateToken_forSpecificDomain() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("specificuse.tld")
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, TokenStatus.NOT_STARTED)
.put(clock.nowUtc().minusDays(2), TokenStatus.VALID)
.put(clock.nowUtc().minusDays(1), TokenStatus.ENDED)
.build())
.build());
doCheckTest(
create(false, "example1.tld", "Alloc token invalid for domain"),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Alloc token not in promo period"));
}
@Test
void testSuccess_nothingExists_reservationsOverrideInvalidAllocationTokens() throws Exception {
setEppInput("domain_check_reserved_allocationtoken.xml");
// Fill out these reasons
doCheckTest(
create(false, "collision.tld", "Cannot be delegated"),
create(false, "reserved.tld", "Reserved"),
create(false, "anchor.tld", "Reserved; alloc. token required"),
create(false, "allowedinsunrise.tld", "Reserved"),
create(false, "premiumcollision.tld", "Cannot be delegated"));
}
@Test
void testSuccess_allocationTokenPromotion_singleYear() throws Exception {
createTld("example");
@@ -500,7 +420,75 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
}
@Test
void testFailure_allocationTokenPromotion_PremiumsNotSet() throws Exception {
void testSuccess_allocationTokenInvalid_overridesOtherErrors() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
persistActiveDomain("example1.tld");
doCheckTest(
create(false, "example1.tld", "The allocation token is invalid"),
create(false, "example2.tld", "The allocation token is invalid"),
create(false, "reserved.tld", "The allocation token is invalid"),
create(false, "specificuse.tld", "The allocation token is invalid"));
}
@Test
void testSuccess_allocationTokenForWrongDomain_overridesOtherConcerns() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
persistActiveDomain("example1.tld");
persistResource(
new AllocationToken.Builder()
.setDomainName("someotherdomain.tld")
.setToken("abc123")
.setTokenType(SINGLE_USE)
.build());
doCheckTest(
create(false, "example1.tld", "Alloc token invalid for domain"),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Alloc token invalid for domain"),
create(false, "specificuse.tld", "Alloc token invalid for domain"));
}
@Test
void testSuccess_outOfDateToken_overridesOtherIssues() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("specificuse.tld")
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, TokenStatus.NOT_STARTED)
.put(clock.nowUtc().minusDays(2), TokenStatus.VALID)
.put(clock.nowUtc().minusDays(1), TokenStatus.ENDED)
.build())
.build());
doCheckTest(
create(false, "example1.tld", "Alloc token not in promo period"),
create(false, "example2.tld", "Alloc token not in promo period"),
create(false, "reserved.tld", "Alloc token not in promo period"),
create(false, "specificuse.tld", "Alloc token not in promo period"));
}
@Test
void testSuccess_redeemedTokenOverridesOtherConcerns() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
Domain domain = persistActiveDomain("example1.tld");
HistoryEntryId historyEntryId = new HistoryEntryId(domain.getRepoId(), 1L);
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRedemptionHistoryId(historyEntryId)
.build());
doCheckTest(
create(false, "example1.tld", "Alloc token was already redeemed"),
create(false, "example2.tld", "Alloc token was already redeemed"),
create(false, "reserved.tld", "Alloc token was already redeemed"),
create(false, "specificuse.tld", "Alloc token was already redeemed"));
}
@Test
void testSuccess_allocationTokenPromotion_noPremium_stillPasses() throws Exception {
createTld("example");
persistResource(
new AllocationToken.Builder()
@@ -515,7 +503,7 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
ImmutableMap.of("DOMAIN", "rich.example"));
doCheckTest(
create(true, "example1.example", null),
create(false, "rich.example", "Token not valid for premium name"),
create(true, "rich.example", null),
create(true, "example3.example", null));
}
@@ -572,7 +560,8 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
doCheckTest(
create(false, "example1.tld", "Alloc token not in promo period"),
create(false, "example2.example", "Alloc token not in promo period"),
create(false, "reserved.tld", "Reserved"));
create(false, "reserved.tld", "Alloc token not in promo period"),
create(false, "rich.example", "Alloc token not in promo period"));
}
@Test
@@ -593,9 +582,10 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
.build());
setEppInput("domain_check_allocationtoken_fee.xml");
doCheckTest(
create(false, "example1.tld", "Alloc token invalid for TLD"),
create(true, "example1.tld", null),
create(true, "example2.example", null),
create(false, "reserved.tld", "Reserved"));
create(false, "reserved.tld", "Reserved"),
create(true, "rich.example", null));
}
@Test
@@ -618,7 +608,8 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
doCheckTest(
create(false, "example1.tld", "Alloc token invalid for client"),
create(false, "example2.example", "Alloc token invalid for client"),
create(false, "reserved.tld", "Reserved"));
create(false, "reserved.tld", "Alloc token invalid for client"),
create(false, "rich.example", "Alloc token invalid for client"));
}
@Test
@@ -999,6 +990,7 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
.setToken("abc123")
.setTokenType(UNLIMITED_USE)
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE, CommandName.TRANSFER))
.setDiscountFraction(0.1)
.build());
setEppInput("domain_check_fee_multiple_commands_allocationtoken_v06.xml");
runFlowAssertResponse(
@@ -1028,6 +1020,7 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
.setToken("abc123")
.setTokenType(UNLIMITED_USE)
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE, CommandName.TRANSFER))
.setDiscountFraction(0.1)
.build());
setEppInput("domain_check_fee_multiple_commands_allocationtoken_v12.xml");
runFlowAssertResponse(
@@ -141,13 +141,10 @@ import google.registry.flows.domain.DomainFlowUtils.TrailingDashException;
import google.registry.flows.domain.DomainFlowUtils.UnexpectedClaimsNoticeException;
import google.registry.flows.domain.DomainFlowUtils.UnsupportedFeeAttributeException;
import google.registry.flows.domain.DomainFlowUtils.UnsupportedMarkTypeException;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.NonexistentAllocationTokenException;
import google.registry.flows.exceptions.OnlyToolCanPassMetadataException;
import google.registry.flows.exceptions.ResourceAlreadyExistsForThisClientException;
import google.registry.flows.exceptions.ResourceCreateContentionException;
@@ -529,51 +526,10 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
"domain_create_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
persistContactsAndHosts();
EppException thrown = assertThrows(InvalidAllocationTokenException.class, this::runFlow);
EppException thrown = assertThrows(NonexistentAllocationTokenException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_reservedDomainCreate_allocationTokenIsForADifferentDomain() {
// Try to register a reserved domain name with an allocation token valid for a different domain
// name.
setEppInput(
"domain_create_allocationtoken.xml", ImmutableMap.of("DOMAIN", "resdom.tld", "YEARS", "2"));
persistContactsAndHosts();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("otherdomain.tld")
.build());
clock.advanceOneMilli();
EppException thrown =
assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
assertAllocationTokenWasNotRedeemed("abc123");
}
@Test
void testFailure_nonreservedDomainCreate_allocationTokenIsForADifferentDomain() {
// Try to register a non-reserved domain name with an allocation token valid for a different
// domain name.
setEppInput(
"domain_create_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
persistContactsAndHosts();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("otherdomain.tld")
.build());
clock.advanceOneMilli();
EppException thrown =
assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
assertAllocationTokenWasNotRedeemed("abc123");
}
@Test
void testFailure_alreadyRedemeedAllocationToken() {
setEppInput(
@@ -1704,32 +1660,6 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, 204.44));
}
@Test
void testSuccess_promotionDoesNotApplyToPremiumPrice() {
// Discounts only apply to premium domains if the token is explicitly configured to allow it.
createTld("example");
persistContactsAndHosts();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(UNLIMITED_USE)
.setDiscountFraction(0.5)
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, TokenStatus.NOT_STARTED)
.put(clock.nowUtc().plusMillis(1), TokenStatus.VALID)
.put(clock.nowUtc().plusSeconds(1), TokenStatus.ENDED)
.build())
.build());
clock.advanceOneMilli();
setEppInput(
"domain_create_premium_allocationtoken.xml",
ImmutableMap.of("YEARS", "2", "FEE", "193.50"));
assertAboutEppExceptions()
.that(assertThrows(AllocationTokenInvalidForPremiumNameException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testSuccess_token_premiumDomainZeroPrice_noFeeExtension() throws Exception {
createTld("example");
@@ -1774,30 +1704,6 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.marshalsToXml();
}
@Test
void testSuccess_promoTokenNotValidForTld() {
persistContactsAndHosts();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(UNLIMITED_USE)
.setAllowedTlds(ImmutableSet.of("example"))
.setDiscountFraction(0.5)
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, TokenStatus.NOT_STARTED)
.put(clock.nowUtc().minusDays(1), TokenStatus.VALID)
.put(clock.nowUtc().plusDays(1), TokenStatus.ENDED)
.build())
.build());
setEppInput(
"domain_create_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
assertAboutEppExceptions()
.that(assertThrows(AllocationTokenNotValidForTldException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testSuccess_promoTokenNotValidForRegistrar() {
persistContactsAndHosts();
@@ -3671,22 +3577,6 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
assertThrows(MissingClaimsNoticeException.class, this::runFlow);
}
@Test
void testFailure_anchorTenant_mismatchedName_viaToken() throws Exception {
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRegistrationBehavior(RegistrationBehavior.ANCHOR_TENANT)
.setDomainName("example.tld")
.build());
persistContactsAndHosts();
setEppInput(
"domain_create_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example-one.tld", "YEARS", "2"));
assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow);
}
@Test
void testSuccess_bulkToken_addsTokenToDomain() throws Exception {
AllocationToken token =
@@ -179,7 +179,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
ImmutableMap<String, String> substitutions,
boolean expectHistoryAndBilling)
throws Exception {
assertMutatingFlow(false);
assertMutatingFlow(true);
String expected =
loadFile(expectedXmlFilename, updateSubstitutions(substitutions, "ROID", "2FF-TLD"));
if (inactive) {
@@ -28,7 +28,6 @@ import static google.registry.testing.DatabaseHelper.persistPremiumList;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.money.CurrencyUnit.JPY;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -39,8 +38,6 @@ import google.registry.flows.EppException;
import google.registry.flows.HttpSessionMetadata;
import google.registry.flows.SessionMetadata;
import google.registry.flows.custom.DomainPricingCustomLogic;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForCurrencyException;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
import google.registry.model.billing.BillingRecurrence;
@@ -214,32 +211,6 @@ public class DomainPricingLogicTest {
.build());
}
@Test
void
testGetDomainCreatePrice_withDiscountPriceToken_domainCurrencyDoesNotMatchTokensCurrency_throwsException() {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDiscountPrice(Money.of(JPY, new BigDecimal("250")))
.setDiscountPremiums(false)
.build());
// Domain's currency is not JPY (is USD).
assertThrows(
AllocationTokenInvalidForCurrencyException.class,
() ->
domainPricingLogic.getCreatePrice(
tld,
"default.example",
clock.nowUtc(),
3,
false,
false,
Optional.of(allocationToken)));
}
@Test
void testGetDomainRenewPrice_oneYear_standardDomain_noBilling_isStandardPrice()
throws EppException {
@@ -335,77 +306,6 @@ public class DomainPricingLogicTest {
.build());
}
@Test
void
testGetDomainRenewPrice_oneYear_premiumDomain_default_withTokenNotValidForPremiums_throwsException() {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDiscountFraction(0.5)
.setDiscountPremiums(false)
.build());
assertThrows(
AllocationTokenInvalidForPremiumNameException.class,
() ->
domainPricingLogic.getRenewPrice(
tld,
"premium.example",
clock.nowUtc(),
1,
persistDomainAndSetRecurrence("premium.example", DEFAULT, Optional.empty()),
Optional.of(allocationToken)));
}
@Test
void
testGetDomainRenewPrice_oneYear_premiumDomain_default_withDiscountPriceToken_throwsException() {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDiscountPrice(Money.of(USD, 5))
.setDiscountPremiums(false)
.build());
assertThrows(
AllocationTokenInvalidForPremiumNameException.class,
() ->
domainPricingLogic.getRenewPrice(
tld,
"premium.example",
clock.nowUtc(),
1,
persistDomainAndSetRecurrence("premium.example", DEFAULT, Optional.empty()),
Optional.of(allocationToken)));
}
@Test
void
testGetDomainRenewPrice_withDiscountPriceToken_domainCurrencyDoesNotMatchTokensCurrency_throwsException() {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDiscountPrice(Money.of(JPY, new BigDecimal("250")))
.setDiscountPremiums(false)
.build());
// Domain's currency is not JPY (is USD).
assertThrows(
AllocationTokenInvalidForCurrencyException.class,
() ->
domainPricingLogic.getRenewPrice(
tld,
"default.example",
clock.nowUtc(),
1,
persistDomainAndSetRecurrence("default.example", DEFAULT, Optional.empty()),
Optional.of(allocationToken)));
}
@Test
void testGetDomainRenewPrice_multiYear_premiumDomain_default_isPremiumCost() throws EppException {
assertThat(
@@ -450,30 +350,6 @@ public class DomainPricingLogicTest {
.build());
}
@Test
void
testGetDomainRenewPrice_multiYear_premiumDomain_default_withTokenNotValidForPremiums_throwsException() {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDiscountFraction(0.5)
.setDiscountPremiums(false)
.setDiscountYears(2)
.build());
assertThrows(
AllocationTokenInvalidForPremiumNameException.class,
() ->
domainPricingLogic.getRenewPrice(
tld,
"premium.example",
clock.nowUtc(),
5,
persistDomainAndSetRecurrence("premium.example", DEFAULT, Optional.empty()),
Optional.of(allocationToken)));
}
@Test
void testGetDomainRenewPrice_oneYear_standardDomain_default_isNonPremiumPrice()
throws EppException {
@@ -69,13 +69,12 @@ import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException
import google.registry.flows.domain.DomainFlowUtils.RegistrarMustBeActiveForThisOperationException;
import google.registry.flows.domain.DomainFlowUtils.UnsupportedFeeAttributeException;
import google.registry.flows.domain.DomainRenewFlow.IncorrectCurrentExpirationDateException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.MissingRemoveBulkPricingTokenOnBulkPricingDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.NonexistentAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.RemoveBulkPricingTokenOnNonBulkPricingDomainException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.model.billing.BillingBase.Flag;
@@ -459,10 +458,14 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
ImmutableMap<String, String> customFeeMap =
updateSubstitutions(
FEE_06_MAP,
"NAME", "costly-renew.tld",
"PERIOD", "1",
"EX_DATE", "2001-04-03T22:00:00.0Z",
"FEE", "111.00");
"NAME",
"costly-renew.tld",
"PERIOD",
"1",
"EX_DATE",
"2001-04-03T22:00:00.0Z",
"FEE",
"111.00");
setEppInput("domain_renew_fee.xml", customFeeMap);
persistDomain();
doSuccessfulTest(
@@ -694,7 +697,7 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
"domain_renew_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "abc123"));
persistDomain();
EppException thrown = assertThrows(InvalidAllocationTokenException.class, this::runFlow);
EppException thrown = assertThrows(NonexistentAllocationTokenException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@@ -711,9 +714,12 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
.setDomainName("otherdomain.tld")
.build());
clock.advanceOneMilli();
EppException thrown =
assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
assertAboutEppExceptions()
.that(
assertThrows(
AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException.class,
this::runFlow))
.marshalsToXml();
assertAllocationTokenWasNotRedeemed("abc123");
}
@@ -784,9 +790,10 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
.put(clock.nowUtc().plusDays(1), TokenStatus.ENDED)
.build())
.build());
assertAboutEppExceptions()
.that(assertThrows(AllocationTokenNotValidForTldException.class, this::runFlow))
.marshalsToXml();
runFlowAssertResponse(
loadFile(
"domain_renew_response.xml",
ImmutableMap.of("DOMAIN", "example.tld", "EXDATE", "2002-04-03T22:00:00.0Z")));
assertAllocationTokenWasNotRedeemed("abc123");
}
@@ -52,12 +52,11 @@ import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.NonexistentAllocationTokenException;
import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.model.billing.BillingBase;
import google.registry.model.billing.BillingBase.Reason;
@@ -897,7 +896,7 @@ class DomainTransferApproveFlowTest
@Test
void testFailure_invalidAllocationToken() throws Exception {
setEppInput("domain_transfer_approve_allocation_token.xml");
EppException thrown = assertThrows(InvalidAllocationTokenException.class, this::runFlow);
EppException thrown = assertThrows(NonexistentAllocationTokenException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@@ -910,9 +909,12 @@ class DomainTransferApproveFlowTest
.setDomainName("otherdomain.tld")
.build());
setEppInput("domain_transfer_approve_allocation_token.xml");
EppException thrown =
assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
assertAboutEppExceptions()
.that(
assertThrows(
AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException.class,
this::runFlow))
.marshalsToXml();
}
@Test
@@ -971,8 +973,7 @@ class DomainTransferApproveFlowTest
.build())
.build());
setEppInput("domain_transfer_approve_allocation_token.xml");
EppException thrown = assertThrows(AllocationTokenNotValidForTldException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
runFlowAssertResponse(loadFile("domain_transfer_approve_response.xml"));
}
@Test
@@ -79,11 +79,9 @@ import google.registry.flows.domain.DomainFlowUtils.PremiumNameBlockedException;
import google.registry.flows.domain.DomainFlowUtils.RegistrarMustBeActiveForThisOperationException;
import google.registry.flows.domain.DomainFlowUtils.UnsupportedFeeAttributeException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.NonexistentAllocationTokenException;
import google.registry.flows.exceptions.AlreadyPendingTransferException;
import google.registry.flows.exceptions.InvalidTransferPeriodValueException;
import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException;
@@ -505,7 +503,7 @@ class DomainTransferRequestFlowTest
implicitTransferTime,
transferCost,
originalGracePeriods,
/* expectTransferBillingEvent = */ true,
/* expectTransferBillingEvent= */ true,
extraExpectedBillingEvents);
assertPollMessagesEmitted(expectedExpirationTime, implicitTransferTime);
@@ -1859,22 +1857,7 @@ class DomainTransferRequestFlowTest
void testFailure_invalidAllocationToken() throws Exception {
setupDomain("example", "tld");
setEppInput("domain_transfer_request_allocation_token.xml", ImmutableMap.of("TOKEN", "abc123"));
EppException thrown = assertThrows(InvalidAllocationTokenException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_allocationTokenIsForDifferentName() throws Exception {
setupDomain("example", "tld");
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("otherdomain.tld")
.build());
setEppInput("domain_transfer_request_allocation_token.xml", ImmutableMap.of("TOKEN", "abc123"));
EppException thrown =
assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow);
EppException thrown = assertThrows(NonexistentAllocationTokenException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@@ -1920,27 +1903,6 @@ class DomainTransferRequestFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_allocationTokenNotValidForTld() throws Exception {
setupDomain("example", "tld");
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(UNLIMITED_USE)
.setAllowedTlds(ImmutableSet.of("example"))
.setDiscountFraction(0.5)
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, TokenStatus.NOT_STARTED)
.put(clock.nowUtc().minusDays(1), TokenStatus.VALID)
.put(clock.nowUtc().plusDays(1), TokenStatus.ENDED)
.build())
.build());
setEppInput("domain_transfer_request_allocation_token.xml", ImmutableMap.of("TOKEN", "abc123"));
EppException thrown = assertThrows(AllocationTokenNotValidForTldException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_allocationTokenAlreadyRedeemed() throws Exception {
setupDomain("example", "tld");
@@ -19,31 +19,25 @@ import static google.registry.model.domain.token.AllocationToken.TokenStatus.CAN
import static google.registry.model.domain.token.AllocationToken.TokenStatus.ENDED;
import static google.registry.model.domain.token.AllocationToken.TokenStatus.NOT_STARTED;
import static google.registry.model.domain.token.AllocationToken.TokenStatus.VALID;
import static google.registry.model.domain.token.AllocationToken.TokenType.DEFAULT_PROMO;
import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE;
import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.time.DateTimeZone.UTC;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForCommandException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.NonexistentAllocationTokenException;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
@@ -52,7 +46,7 @@ import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.model.tld.Tld;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeClock;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
@@ -62,319 +56,322 @@ import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link AllocationTokenFlowUtils}. */
class AllocationTokenFlowUtilsTest {
private final AllocationTokenFlowUtils flowUtils = new AllocationTokenFlowUtils();
private final FakeClock clock = new FakeClock(DateTime.parse("2025-01-10T01:00:00.000Z"));
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension();
private final AllocationTokenExtension allocationTokenExtension =
mock(AllocationTokenExtension.class);
private Tld tld;
@BeforeEach
void beforeEach() {
createTld("tld");
tld = createTld("tld");
}
@Test
void test_validateToken_successfullyVerifiesValidTokenOnCreate() throws Exception {
void testSuccess_redeemsToken() {
HistoryEntryId historyEntryId = new HistoryEntryId("repoId", 10L);
assertThat(
AllocationTokenFlowUtils.redeemToken(singleUseTokenBuilder().build(), historyEntryId)
.getRedemptionHistoryId())
.hasValue(historyEntryId);
}
@Test
void testInvalidForPremiumName_validForPremium() {
AllocationToken token = singleUseTokenBuilder().setDiscountPremiums(true).build();
assertThat(AllocationTokenFlowUtils.discountTokenInvalidForPremiumName(token, true)).isFalse();
}
@Test
void testInvalidForPremiumName_notPremium() {
assertThat(
AllocationTokenFlowUtils.discountTokenInvalidForPremiumName(
singleUseTokenBuilder().build(), false))
.isFalse();
}
@Test
void testInvalidForPremiumName_invalidForPremium() {
assertThat(
AllocationTokenFlowUtils.discountTokenInvalidForPremiumName(
singleUseTokenBuilder().build(), true))
.isTrue();
}
@Test
void testSuccess_loadFromExtension() throws Exception {
AllocationToken token =
persistResource(
new AllocationToken.Builder()
.setToken("tokeN")
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE, CommandName.RESTORE))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setTokenType(SINGLE_USE)
.build());
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
assertThat(
flowUtils
.verifyAllocationTokenCreateIfPresent(
createCommand("blah.tld"),
Tld.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
Optional.of(allocationTokenExtension))
.get())
.isEqualTo(token);
AllocationTokenFlowUtils.loadAllocationTokenFromExtension(
"TheRegistrar",
"example.tld",
clock.nowUtc(),
Optional.of(allocationTokenExtension)))
.hasValue(token);
}
@Test
void test_validateToken_successfullyVerifiesValidTokenExistingDomain() throws Exception {
void testSuccess_loadOrDefault_fromExtensionEvenWhenDefaultPresent() throws Exception {
persistDefaultToken();
AllocationToken token =
persistResource(
new AllocationToken.Builder()
.setToken("tokeN")
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE, CommandName.RENEW))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setTokenType(SINGLE_USE)
.build());
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
assertThat(
flowUtils
.verifyAllocationTokenIfPresent(
DatabaseHelper.newDomain("blah.tld"),
Tld.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
CommandName.RENEW,
Optional.of(allocationTokenExtension))
.get())
.isEqualTo(token);
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
"TheRegistrar",
clock.nowUtc(),
Optional.of(allocationTokenExtension),
tld,
"example.tld",
CommandName.CREATE))
.hasValue(token);
}
void test_validateToken_emptyAllowedEppActions_successfullyVerifiesValidTokenExistingDomain()
throws Exception {
AllocationToken token =
persistResource(
new AllocationToken.Builder().setToken("tokeN").setTokenType(SINGLE_USE).build());
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
@Test
void testSuccess_loadOrDefault_defaultWhenNonePresent() throws Exception {
AllocationToken defaultToken = persistDefaultToken();
assertThat(
flowUtils
.verifyAllocationTokenIfPresent(
DatabaseHelper.newDomain("blah.tld"),
Tld.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
CommandName.RENEW,
Optional.of(allocationTokenExtension))
.get())
.isEqualTo(token);
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
"TheRegistrar",
clock.nowUtc(),
Optional.empty(),
tld,
"example.tld",
CommandName.CREATE))
.hasValue(defaultToken);
}
@Test
void test_validateTokenCreate_failsOnNonexistentToken() {
assertValidateCreateThrowsEppException(InvalidAllocationTokenException.class);
}
@Test
void test_validateTokenExistingDomain_failsOnNonexistentToken() {
assertValidateExistingDomainThrowsEppException(InvalidAllocationTokenException.class);
}
@Test
void test_validateTokenCreate_failsOnNullToken() {
assertAboutEppExceptions()
.that(
assertThrows(
InvalidAllocationTokenException.class,
() ->
flowUtils.verifyAllocationTokenCreateIfPresent(
createCommand("blah.tld"),
Tld.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
Optional.of(allocationTokenExtension))))
.marshalsToXml();
}
@Test
void test_validateTokenExistingDomain_failsOnNullToken() {
assertAboutEppExceptions()
.that(
assertThrows(
InvalidAllocationTokenException.class,
() ->
flowUtils.verifyAllocationTokenIfPresent(
DatabaseHelper.newDomain("blah.tld"),
Tld.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
CommandName.RENEW,
Optional.of(allocationTokenExtension))))
.marshalsToXml();
}
@Test
void test_validateTokenCreate_invalidForClientId() {
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setAllowedRegistrarIds(ImmutableSet.of("NewRegistrar"))
.build());
assertValidateCreateThrowsEppException(AllocationTokenNotValidForRegistrarException.class);
}
@Test
void test_validateTokenExistingDomain_invalidForClientId() {
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setAllowedRegistrarIds(ImmutableSet.of("NewRegistrar"))
.build());
assertValidateExistingDomainThrowsEppException(
AllocationTokenNotValidForRegistrarException.class);
}
@Test
void test_validateTokenCreate_invalidForTld() {
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setAllowedTlds(ImmutableSet.of("nottld"))
.build());
assertValidateCreateThrowsEppException(AllocationTokenNotValidForTldException.class);
}
@Test
void test_validateTokenExistingDomain_invalidForTld() {
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setAllowedTlds(ImmutableSet.of("nottld"))
.build());
assertValidateExistingDomainThrowsEppException(AllocationTokenNotValidForTldException.class);
}
@Test
void test_validateTokenCreate_beforePromoStart() {
persistResource(createOneMonthPromoTokenBuilder(DateTime.now(UTC).plusDays(1)).build());
assertValidateCreateThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
void test_validateTokenExistingDomain_beforePromoStart() {
persistResource(createOneMonthPromoTokenBuilder(DateTime.now(UTC).plusDays(1)).build());
assertValidateExistingDomainThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
void test_validateTokenCreate_afterPromoEnd() {
persistResource(createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusMonths(2)).build());
assertValidateCreateThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
void test_validateTokenExistingDomain_afterPromoEnd() {
persistResource(createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusMonths(2)).build());
assertValidateExistingDomainThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
void test_validateTokenCreate_promoCancelled() {
// the promo would be valid, but it was cancelled 12 hours ago
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, NOT_STARTED)
.put(DateTime.now(UTC).minusMonths(1), VALID)
.put(DateTime.now(UTC).minusHours(12), CANCELLED)
.build())
.build());
assertValidateCreateThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
void test_validateTokenExistingDomain_promoCancelled() {
// the promo would be valid, but it was cancelled 12 hours ago
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, NOT_STARTED)
.put(DateTime.now(UTC).minusMonths(1), VALID)
.put(DateTime.now(UTC).minusHours(12), CANCELLED)
.build())
.build());
assertValidateExistingDomainThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
void test_validateTokenCreate_invalidCommand() {
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setAllowedEppActions(ImmutableSet.of(CommandName.RENEW))
.build());
assertValidateCreateThrowsEppException(AllocationTokenNotValidForCommandException.class);
}
@Test
void test_validateTokenExistingDomain_invalidCommand() {
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.build());
assertValidateExistingDomainThrowsEppException(
AllocationTokenNotValidForCommandException.class);
}
@Test
void test_checkDomainsWithToken_successfullyVerifiesValidToken() {
persistResource(
new AllocationToken.Builder().setToken("tokeN").setTokenType(SINGLE_USE).build());
assertThat(
flowUtils
.checkDomainsWithToken(
ImmutableList.of(
InternetDomainName.from("blah.tld"), InternetDomainName.from("blah2.tld")),
"tokeN",
"TheRegistrar",
DateTime.now(UTC))
.domainCheckResults())
.containsExactlyEntriesIn(
ImmutableMap.of(
InternetDomainName.from("blah.tld"), "", InternetDomainName.from("blah2.tld"), ""))
.inOrder();
}
@Test
void test_checkDomainsWithToken_showsFailureMessageForRedeemedToken() {
Domain domain = persistActiveDomain("example.tld");
HistoryEntryId historyEntryId = new HistoryEntryId(domain.getRepoId(), 1051L);
void testSuccess_loadOrDefault_defaultWhenTokenIsPresentButNotApplicable() throws Exception {
AllocationToken defaultToken = persistDefaultToken();
persistResource(
new AllocationToken.Builder()
.setToken("tokeN")
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setTokenType(SINGLE_USE)
.setRedemptionHistoryId(historyEntryId)
.setAllowedTlds(ImmutableSet.of("othertld"))
.build());
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
assertThat(
flowUtils
.checkDomainsWithToken(
ImmutableList.of(
InternetDomainName.from("blah.tld"), InternetDomainName.from("blah2.tld")),
"tokeN",
"TheRegistrar",
DateTime.now(UTC))
.domainCheckResults())
.containsExactlyEntriesIn(
ImmutableMap.of(
InternetDomainName.from("blah.tld"),
"Alloc token was already redeemed",
InternetDomainName.from("blah2.tld"),
"Alloc token was already redeemed"))
.inOrder();
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
"TheRegistrar",
clock.nowUtc(),
Optional.of(allocationTokenExtension),
tld,
"example.tld",
CommandName.CREATE))
.hasValue(defaultToken);
}
private void assertValidateCreateThrowsEppException(Class<? extends EppException> clazz) {
@Test
void testValidAgainstDomain_validAllReasons() {
AllocationToken token = singleUseTokenBuilder().setDiscountPremiums(true).build();
assertThat(
AllocationTokenFlowUtils.tokenIsValidAgainstDomain(
InternetDomainName.from("rich.tld"), token, CommandName.CREATE, clock.nowUtc()))
.isTrue();
}
@Test
void testValidAgainstDomain_invalidPremium() {
AllocationToken token = singleUseTokenBuilder().build();
assertThat(
AllocationTokenFlowUtils.tokenIsValidAgainstDomain(
InternetDomainName.from("rich.tld"), token, CommandName.CREATE, clock.nowUtc()))
.isFalse();
}
@Test
void testValidAgainstDomain_invalidAction() {
AllocationToken token =
singleUseTokenBuilder().setAllowedEppActions(ImmutableSet.of(CommandName.RESTORE)).build();
assertThat(
AllocationTokenFlowUtils.tokenIsValidAgainstDomain(
InternetDomainName.from("domain.tld"), token, CommandName.CREATE, clock.nowUtc()))
.isFalse();
}
@Test
void testValidAgainstDomain_invalidTld() {
createTld("othertld");
AllocationToken token = singleUseTokenBuilder().build();
assertThat(
AllocationTokenFlowUtils.tokenIsValidAgainstDomain(
InternetDomainName.from("domain.othertld"),
token,
CommandName.CREATE,
clock.nowUtc()))
.isFalse();
}
@Test
void testValidAgainstDomain_invalidDomain() {
AllocationToken token = singleUseTokenBuilder().setDomainName("anchor.tld").build();
assertThat(
AllocationTokenFlowUtils.tokenIsValidAgainstDomain(
InternetDomainName.from("domain.tld"), token, CommandName.CREATE, clock.nowUtc()))
.isFalse();
}
@Test
void testFailure_redeemToken_nonSingleUse() {
assertThrows(
IllegalArgumentException.class,
() ->
AllocationTokenFlowUtils.redeemToken(
createOneMonthPromoTokenBuilder(clock.nowUtc()).build(),
new HistoryEntryId("repoId", 10L)));
}
@Test
void testFailure_loadFromExtension_nonexistentToken() {
assertLoadTokenFromExtensionThrowsException(NonexistentAllocationTokenException.class);
}
@Test
void testFailure_loadFromExtension_nullToken() {
when(allocationTokenExtension.getAllocationToken()).thenReturn(null);
assertLoadTokenFromExtensionThrowsException(NonexistentAllocationTokenException.class);
}
@Test
void testFailure_tokenInvalidForRegistrar() {
persistResource(
createOneMonthPromoTokenBuilder(clock.nowUtc().minusDays(1))
.setAllowedRegistrarIds(ImmutableSet.of("NewRegistrar"))
.build());
assertLoadTokenFromExtensionThrowsException(AllocationTokenNotValidForRegistrarException.class);
}
@Test
void testFailure_beforePromoStart() {
persistResource(createOneMonthPromoTokenBuilder(clock.nowUtc().plusDays(1)).build());
assertLoadTokenFromExtensionThrowsException(AllocationTokenNotInPromotionException.class);
}
@Test
void testFailure_afterPromoEnd() {
persistResource(createOneMonthPromoTokenBuilder(clock.nowUtc().minusMonths(2)).build());
assertLoadTokenFromExtensionThrowsException(AllocationTokenNotInPromotionException.class);
}
@Test
void testFailure_promoCancelled() {
// the promo would be valid, but it was cancelled 12 hours ago
persistResource(
createOneMonthPromoTokenBuilder(clock.nowUtc().minusDays(1))
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, NOT_STARTED)
.put(clock.nowUtc().minusMonths(1), VALID)
.put(clock.nowUtc().minusHours(12), CANCELLED)
.build())
.build());
assertLoadTokenFromExtensionThrowsException(AllocationTokenNotInPromotionException.class);
}
@Test
void testFailure_loadOrDefault_badTokenProvided() throws Exception {
when(allocationTokenExtension.getAllocationToken()).thenReturn("asdf");
assertThrows(
NonexistentAllocationTokenException.class,
() ->
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
"TheRegistrar",
clock.nowUtc(),
Optional.of(allocationTokenExtension),
tld,
"example.tld",
CommandName.CREATE));
}
@Test
void testFailure_loadOrDefault_noValidTokens() throws Exception {
assertThat(
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
"TheRegistrar",
clock.nowUtc(),
Optional.empty(),
tld,
"example.tld",
CommandName.CREATE))
.isEmpty();
}
@Test
void testFailure_loadOrDefault_badDomainName() throws Exception {
// Tokens tied to a domain should throw a catastrophic exception if used for a different domain
persistResource(singleUseTokenBuilder().setDomainName("someotherdomain.tld").build());
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
assertThrows(
AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException.class,
() ->
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
"TheRegistrar",
clock.nowUtc(),
Optional.of(allocationTokenExtension),
tld,
"example.tld",
CommandName.CREATE));
}
private AllocationToken persistDefaultToken() {
AllocationToken defaultToken =
persistResource(
new AllocationToken.Builder()
.setToken("defaultToken")
.setDiscountFraction(0.1)
.setAllowedTlds(ImmutableSet.of("tld"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setTokenType(DEFAULT_PROMO)
.build());
tld =
persistResource(
tld.asBuilder()
.setDefaultPromoTokens(ImmutableList.of(defaultToken.createVKey()))
.build());
return defaultToken;
}
private void assertLoadTokenFromExtensionThrowsException(Class<? extends EppException> clazz) {
assertAboutEppExceptions()
.that(
assertThrows(
clazz,
() ->
flowUtils.verifyAllocationTokenCreateIfPresent(
createCommand("blah.tld"),
Tld.get("tld"),
AllocationTokenFlowUtils.loadAllocationTokenFromExtension(
"TheRegistrar",
DateTime.now(UTC),
"example.tld",
clock.nowUtc(),
Optional.of(allocationTokenExtension))))
.marshalsToXml();
}
private void assertValidateExistingDomainThrowsEppException(Class<? extends EppException> clazz) {
assertAboutEppExceptions()
.that(
assertThrows(
clazz,
() ->
flowUtils.verifyAllocationTokenIfPresent(
DatabaseHelper.newDomain("blah.tld"),
Tld.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
CommandName.RENEW,
Optional.of(allocationTokenExtension))))
.marshalsToXml();
}
private static DomainCommand.Create createCommand(String domainName) {
DomainCommand.Create command = mock(DomainCommand.Create.class);
when(command.getDomainName()).thenReturn(domainName);
return command;
private AllocationToken.Builder singleUseTokenBuilder() {
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
return new AllocationToken.Builder()
.setTokenType(SINGLE_USE)
.setToken("tokeN")
.setAllowedTlds(ImmutableSet.of("tld"))
.setDiscountFraction(0.1)
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"));
}
private AllocationToken.Builder createOneMonthPromoTokenBuilder(DateTime promoStart) {
@@ -36,27 +36,27 @@ public class EppResourceTest extends EntityTestCase {
new TestCacheExtension.Builder().withEppResourceCache(Duration.ofDays(1)).build();
@Test
void test_loadCached_ignoresContactChange() {
void test_loadByCacheIfEnabled_ignoresContactChange() {
Contact originalContact = persistActiveContact("contact123");
assertThat(EppResource.loadCached(ImmutableList.of(originalContact.createVKey())))
assertThat(EppResource.loadByCacheIfEnabled(ImmutableList.of(originalContact.createVKey())))
.containsExactly(originalContact.createVKey(), originalContact);
Contact modifiedContact =
persistResource(originalContact.asBuilder().setEmailAddress("different@fake.lol").build());
assertThat(EppResource.loadCached(ImmutableList.of(originalContact.createVKey())))
assertThat(EppResource.loadByCacheIfEnabled(ImmutableList.of(originalContact.createVKey())))
.containsExactly(originalContact.createVKey(), originalContact);
assertThat(loadByForeignKey(Contact.class, "contact123", fakeClock.nowUtc()))
.hasValue(modifiedContact);
}
@Test
void test_loadCached_ignoresHostChange() {
void test_loadByCacheIfEnabled_ignoresHostChange() {
Host originalHost = persistActiveHost("ns1.example.com");
assertThat(EppResource.loadCached(ImmutableList.of(originalHost.createVKey())))
assertThat(EppResource.loadByCacheIfEnabled(ImmutableList.of(originalHost.createVKey())))
.containsExactly(originalHost.createVKey(), originalHost);
Host modifiedHost =
persistResource(
originalHost.asBuilder().setLastTransferTime(fakeClock.nowUtc().minusDays(60)).build());
assertThat(EppResource.loadCached(ImmutableList.of(originalHost.createVKey())))
assertThat(EppResource.loadByCacheIfEnabled(ImmutableList.of(originalHost.createVKey())))
.containsExactly(originalHost.createVKey(), originalHost);
assertThat(loadByForeignKey(Host.class, "ns1.example.com", fakeClock.nowUtc()))
.hasValue(modifiedHost);
@@ -121,7 +121,7 @@ class ForeignKeyUtilsTest {
fakeClock.advanceOneMilli();
Host newHost1 = persistActiveHost("ns1.example.com");
assertThat(
ForeignKeyUtils.loadCached(
ForeignKeyUtils.loadByCacheIfEnabled(
Host.class,
ImmutableList.of("ns1.example.com", "ns2.example.com", "ns3.example.com"),
fakeClock.nowUtc()))
@@ -134,7 +134,7 @@ class ForeignKeyUtilsTest {
Host host2 = persistActiveHost("ns2.example.com");
persistResource(host2.asBuilder().setDeletionTime(fakeClock.nowUtc().minusDays(1)).build());
assertThat(
ForeignKeyUtils.loadCached(
ForeignKeyUtils.loadByCacheIfEnabled(
Host.class,
ImmutableList.of("ns1.example.com", "ns2.example.com", "ns3.example.com"),
fakeClock.nowUtc()))
@@ -144,7 +144,7 @@ class ForeignKeyUtilsTest {
persistActiveHost("ns1.example.com");
// Even though a new host1 is now live, the cache still returns the VKey to the old one.
assertThat(
ForeignKeyUtils.loadCached(
ForeignKeyUtils.loadByCacheIfEnabled(
Host.class,
ImmutableList.of("ns1.example.com", "ns2.example.com", "ns3.example.com"),
fakeClock.nowUtc()))
@@ -0,0 +1,65 @@
// Copyright 2025 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.model.console;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.junit.Assert.assertThrows;
import google.registry.model.EntityTestCase;
import google.registry.persistence.VKey;
import google.registry.testing.DatabaseHelper;
import org.junit.jupiter.api.Test;
/** Tests for {@link PasswordResetRequest}. */
public class PasswordResetRequestTest extends EntityTestCase {
PasswordResetRequestTest() {
super(JpaEntityCoverageCheck.ENABLED);
}
@Test
void testSuccess_persistence() {
PasswordResetRequest request =
new PasswordResetRequest.Builder()
.setRequester("requestor@email.tld")
.setDestinationEmail("destination@email.tld")
.setType(PasswordResetRequest.Type.EPP)
.setRegistrarId("TheRegistrar")
.build();
String verificationCode = request.getVerificationCode();
assertThat(verificationCode).isNotEmpty();
persistResource(request);
PasswordResetRequest fromDatabase =
DatabaseHelper.loadByKey(VKey.create(PasswordResetRequest.class, verificationCode));
assertAboutImmutableObjects().that(fromDatabase).isEqualExceptFields(request, "requestTime");
assertThat(fromDatabase.getRequestTime()).isEqualTo(fakeClock.nowUtc());
}
@Test
void testFailure_nullFields() {
PasswordResetRequest.Builder builder = new PasswordResetRequest.Builder();
assertThrows(IllegalArgumentException.class, builder::build);
builder.setType(PasswordResetRequest.Type.EPP);
assertThrows(IllegalArgumentException.class, builder::build);
builder.setRequester("foobar@email.tld");
assertThrows(IllegalArgumentException.class, builder::build);
builder.setDestinationEmail("email@email.tld");
assertThrows(IllegalArgumentException.class, builder::build);
builder.setRegistrarId("TheRegistrar");
builder.build();
}
}
@@ -44,6 +44,7 @@ import google.registry.model.registrar.Registrar.Type;
import google.registry.model.tld.Tld;
import google.registry.model.tld.Tld.TldType;
import google.registry.model.tld.Tlds;
import google.registry.testing.DatabaseHelper;
import google.registry.util.CidrAddressBlock;
import google.registry.util.SerializeUtils;
import java.math.BigDecimal;
@@ -340,6 +341,7 @@ class RegistrarTest extends EntityTestCase {
.setFaxNumber("+1.2125551213")
.setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH, RegistrarPoc.Type.ABUSE))
.build());
abuseAdminContact = DatabaseHelper.loadByKey(abuseAdminContact.createVKey());
ImmutableSortedSet<RegistrarPoc> techContacts =
registrar.getContactsOfType(RegistrarPoc.Type.TECH);
assertThat(techContacts).containsExactly(newTechContact, newTechAbuseContact).inOrder();
@@ -496,6 +498,28 @@ class RegistrarTest extends EntityTestCase {
.isEqualTo(fakeClock.nowUtc());
}
@Test
void testSuccess_setLastPocVerificationDate() {
assertThat(
registrar
.asBuilder()
.setLastPocVerificationDate(fakeClock.nowUtc())
.build()
.getLastPocVerificationDate())
.isEqualTo(fakeClock.nowUtc());
}
@Test
void testFailure_setLastPocVerificationDate_nullDate() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() -> new Registrar.Builder().setLastPocVerificationDate(null).build());
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Registrar lastPocVerificationDate cannot be null");
}
@Test
void testFailure_setLastExpiringFailoverCertNotificationSentDate_nullDate() {
IllegalArgumentException thrown =
@@ -379,6 +379,7 @@ public abstract class JpaTransactionManagerExtension
.setPassword("foo-BAR2")
.setPhoneNumber("+1.3334445555")
.setPhonePasscode("12345")
.setRdapBaseUrls(ImmutableSet.of("https://rdap.newregistrar.com/"))
.setRegistryLockAllowed(false)
.build();
}
@@ -393,6 +394,7 @@ public abstract class JpaTransactionManagerExtension
.setPassword("password2")
.setPhoneNumber("+1.2223334444")
.setPhonePasscode("22222")
.setRdapBaseUrls(ImmutableSet.of("https://rdap.theregistrar.com/"))
.setRegistryLockAllowed(true)
.build();
}
@@ -90,12 +90,6 @@ class RdapActionBaseTest extends RdapActionBaseTestCase<RdapActionBaseTest.RdapT
assertThat(response.getStatus()).isEqualTo(500);
}
@Test
void testValidName_works() {
assertThat(generateActualJson("no.thing")).isEqualTo(loadJsonFile("rdapjson_toplevel.json"));
assertThat(response.getStatus()).isEqualTo(200);
}
@Test
void testContentType_rdapjson_utf8() {
generateActualJson("no.thing");
@@ -22,7 +22,12 @@ import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import static org.mockito.Mockito.mock;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.persistence.transaction.JpaTestExtensions;
@@ -35,6 +40,7 @@ import google.registry.util.Idn;
import google.registry.util.TypeUtils;
import java.util.HashMap;
import java.util.Optional;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -43,6 +49,7 @@ import org.junit.jupiter.api.extension.RegisterExtension;
abstract class RdapActionBaseTestCase<A extends RdapActionBase> {
protected final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ"));
static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
@RegisterExtension
final JpaIntegrationTestExtension jpa =
@@ -107,18 +114,13 @@ abstract class RdapActionBaseTestCase<A extends RdapActionBase> {
metricRole = ADMINISTRATOR;
}
JsonObject generateActualJson(String domainName) {
action.requestPath = actionPath + domainName;
action.requestMethod = GET;
action.run();
return RdapTestHelper.parseJsonObject(response.getPayload());
JsonObject generateActualJson(String name) {
return RdapTestHelper.parseJsonObject(runAction(name));
}
String generateHeadPayload(String domainName) {
action.requestPath = actionPath + domainName;
String generateHeadPayload(String name) {
action.requestMethod = HEAD;
action.run();
return response.getPayload();
return runAction(name);
}
JsonObject generateExpectedJsonError(String description, int code) {
@@ -138,16 +140,125 @@ abstract class RdapActionBaseTestCase<A extends RdapActionBase> {
"TITLE",
title,
"CODE",
String.valueOf(code));
String.valueOf(code),
"REQUEST_URL",
action.requestUrl);
}
static JsonFileBuilder jsonFileBuilder() {
return new JsonFileBuilder();
JsonFileBuilder jsonFileBuilder() {
return new JsonFileBuilder(action.requestUrl);
}
private String runAction(String name) {
action.requestPath = actionPath + name;
action.requestUrl = "https://example.tld" + actionPath + name;
action.run();
return response.getPayload();
}
JsonElement createTosNotice() {
return JsonParser.parseString(
"""
{
"title": "RDAP Terms of Service",
"description": [
"By querying our Domain Database, you are agreeing to comply with these terms so please read \
them carefully.",
"Any information provided is 'as is' without any guarantee of accuracy.",
"Please do not misuse the Domain Database. It is intended solely for query-based access.",
"Don't use the Domain Database to allow, enable, or otherwise support the transmission of mass \
unsolicited, commercial advertising or solicitations.",
"Don't access our Domain Database through the use of high volume, automated electronic \
processes that send queries or data to the systems of any ICANN-accredited registrar.",
"You may only use the information contained in the Domain Database for lawful purposes.",
"Do not compile, repackage, disseminate, or otherwise use the information contained in the \
Domain Database in its entirety, or in any substantial portion, without our prior written \
permission.",
"We may retain certain details about queries to our Domain Database for the purposes of \
detecting and preventing misuse.",
"We reserve the right to restrict or deny your access to the database if we suspect that you \
have failed to comply with these terms.",
"We reserve the right to modify this agreement at any time."
],
"links": [
{
"rel": "self",
"href": "https://example.tld/rdap/help/tos",
"type": "application/rdap+json",
"value": "%REQUEST_URL%"
},
{
"rel": "terms-of-service",
"href": "https://www.example.tld/about/rdap/tos.html",
"type": "text/html",
"value": "%REQUEST_URL%"
}
]
}
"""
.replaceAll("%REQUEST_URL%", action.requestUrl));
}
JsonObject addPermanentBoilerplateNotices(JsonObject jsonObject) {
if (!jsonObject.has("notices")) {
jsonObject.add("notices", new JsonArray());
}
JsonArray notices = jsonObject.getAsJsonArray("notices");
notices.add(createTosNotice());
return jsonObject;
}
JsonObject addDomainBoilerplateNotices(JsonObject jsonObject) {
addPermanentBoilerplateNotices(jsonObject);
JsonArray notices = jsonObject.getAsJsonArray("notices");
notices.add(
JsonParser.parseString(
"""
{
"title": "Status Codes",
"description": [
"For more information on domain status codes, please visit https://icann.org/epp"
],
"links": [
{
"rel": "glossary",
"href": "https://icann.org/epp",
"type": "text/html",
"value": "%REQUEST_URL%"
}
]
}
"""
.replaceAll("%REQUEST_URL%", action.requestUrl)));
notices.add(
JsonParser.parseString(
"""
{
"title": "RDDS Inaccuracy Complaint Form",
"description": [
"URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf"
],
"links": [
{
"rel": "help",
"href": "https://icann.org/wicf",
"type": "text/html",
"value": "%REQUEST_URL%"
}
]
}
"""
.replaceAll("%REQUEST_URL%", action.requestUrl)));
return jsonObject;
}
protected static final class JsonFileBuilder {
private final HashMap<String, String> substitutions = new HashMap<>();
private JsonFileBuilder(String requestUrl) {
substitutions.put("REQUEST_URL", requestUrl);
}
public JsonObject load(String filename) {
return RdapTestHelper.loadJsonFile(filename, substitutions);
}
@@ -158,6 +269,14 @@ abstract class RdapActionBaseTestCase<A extends RdapActionBase> {
return this;
}
public JsonFileBuilder putAll(String... keysAndValues) {
checkArgument(keysAndValues.length % 2 == 0);
for (int i = 0; i < keysAndValues.length; i += 2) {
put(keysAndValues[i], keysAndValues[i + 1]);
}
return this;
}
public JsonFileBuilder put(String key, int index, String value) {
return put(String.format("%s%d", key, index), value);
}
@@ -189,10 +308,38 @@ abstract class RdapActionBaseTestCase<A extends RdapActionBase> {
return putNext("REGISTRAR_FULL_NAME_", fullName);
}
JsonFileBuilder addFullRegistrar(
String handle, @Nullable String fullName, String status, @Nullable String address) {
if (fullName != null) {
putNext("REGISTRAR_FULLNAME_", fullName);
}
if (address != null) {
putNext("REGISTRAR_ADDRESS_", address);
}
return putNext("REGISTRAR_HANDLE_", handle, "STATUS_", status);
}
JsonFileBuilder addContact(String handle) {
return putNext("CONTACT_HANDLE_", handle);
}
JsonFileBuilder addFullContact(
String handle,
@Nullable String status,
@Nullable String fullName,
@Nullable String address) {
if (fullName != null) {
putNext("CONTACT_FULLNAME_", fullName);
}
if (address != null) {
putNext("CONTACT_ADDRESS_", address);
}
if (status != null) {
putNext("STATUS_", status);
}
return putNext("CONTACT_HANDLE_", handle);
}
JsonFileBuilder setNextQuery(String nextQuery) {
return put("NEXT_QUERY", nextQuery);
}
@@ -43,12 +43,13 @@ final class RdapDataStructuresTest {
@Test
void testRdapConformance() {
assertThat(RdapConformance.INSTANCE.toJson())
.isEqualTo(createJson(
"[",
" 'rdap_level_0',",
" 'icann_rdap_response_profile_0',",
" 'icann_rdap_technical_implementation_guide_0'",
"]"));
.isEqualTo(
createJson(
"[",
" 'rdap_level_0',",
" 'icann_rdap_response_profile_1',",
" 'icann_rdap_technical_implementation_guide_1'",
"]"));
}
@Test
@@ -59,9 +60,12 @@ final class RdapDataStructuresTest {
.setRel("myRel")
.setTitle("myTitle")
.setType("myType")
.setValue("myValue")
.build();
assertThat(link.toJson())
.isEqualTo(createJson("{'href':'myHref','rel':'myRel','title':'myTitle','type':'myType'}"));
.isEqualTo(
createJson(
"{'href':'myHref','rel':'myRel','title':'myTitle','type':'myType','value':'myValue'}"));
assertRestrictedNames(link, "links[]");
}
@@ -230,16 +230,11 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
clock.nowUtc().minusMonths(6)));
}
private JsonObject addBoilerplate(JsonObject obj) {
RdapTestHelper.addDomainBoilerplateNotices(obj, "https://example.tld/rdap/");
return obj;
}
private void assertProperResponseForCatLol(String queryString, String expectedOutputFile) {
assertAboutJson()
.that(generateActualJson(queryString))
.isEqualTo(
addBoilerplate(
addDomainBoilerplateNotices(
jsonFileBuilder()
.addDomain("cat.lol", "C-LOL")
.addContact("4-ROID")
@@ -357,7 +352,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
assertAboutJson()
.that(generateActualJson("cat.みんな"))
.isEqualTo(
addBoilerplate(
addDomainBoilerplateNotices(
jsonFileBuilder()
.addDomain("cat.みんな", "1D-Q9JYB4C")
.addContact("19-ROID")
@@ -376,7 +371,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
assertAboutJson()
.that(generateActualJson("cat.%E3%81%BF%E3%82%93%E3%81%AA"))
.isEqualTo(
addBoilerplate(
addDomainBoilerplateNotices(
jsonFileBuilder()
.addDomain("cat.みんな", "1D-Q9JYB4C")
.addContact("19-ROID")
@@ -395,7 +390,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
assertAboutJson()
.that(generateActualJson("cat.xn--q9jyb4c"))
.isEqualTo(
addBoilerplate(
addDomainBoilerplateNotices(
jsonFileBuilder()
.addDomain("cat.みんな", "1D-Q9JYB4C")
.addContact("19-ROID")
@@ -414,7 +409,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
assertAboutJson()
.that(generateActualJson("cat.1.tld"))
.isEqualTo(
addBoilerplate(
addDomainBoilerplateNotices(
jsonFileBuilder()
.addDomain("cat.1.tld", "25-1_TLD")
.addContact("21-ROID")
@@ -473,7 +468,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
assertAboutJson()
.that(generateActualJson("dodo.lol"))
.isEqualTo(
addBoilerplate(
addDomainBoilerplateNotices(
jsonFileBuilder()
.addDomain("dodo.lol", "15-LOL")
.addContact("11-ROID")
@@ -493,7 +488,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
assertAboutJson()
.that(generateActualJson("dodo.lol"))
.isEqualTo(
addBoilerplate(
addDomainBoilerplateNotices(
jsonFileBuilder()
.addDomain("dodo.lol", "15-LOL")
.addContact("11-ROID")
@@ -512,7 +507,9 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
"addgraceperiod", "lol", clock.nowUtc(), clock.nowUtc().plusYears(1));
assertAboutJson()
.that(generateActualJson("addgraceperiod.lol"))
.isEqualTo(addBoilerplate(jsonFileBuilder().load("rdap_domain_add_grace_period.json")));
.isEqualTo(
addDomainBoilerplateNotices(
jsonFileBuilder().load("rdap_domain_add_grace_period.json")));
}
@Test
@@ -522,7 +519,8 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
assertAboutJson()
.that(generateActualJson("autorenew.lol"))
.isEqualTo(
addBoilerplate(jsonFileBuilder().load("rdap_domain_auto_renew_grace_period.json")));
addDomainBoilerplateNotices(
jsonFileBuilder().load("rdap_domain_auto_renew_grace_period.json")));
}
@Test
@@ -545,7 +543,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
assertAboutJson()
.that(generateActualJson("redemption.lol"))
.isEqualTo(
addBoilerplate(
addDomainBoilerplateNotices(
jsonFileBuilder().load("rdap_domain_pending_delete_redemption_grace_period.json")));
}
@@ -568,7 +566,8 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
assertAboutJson()
.that(generateActualJson("renew.lol"))
.isEqualTo(
addBoilerplate(jsonFileBuilder().load("rdap_domain_explicit_renew_grace_period.json")));
addDomainBoilerplateNotices(
jsonFileBuilder().load("rdap_domain_explicit_renew_grace_period.json")));
}
@Test
@@ -590,7 +589,8 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
assertAboutJson()
.that(generateActualJson("transfer.lol"))
.isEqualTo(
addBoilerplate(jsonFileBuilder().load("rdap_domain_transfer_grace_period.json")));
addDomainBoilerplateNotices(
jsonFileBuilder().load("rdap_domain_transfer_grace_period.json")));
}
@Test
@@ -631,12 +631,15 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
"rel",
"alternate",
"type",
"text/html")));
"text/html",
"value",
"https://example.tld/rdap/domain/example.lol")));
JsonObject actuaResponse = generateActualJson("example.lol");
JsonObject expectedErrorResponse = generateExpectedJsonError("example.lol blocked by BSA", 404);
expectedErrorResponse
.getAsJsonArray("notices")
.add(RdapTestHelper.GSON.toJsonTree(expectedBsaNotice));
assertAboutJson().that(generateActualJson("example.lol")).isEqualTo(expectedErrorResponse);
assertAboutJson().that(actuaResponse).isEqualTo(expectedErrorResponse);
assertThat(response.getStatus()).isEqualTo(404);
}
@@ -473,8 +473,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
private JsonObject wrapInSearchReply(JsonObject obj) {
obj = RdapTestHelper.wrapInSearchReply("domainSearchResults", obj);
RdapTestHelper.addDomainBoilerplateNotices(obj, "https://example.tld/rdap/");
return obj;
return addDomainBoilerplateNotices(obj);
}
private void runSuccessfulTest(RequestType requestType, String queryString, JsonObject expected) {
@@ -15,7 +15,6 @@
package google.registry.rdap;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.rdap.RdapTestHelper.loadJsonFile;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.DatabaseHelper.persistSimpleResources;
@@ -28,7 +27,6 @@ import static google.registry.testing.GsonSubject.assertAboutJson;
import static org.mockito.Mockito.verify;
import com.google.common.collect.ImmutableList;
import com.google.gson.JsonObject;
import google.registry.model.contact.Contact;
import google.registry.model.host.Host;
import google.registry.model.registrar.Registrar;
@@ -39,13 +37,15 @@ import google.registry.rdap.RdapSearchResults.IncompletenessWarningType;
import google.registry.request.Action;
import google.registry.testing.FullFieldsTestEntityHelper;
import java.util.Optional;
import javax.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link RdapEntityAction}. */
class RdapEntityActionTest extends RdapActionBaseTestCase<RdapEntityAction> {
private static final String CONTACT_NAME = "(◕‿◕)";
private static final String CONTACT_ADDRESS = "\"1 Smiley Row\", \"Suite みんな\"";
RdapEntityActionTest() {
super(RdapEntityAction.class);
}
@@ -67,7 +67,7 @@ class RdapEntityActionTest extends RdapActionBaseTestCase<RdapEntityAction> {
registrant =
FullFieldsTestEntityHelper.makeAndPersistContact(
"8372808-REG",
"(◕‿◕)",
CONTACT_NAME,
"lol@cat.みんな",
ImmutableList.of("1 Smiley Row", "Suite みんな"),
clock.nowUtc(),
@@ -75,7 +75,7 @@ class RdapEntityActionTest extends RdapActionBaseTestCase<RdapEntityAction> {
adminContact =
FullFieldsTestEntityHelper.makeAndPersistContact(
"8372808-ADM",
"(◕‿◕)",
CONTACT_NAME,
"lol@cat.みんな",
ImmutableList.of("1 Smiley Row", "Suite みんな"),
clock.nowUtc(),
@@ -83,7 +83,7 @@ class RdapEntityActionTest extends RdapActionBaseTestCase<RdapEntityAction> {
techContact =
FullFieldsTestEntityHelper.makeAndPersistContact(
"8372808-TEC",
"(◕‿◕)",
CONTACT_NAME,
"lol@cat.みんな",
ImmutableList.of("1 Smiley Row", "Suite みんな"),
clock.nowUtc(),
@@ -110,7 +110,7 @@ class RdapEntityActionTest extends RdapActionBaseTestCase<RdapEntityAction> {
disconnectedContact =
FullFieldsTestEntityHelper.makeAndPersistContact(
"8372808-DIS",
"(◕‿◕)",
CONTACT_NAME,
"lol@cat.みんな",
ImmutableList.of("1 Smiley Row", "Suite みんな"),
clock.nowUtc(),
@@ -123,186 +123,191 @@ class RdapEntityActionTest extends RdapActionBaseTestCase<RdapEntityAction> {
clock.nowUtc().minusMonths(6));
}
private JsonObject generateExpectedJson(
String handle,
String fullName,
String status,
@Nullable String address,
String expectedOutputFile) {
return loadJsonFile(
expectedOutputFile,
"NAME", handle,
"FULLNAME", fullName,
"ADDRESS", (address == null) ? "\"1 Smiley Row\", \"Suite みんな\"" : address,
"TYPE", "entity",
"STATUS", status);
}
private JsonObject generateExpectedJsonWithTopLevelEntries(
String handle,
String expectedOutputFile) {
return generateExpectedJsonWithTopLevelEntries(
handle, "(◕‿◕)", "active", null, expectedOutputFile);
}
private JsonObject generateExpectedJsonWithTopLevelEntries(
String handle,
String fullName,
String status,
String address,
String expectedOutputFile) {
JsonObject obj = generateExpectedJson(handle, fullName, status, address, expectedOutputFile);
RdapTestHelper.addNonDomainBoilerplateNotices(obj, "https://example.tld/rdap/");
return obj;
}
private void runSuccessfulHandleTest(String handleQuery, String fileName) {
runSuccessfulHandleTest(handleQuery, "(◕‿◕)", "active", null, fileName);
}
private void runSuccessfulHandleTest(String handleQuery, String fullName, String fileName) {
runSuccessfulHandleTest(handleQuery, fullName, "active", null, fileName);
}
private void runSuccessfulHandleTest(
String handleQuery,
String fullName,
String rdapStatus,
String address,
String fileName) {
@Test
void testUnknownEntity_RoidPattern_notFound() {
assertAboutJson()
.that(generateActualJson(handleQuery))
.isEqualTo(
generateExpectedJsonWithTopLevelEntries(
handleQuery, fullName, rdapStatus, address, fileName));
assertThat(response.getStatus()).isEqualTo(200);
}
private void runNotFoundTest(String handleQuery) {
assertAboutJson()
.that(generateActualJson(handleQuery))
.isEqualTo(generateExpectedJsonError(handleQuery + " not found", 404));
.that(generateActualJson("_MISSING-ENTITY_"))
.isEqualTo(generateExpectedJsonError("_MISSING-ENTITY_ not found", 404));
assertThat(response.getStatus()).isEqualTo(404);
}
@Test
void testUnknownEntity_RoidPattern_notFound() {
runNotFoundTest("_MISSING-ENTITY_");
}
@Test
void testUnknownEntity_IanaPattern_notFound() {
runNotFoundTest("123");
assertAboutJson()
.that(generateActualJson("123"))
.isEqualTo(generateExpectedJsonError("123 not found", 404));
assertThat(response.getStatus()).isEqualTo(404);
}
@Test
void testUnknownEntity_notRoidNotIana_notFound() {
// Since we allow search by registrar name, every string is a possible name
runNotFoundTest("some,random,string");
assertAboutJson()
.that(generateActualJson("some,random,string"))
.isEqualTo(generateExpectedJsonError("some,random,string not found", 404));
assertThat(response.getStatus()).isEqualTo(404);
}
@Test
void testValidRegistrantContact_works() {
login("evilregistrar");
runSuccessfulHandleTest(registrant.getRepoId(), "rdap_associated_contact.json");
assertAboutJson()
.that(generateActualJson(registrant.getRepoId()))
.isEqualTo(
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addFullContact(registrant.getRepoId(), null, CONTACT_NAME, CONTACT_ADDRESS)
.load("rdap_associated_contact.json")));
}
@Test
void testValidRegistrantContact_found_asAdministrator() {
loginAsAdmin();
runSuccessfulHandleTest(registrant.getRepoId(), "rdap_associated_contact.json");
assertAboutJson()
.that(generateActualJson(registrant.getRepoId()))
.isEqualTo(
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addFullContact(registrant.getRepoId(), null, CONTACT_NAME, CONTACT_ADDRESS)
.load("rdap_associated_contact.json")));
}
@Test
void testValidRegistrantContact_found_notLoggedIn() {
runSuccessfulHandleTest(
registrant.getRepoId(),
"(◕‿◕)",
"active",
null,
"rdap_associated_contact_no_personal_data.json");
assertAboutJson()
.that(generateActualJson(registrant.getRepoId()))
.isEqualTo(
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addFullContact(registrant.getRepoId(), "active", CONTACT_NAME, CONTACT_ADDRESS)
.load("rdap_associated_contact_no_personal_data.json")));
}
@Test
void testValidRegistrantContact_found_loggedInAsOtherRegistrar() {
login("otherregistrar");
runSuccessfulHandleTest(
registrant.getRepoId(),
"(◕‿◕)",
"active",
null,
"rdap_associated_contact_no_personal_data.json");
assertAboutJson()
.that(generateActualJson(registrant.getRepoId()))
.isEqualTo(
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addFullContact(registrant.getRepoId(), "active", CONTACT_NAME, CONTACT_ADDRESS)
.load("rdap_associated_contact_no_personal_data.json")));
}
@Test
void testValidAdminContact_works() {
login("evilregistrar");
runSuccessfulHandleTest(adminContact.getRepoId(), "rdap_associated_contact.json");
assertAboutJson()
.that(generateActualJson(adminContact.getRepoId()))
.isEqualTo(
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addFullContact(adminContact.getRepoId(), null, CONTACT_NAME, CONTACT_ADDRESS)
.load("rdap_associated_contact.json")));
}
@Test
void testValidTechContact_works() {
login("evilregistrar");
runSuccessfulHandleTest(techContact.getRepoId(), "rdap_associated_contact.json");
assertAboutJson()
.that(generateActualJson(techContact.getRepoId()))
.isEqualTo(
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addFullContact(techContact.getRepoId(), null, CONTACT_NAME, CONTACT_ADDRESS)
.load("rdap_associated_contact.json")));
}
@Test
void testValidDisconnectedContact_works() {
login("evilregistrar");
runSuccessfulHandleTest(disconnectedContact.getRepoId(), "rdap_contact.json");
assertAboutJson()
.that(generateActualJson(disconnectedContact.getRepoId()))
.isEqualTo(
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addFullContact(
disconnectedContact.getRepoId(), "active", CONTACT_NAME, CONTACT_ADDRESS)
.load("rdap_contact.json")));
}
@Test
void testDeletedContact_notFound() {
runNotFoundTest(deletedContact.getRepoId());
String repoId = deletedContact.getRepoId();
assertAboutJson()
.that(generateActualJson(repoId))
.isEqualTo(generateExpectedJsonError(repoId + " not found", 404));
assertThat(response.getStatus()).isEqualTo(404);
}
@Test
void testDeletedContact_notFound_includeDeletedSetFalse() {
action.includeDeletedParam = Optional.of(false);
runNotFoundTest(deletedContact.getRepoId());
String repoId = deletedContact.getRepoId();
assertAboutJson()
.that(generateActualJson(repoId))
.isEqualTo(generateExpectedJsonError(repoId + " not found", 404));
assertThat(response.getStatus()).isEqualTo(404);
}
@Test
void testDeletedContact_notFound_notLoggedIn() {
action.includeDeletedParam = Optional.of(true);
runNotFoundTest(deletedContact.getRepoId());
String repoId = deletedContact.getRepoId();
assertAboutJson()
.that(generateActualJson(repoId))
.isEqualTo(generateExpectedJsonError(repoId + " not found", 404));
assertThat(response.getStatus()).isEqualTo(404);
}
@Test
void testDeletedContact_notFound_loggedInAsDifferentRegistrar() {
login("idnregistrar");
action.includeDeletedParam = Optional.of(true);
runNotFoundTest(deletedContact.getRepoId());
String repoId = deletedContact.getRepoId();
assertAboutJson()
.that(generateActualJson(repoId))
.isEqualTo(generateExpectedJsonError(repoId + " not found", 404));
assertThat(response.getStatus()).isEqualTo(404);
}
@Test
void testDeletedContact_found_loggedInAsCorrectRegistrar() {
login("evilregistrar");
action.includeDeletedParam = Optional.of(true);
runSuccessfulHandleTest(
deletedContact.getRepoId(),
"",
"inactive",
"",
"rdap_contact_deleted.json");
assertAboutJson()
.that(generateActualJson(deletedContact.getRepoId()))
.isEqualTo(
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addContact(deletedContact.getRepoId())
.load("rdap_contact_deleted.json")));
}
@Test
void testDeletedContact_found_loggedInAsAdmin() {
loginAsAdmin();
action.includeDeletedParam = Optional.of(true);
runSuccessfulHandleTest(
deletedContact.getRepoId(),
"",
"inactive",
"",
"rdap_contact_deleted.json");
assertAboutJson()
.that(generateActualJson(deletedContact.getRepoId()))
.isEqualTo(
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addContact(deletedContact.getRepoId())
.load("rdap_contact_deleted.json")));
}
@Test
void testRegistrar_found() {
runSuccessfulHandleTest("101", "Yes Virginia <script>", "rdap_registrar.json");
assertAboutJson()
.that(generateActualJson("101"))
.isEqualTo(
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addFullRegistrar("101", "Yes Virginia <script>", "active", null)
.load("rdap_registrar.json")));
assertThat(response.getStatus()).isEqualTo(200);
}
@Test
@@ -310,58 +315,97 @@ class RdapEntityActionTest extends RdapActionBaseTestCase<RdapEntityAction> {
assertAboutJson()
.that(generateActualJson("IDN%20Registrar"))
.isEqualTo(
generateExpectedJsonWithTopLevelEntries(
"102", "IDN Registrar", "active", null, "rdap_registrar.json"));
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addFullRegistrar("102", "IDN Registrar", "active", null)
.load("rdap_registrar.json")));
assertThat(response.getStatus()).isEqualTo(200);
}
@Test
void testRegistrar102_works() {
runSuccessfulHandleTest("102", "IDN Registrar", "rdap_registrar.json");
assertAboutJson()
.that(generateActualJson("102"))
.isEqualTo(
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addFullRegistrar("102", "IDN Registrar", "active", null)
.load("rdap_registrar.json")));
}
@Test
void testRegistrar103_works() {
runSuccessfulHandleTest("103", "Multilevel Registrar", "rdap_registrar.json");
assertAboutJson()
.that(generateActualJson("103"))
.isEqualTo(
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addFullRegistrar("103", "Multilevel Registrar", "active", null)
.load("rdap_registrar.json")));
assertThat(response.getStatus()).isEqualTo(200);
}
@Test
void testRegistrar104_notFound() {
runNotFoundTest("104");
assertAboutJson()
.that(generateActualJson("104"))
.isEqualTo(generateExpectedJsonError("104 not found", 404));
assertThat(response.getStatus()).isEqualTo(404);
}
@Test
void testRegistrar104_notFound_deletedFlagWhenNotLoggedIn() {
action.includeDeletedParam = Optional.of(true);
runNotFoundTest("104");
assertAboutJson()
.that(generateActualJson("104"))
.isEqualTo(generateExpectedJsonError("104 not found", 404));
assertThat(response.getStatus()).isEqualTo(404);
}
@Test
void testRegistrar104_found_deletedFlagWhenLoggedIn() {
login("deletedregistrar");
action.includeDeletedParam = Optional.of(true);
runSuccessfulHandleTest(
"104", "Yes Virginia <script>", "inactive", null, "rdap_registrar.json");
assertAboutJson()
.that(generateActualJson("104"))
.isEqualTo(
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addFullRegistrar("104", "Yes Virginia <script>", "inactive", null)
.load("rdap_registrar.json")));
assertThat(response.getStatus()).isEqualTo(200);
}
@Test
void testRegistrar104_notFound_deletedFlagWhenLoggedInAsOther() {
login("1tldregistrar");
action.includeDeletedParam = Optional.of(true);
runNotFoundTest("104");
assertAboutJson()
.that(generateActualJson("104"))
.isEqualTo(generateExpectedJsonError("104 not found", 404));
assertThat(response.getStatus()).isEqualTo(404);
}
@Test
void testRegistrar104_found_deletedFlagWhenLoggedInAsAdmin() {
loginAsAdmin();
action.includeDeletedParam = Optional.of(true);
runSuccessfulHandleTest(
"104", "Yes Virginia <script>", "inactive", null, "rdap_registrar.json");
assertAboutJson()
.that(generateActualJson("104"))
.isEqualTo(
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addFullRegistrar("104", "Yes Virginia <script>", "inactive", null)
.load("rdap_registrar.json")));
assertThat(response.getStatus()).isEqualTo(200);
}
@Test
void testRegistrar105_doesNotExist() {
runNotFoundTest("105");
assertAboutJson()
.that(generateActualJson("105"))
.isEqualTo(generateExpectedJsonError("105 not found", 404));
assertThat(response.getStatus()).isEqualTo(404);
}
@Test
@@ -370,8 +414,10 @@ class RdapEntityActionTest extends RdapActionBaseTestCase<RdapEntityAction> {
assertAboutJson()
.that(generateActualJson(techContact.getRepoId() + "?key=value"))
.isEqualTo(
generateExpectedJsonWithTopLevelEntries(
techContact.getRepoId(), "rdap_associated_contact.json"));
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addFullContact(techContact.getRepoId(), null, CONTACT_NAME, CONTACT_ADDRESS)
.load("rdap_associated_contact.json")));
assertThat(response.getStatus()).isEqualTo(200);
}
@@ -15,7 +15,6 @@
package google.registry.rdap;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.rdap.RdapTestHelper.loadJsonFile;
import static google.registry.rdap.RdapTestHelper.parseJsonObject;
import static google.registry.request.Action.Method.GET;
import static google.registry.testing.DatabaseHelper.createTld;
@@ -30,7 +29,6 @@ import static google.registry.testing.GsonSubject.assertAboutJson;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
@@ -45,13 +43,15 @@ import google.registry.testing.FakeResponse;
import google.registry.testing.FullFieldsTestEntityHelper;
import java.net.URLDecoder;
import java.util.Optional;
import javax.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link RdapEntitySearchAction}. */
class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySearchAction> {
private static final String BINKY_ADDRESS = "\"123 Blinky St\", \"Blinkyland\"";
private static final String BINKY_FULL_NAME = "Blinky (赤ベイ)";
RdapEntitySearchActionTest() {
super(RdapEntitySearchAction.class);
}
@@ -128,7 +128,7 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
FullFieldsTestEntityHelper.makeAndPersistContact(
"blinky",
"Blinky (赤ベイ)",
BINKY_FULL_NAME,
"blinky@b.tld",
ImmutableList.of("123 Blinky St", "Blinkyland"),
clock.nowUtc(),
@@ -150,45 +150,9 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
action.subtypeParam = Optional.empty();
}
private JsonObject generateExpectedJson(String expectedOutputFile) {
return loadJsonFile(expectedOutputFile, "TYPE", "entity");
}
private JsonObject generateExpectedJson(
String handle,
String expectedOutputFile) {
return generateExpectedJson(handle, null, "active", null, expectedOutputFile);
}
private JsonObject generateExpectedJson(
String handle,
@Nullable String fullName,
String status,
@Nullable String address,
String expectedOutputFile) {
ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<>();
builder.put("NAME", handle);
if (fullName != null) {
builder.put("FULLNAME", fullName);
}
if (address != null) {
builder.put("ADDRESS", address);
}
builder.put("TYPE", "entity");
builder.put("STATUS", status);
return loadJsonFile(expectedOutputFile, builder.build());
}
private JsonObject generateExpectedJsonForEntity(
String handle,
String fullName,
String status,
@Nullable String address,
String expectedOutputFile) {
JsonObject obj = generateExpectedJson(handle, fullName, status, address, expectedOutputFile);
obj = RdapTestHelper.wrapInSearchReply("entitySearchResults", obj);
RdapTestHelper.addNonDomainBoilerplateNotices(obj, "https://example.tld/rdap/");
return obj;
private JsonObject addBoilerplate(JsonObject jsonObject) {
jsonObject = RdapTestHelper.wrapInSearchReply("entitySearchResults", jsonObject);
return addPermanentBoilerplateNotices(jsonObject);
}
private void createManyContactsAndRegistrars(
@@ -259,38 +223,6 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
verifyMetrics(numContactsRetrieved);
}
private void runSuccessfulNameTestWithBlinky(String queryString, String fileName) {
runSuccessfulNameTest(
queryString,
"2-ROID",
"Blinky (赤ベイ)",
"active",
"\"123 Blinky St\", \"Blinkyland\"",
fileName);
}
private void runSuccessfulNameTest(
String queryString,
String handle,
@Nullable String fullName,
String fileName) {
runSuccessfulNameTest(queryString, handle, fullName, "active", null, fileName);
}
private void runSuccessfulNameTest(
String queryString,
String handle,
@Nullable String fullName,
String status,
@Nullable String address,
String fileName) {
rememberWildcardType(queryString);
assertAboutJson()
.that(generateActualJsonWithFullName(queryString))
.isEqualTo(generateExpectedJsonForEntity(handle, fullName, status, address, fileName));
assertThat(response.getStatus()).isEqualTo(200);
}
private void runNotFoundNameTest(String queryString) {
rememberWildcardType(queryString);
assertAboutJson()
@@ -299,38 +231,6 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
assertThat(response.getStatus()).isEqualTo(404);
}
private void runSuccessfulHandleTestWithBlinky(String queryString, String fileName) {
runSuccessfulHandleTest(
queryString,
"2-ROID",
"Blinky (赤ベイ)",
"active",
"\"123 Blinky St\", \"Blinkyland\"",
fileName);
}
private void runSuccessfulHandleTest(
String queryString,
String handle,
@Nullable String fullName,
String fileName) {
runSuccessfulHandleTest(queryString, handle, fullName, "active", null, fileName);
}
private void runSuccessfulHandleTest(
String queryString,
String handle,
@Nullable String fullName,
String status,
@Nullable String address,
String fileName) {
rememberWildcardType(queryString);
assertAboutJson()
.that(generateActualJsonWithHandle(queryString))
.isEqualTo(generateExpectedJsonForEntity(handle, fullName, status, address, fileName));
assertThat(response.getStatus()).isEqualTo(200);
}
private void runNotFoundHandleTest(String queryString) {
rememberWildcardType(queryString);
assertAboutJson()
@@ -470,7 +370,7 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testInvalidSubtype_rejected() {
action.subtypeParam = Optional.of("Space Aliens");
assertAboutJson()
.that(generateActualJsonWithFullName("Blinky (赤ベイ)"))
.that(generateActualJsonWithFullName(BINKY_FULL_NAME))
.isEqualTo(
generateExpectedJsonError(
"Subtype parameter must specify contacts, registrars or all", 400));
@@ -482,23 +382,44 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
@Test
void testNameMatchContact_found() {
login("2-RegistrarTest");
runSuccessfulNameTestWithBlinky("Blinky (赤ベイ)", "rdap_contact.json");
rememberWildcardType(BINKY_FULL_NAME);
assertAboutJson()
.that(generateActualJsonWithFullName(BINKY_FULL_NAME))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullContact("2-ROID", "active", BINKY_FULL_NAME, BINKY_ADDRESS)
.load("rdap_contact.json")));
verifyMetrics(1);
}
@Test
void testNameMatchContact_found_subtypeAll() {
login("2-RegistrarTest");
rememberWildcardType(BINKY_FULL_NAME);
action.subtypeParam = Optional.of("aLl");
runSuccessfulNameTestWithBlinky("Blinky (赤ベイ)", "rdap_contact.json");
assertAboutJson()
.that(generateActualJsonWithFullName(BINKY_FULL_NAME))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullContact("2-ROID", "active", BINKY_FULL_NAME, BINKY_ADDRESS)
.load("rdap_contact.json")));
verifyMetrics(1);
}
@Test
void testNameMatchContact_found_subtypeContacts() {
login("2-RegistrarTest");
rememberWildcardType(BINKY_FULL_NAME);
action.subtypeParam = Optional.of("cONTACTS");
runSuccessfulNameTestWithBlinky("Blinky (赤ベイ)", "rdap_contact.json");
assertAboutJson()
.that(generateActualJsonWithFullName(BINKY_FULL_NAME))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullContact("2-ROID", "active", BINKY_FULL_NAME, BINKY_ADDRESS)
.load("rdap_contact.json")));
verifyMetrics(1);
}
@@ -506,15 +427,22 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testNameMatchContact_notFound_subtypeRegistrars() {
login("2-RegistrarTest");
action.subtypeParam = Optional.of("Registrars");
runNotFoundNameTest("Blinky (赤ベイ)");
runNotFoundNameTest(BINKY_FULL_NAME);
verifyErrorMetrics(0);
}
@Test
void testNameMatchContact_found_specifyingSameRegistrar() {
login("2-RegistrarTest");
rememberWildcardType(BINKY_FULL_NAME);
action.registrarParam = Optional.of("2-RegistrarTest");
runSuccessfulNameTestWithBlinky("Blinky (赤ベイ)", "rdap_contact.json");
assertAboutJson()
.that(generateActualJsonWithFullName(BINKY_FULL_NAME))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullContact("2-ROID", "active", BINKY_FULL_NAME, BINKY_ADDRESS)
.load("rdap_contact.json")));
verifyMetrics(1);
}
@@ -522,35 +450,48 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testNameMatchContact_notFound_specifyingOtherRegistrar() {
login("2-RegistrarTest");
action.registrarParam = Optional.of("2-RegistrarInact");
runNotFoundNameTest("Blinky (赤ベイ)");
runNotFoundNameTest(BINKY_FULL_NAME);
verifyErrorMetrics(0);
}
@Test
void testNameMatchContact_found_asAdministrator() {
loginAsAdmin();
rememberWildcardType("Blinky (赤ベイ)");
runSuccessfulNameTestWithBlinky("Blinky (赤ベイ)", "rdap_contact.json");
rememberWildcardType(BINKY_FULL_NAME);
assertAboutJson()
.that(generateActualJsonWithFullName(BINKY_FULL_NAME))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullContact("2-ROID", "active", BINKY_FULL_NAME, BINKY_ADDRESS)
.load("rdap_contact.json")));
verifyMetrics(1);
}
@Test
void testNameMatchContact_notFound_notLoggedIn() {
runNotFoundNameTest("Blinky (赤ベイ)");
runNotFoundNameTest(BINKY_FULL_NAME);
verifyErrorMetrics(0);
}
@Test
void testNameMatchContact_notFound_loggedInAsOtherRegistrar() {
login("2-Registrar");
runNotFoundNameTest("Blinky (赤ベイ)");
runNotFoundNameTest(BINKY_FULL_NAME);
verifyErrorMetrics(0);
}
@Test
void testNameMatchContact_found_wildcard() {
login("2-RegistrarTest");
runSuccessfulNameTestWithBlinky("Blinky*", "rdap_contact.json");
rememberWildcardType("Blinky*");
assertAboutJson()
.that(generateActualJsonWithFullName("Blinky*"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullContact("2-ROID", "active", BINKY_FULL_NAME, BINKY_ADDRESS)
.load("rdap_contact.json")));
verifyMetrics(1);
}
@@ -558,7 +499,14 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testNameMatchContact_found_wildcardSpecifyingSameRegistrar() {
login("2-RegistrarTest");
action.registrarParam = Optional.of("2-RegistrarTest");
runSuccessfulNameTestWithBlinky("Blinky*", "rdap_contact.json");
rememberWildcardType("Blinky*");
assertAboutJson()
.that(generateActualJsonWithFullName("Blinky*"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullContact("2-ROID", "active", BINKY_FULL_NAME, BINKY_ADDRESS)
.load("rdap_contact.json")));
verifyMetrics(1);
}
@@ -576,7 +524,7 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
rememberWildcardType("Blin*");
assertAboutJson()
.that(generateActualJsonWithFullName("Blin*"))
.isEqualTo(generateExpectedJson("rdap_multiple_contacts2.json"));
.isEqualTo(jsonFileBuilder().load("rdap_multiple_contacts2.json"));
assertThat(response.getStatus()).isEqualTo(200);
verifyMetrics(2);
}
@@ -615,8 +563,14 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
@Test
void testNameMatchRegistrar_found() {
login("2-RegistrarTest");
runSuccessfulNameTest(
"Yes Virginia <script>", "20", "Yes Virginia <script>", "rdap_registrar.json");
rememberWildcardType("Yes Virginia <script>");
assertAboutJson()
.that(generateActualJsonWithFullName("Yes Virginia <script>"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullRegistrar("20", "Yes Virginia <script>", "active", null)
.load("rdap_registrar.json")));
verifyMetrics(0);
}
@@ -624,8 +578,14 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testNameMatchRegistrar_found_subtypeAll() {
login("2-RegistrarTest");
action.subtypeParam = Optional.of("all");
runSuccessfulNameTest(
"Yes Virginia <script>", "20", "Yes Virginia <script>", "rdap_registrar.json");
rememberWildcardType("Yes Virginia <script>");
assertAboutJson()
.that(generateActualJsonWithFullName("Yes Virginia <script>"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullRegistrar("20", "Yes Virginia <script>", "active", null)
.load("rdap_registrar.json")));
verifyMetrics(0);
}
@@ -633,8 +593,14 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testNameMatchRegistrar_found_subtypeRegistrars() {
login("2-RegistrarTest");
action.subtypeParam = Optional.of("REGISTRARS");
runSuccessfulNameTest(
"Yes Virginia <script>", "20", "Yes Virginia <script>", "rdap_registrar.json");
rememberWildcardType("Yes Virginia <script>");
assertAboutJson()
.that(generateActualJsonWithFullName("Yes Virginia <script>"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullRegistrar("20", "Yes Virginia <script>", "active", null)
.load("rdap_registrar.json")));
verifyMetrics(0);
}
@@ -649,8 +615,14 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
@Test
void testNameMatchRegistrar_found_specifyingSameRegistrar() {
action.registrarParam = Optional.of("2-Registrar");
runSuccessfulNameTest(
"Yes Virginia <script>", "20", "Yes Virginia <script>", "rdap_registrar.json");
rememberWildcardType("Yes Virginia <script>");
assertAboutJson()
.that(generateActualJsonWithFullName("Yes Virginia <script>"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullRegistrar("20", "Yes Virginia <script>", "active", null)
.load("rdap_registrar.json")));
verifyMetrics(0);
}
@@ -666,10 +638,9 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
login("2-RegistrarTest");
createManyContactsAndRegistrars(4, 0, registrarTest);
rememberWildcardType("Entity *");
// JsonObject foo = generateActualJsonWithFullName("Entity *");
assertAboutJson()
.that(generateActualJsonWithFullName("Entity *"))
.isEqualTo(generateExpectedJson("rdap_nontruncated_contacts.json"));
.isEqualTo(jsonFileBuilder().load("rdap_nontruncated_contacts.json"));
assertThat(response.getStatus()).isEqualTo(200);
verifyMetrics(4);
}
@@ -682,8 +653,9 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
assertAboutJson()
.that(generateActualJsonWithFullName("Entity *"))
.isEqualTo(
generateExpectedJson(
"fn=Entity+*&cursor=YzpFbnRpdHkgNA%3D%3D", "rdap_truncated_contacts.json"));
jsonFileBuilder()
.put("NAME", "fn=Entity+*&cursor=YzpFbnRpdHkgNA%3D%3D")
.load("rdap_truncated_contacts.json"));
assertThat(response.getStatus()).isEqualTo(200);
verifyMetrics(5, IncompletenessWarningType.TRUNCATED);
}
@@ -696,8 +668,9 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
assertAboutJson()
.that(generateActualJsonWithFullName("Entity *"))
.isEqualTo(
generateExpectedJson(
"fn=Entity+*&cursor=YzpFbnRpdHkgNA%3D%3D", "rdap_truncated_contacts.json"));
jsonFileBuilder()
.put("NAME", "fn=Entity+*&cursor=YzpFbnRpdHkgNA%3D%3D")
.load("rdap_truncated_contacts.json"));
assertThat(response.getStatus()).isEqualTo(200);
// For contacts, we only need to fetch one result set's worth (plus one).
verifyMetrics(5, IncompletenessWarningType.TRUNCATED);
@@ -728,7 +701,7 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
rememberWildcardType("Entity *");
assertAboutJson()
.that(generateActualJsonWithFullName("Entity *"))
.isEqualTo(generateExpectedJson("rdap_nontruncated_registrars.json"));
.isEqualTo(jsonFileBuilder().load("rdap_nontruncated_registrars.json"));
assertThat(response.getStatus()).isEqualTo(200);
verifyMetrics(0);
}
@@ -740,8 +713,9 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
assertAboutJson()
.that(generateActualJsonWithFullName("Entity *"))
.isEqualTo(
generateExpectedJson(
"fn=Entity+*&cursor=cjpFbnRpdHkgNA%3D%3D", "rdap_truncated_registrars.json"));
jsonFileBuilder()
.put("NAME", "fn=Entity+*&cursor=cjpFbnRpdHkgNA%3D%3D")
.load("rdap_truncated_registrars.json"));
assertThat(response.getStatus()).isEqualTo(200);
verifyMetrics(0, IncompletenessWarningType.TRUNCATED);
}
@@ -753,8 +727,9 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
assertAboutJson()
.that(generateActualJsonWithFullName("Entity *"))
.isEqualTo(
generateExpectedJson(
"fn=Entity+*&cursor=cjpFbnRpdHkgNA%3D%3D", "rdap_truncated_registrars.json"));
jsonFileBuilder()
.put("NAME", "fn=Entity+*&cursor=cjpFbnRpdHkgNA%3D%3D")
.load("rdap_truncated_registrars.json"));
assertThat(response.getStatus()).isEqualTo(200);
verifyMetrics(0, IncompletenessWarningType.TRUNCATED);
}
@@ -815,8 +790,9 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
assertAboutJson()
.that(generateActualJsonWithFullName("Entity *"))
.isEqualTo(
generateExpectedJson(
"fn=Entity+*&cursor=cjpFbnRpdHkgNA%3D%3D", "rdap_truncated_mixed_entities.json"));
jsonFileBuilder()
.put("NAME", "fn=Entity+*&cursor=cjpFbnRpdHkgNA%3D%3D")
.load("rdap_truncated_mixed_entities.json"));
assertThat(response.getStatus()).isEqualTo(200);
verifyMetrics(3, IncompletenessWarningType.TRUNCATED);
}
@@ -845,7 +821,7 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
rememberWildcardType("Entity *");
assertAboutJson()
.that(generateActualJsonWithFullName("Entity *"))
.isEqualTo(generateExpectedJson("rdap_nontruncated_contacts.json"));
.isEqualTo(jsonFileBuilder().load("rdap_nontruncated_contacts.json"));
assertThat(response.getStatus()).isEqualTo(200);
verifyMetrics(4);
}
@@ -855,8 +831,14 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
login("2-RegistrarTest");
action.subtypeParam = Optional.of("registrars");
createManyContactsAndRegistrars(1, 1, registrarTest);
runSuccessfulNameTest(
"Entity *", "301", "Entity 2", "rdap_registrar.json");
rememberWildcardType("Entity *");
assertAboutJson()
.that(generateActualJsonWithFullName("Entity *"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullRegistrar("301", "Entity 2", "active", null)
.load("rdap_registrar.json")));
verifyMetrics(0);
}
@@ -878,7 +860,14 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testNameMatchRegistrar_found_inactiveAsSameRegistrar() {
action.includeDeletedParam = Optional.of(true);
login("2-RegistrarInact");
runSuccessfulNameTest("No Way", "21", "No Way", "inactive", null, "rdap_registrar.json");
rememberWildcardType("No Way");
assertAboutJson()
.that(generateActualJsonWithFullName("No Way"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullRegistrar("21", "No Way", "inactive", null)
.load("rdap_registrar.json")));
verifyMetrics(0);
}
@@ -886,7 +875,14 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testNameMatchRegistrar_found_inactiveAsAdmin() {
action.includeDeletedParam = Optional.of(true);
loginAsAdmin();
runSuccessfulNameTest("No Way", "21", "No Way", "inactive", null, "rdap_registrar.json");
rememberWildcardType("No Way");
assertAboutJson()
.that(generateActualJsonWithFullName("No Way"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullRegistrar("21", "No Way", "inactive", null)
.load("rdap_registrar.json")));
verifyMetrics(0);
}
@@ -908,8 +904,14 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testNameMatchRegistrar_found_testAsSameRegistrar() {
action.includeDeletedParam = Optional.of(true);
login("2-RegistrarTest");
runSuccessfulNameTest(
"Da Test Registrar", "not applicable", "Da Test Registrar", "rdap_registrar_test.json");
rememberWildcardType("Da Test Registrar");
assertAboutJson()
.that(generateActualJsonWithFullName("Da Test Registrar"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullRegistrar("not applicable", "Da Test Registrar", "active", null)
.load("rdap_registrar_test.json")));
verifyMetrics(0);
}
@@ -917,15 +919,28 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testNameMatchRegistrar_found_testAsAdmin() {
action.includeDeletedParam = Optional.of(true);
loginAsAdmin();
runSuccessfulNameTest(
"Da Test Registrar", "not applicable", "Da Test Registrar", "rdap_registrar_test.json");
rememberWildcardType("Da Test Registrar");
assertAboutJson()
.that(generateActualJsonWithFullName("Da Test Registrar"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullRegistrar("not applicable", "Da Test Registrar", "active", null)
.load("rdap_registrar_test.json")));
verifyMetrics(0);
}
@Test
void testHandleMatchContact_found() {
login("2-RegistrarTest");
runSuccessfulHandleTestWithBlinky("2-ROID", "rdap_contact.json");
rememberWildcardType("2-ROID");
assertAboutJson()
.that(generateActualJsonWithHandle("2-ROID"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullContact("2-ROID", "active", BINKY_FULL_NAME, BINKY_ADDRESS)
.load("rdap_contact.json")));
verifyMetrics(1);
}
@@ -933,7 +948,14 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testHandleMatchContact_found_subtypeAll() {
login("2-RegistrarTest");
action.subtypeParam = Optional.of("all");
runSuccessfulHandleTestWithBlinky("2-ROID", "rdap_contact.json");
rememberWildcardType("2-ROID");
assertAboutJson()
.that(generateActualJsonWithHandle("2-ROID"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullContact("2-ROID", "active", BINKY_FULL_NAME, BINKY_ADDRESS)
.load("rdap_contact.json")));
verifyMetrics(1);
}
@@ -941,7 +963,14 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testHandleMatchContact_found_subtypeContacts() {
login("2-RegistrarTest");
action.subtypeParam = Optional.of("contacts");
runSuccessfulHandleTestWithBlinky("2-ROID", "rdap_contact.json");
rememberWildcardType("2-ROID");
assertAboutJson()
.that(generateActualJsonWithHandle("2-ROID"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullContact("2-ROID", "active", BINKY_FULL_NAME, BINKY_ADDRESS)
.load("rdap_contact.json")));
verifyMetrics(1);
}
@@ -956,7 +985,12 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
@Test
void testHandleMatchContact_found_specifyingSameRegistrar() {
action.registrarParam = Optional.of("2-RegistrarTest");
runSuccessfulHandleTestWithBlinky("2-ROID", "rdap_contact_no_personal_data_with_remark.json");
rememberWildcardType("2-ROID");
assertAboutJson()
.that(generateActualJsonWithHandle("2-ROID"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder().load("rdap_contact_no_personal_data_with_remark.json")));
verifyMetrics(1);
}
@@ -986,13 +1020,12 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testHandleMatchContact_found_deletedWhenLoggedInAsSameRegistrar() {
login("2-Registrar");
action.includeDeletedParam = Optional.of(true);
runSuccessfulHandleTest(
"6-ROID",
"6-ROID",
"",
"inactive",
"",
"rdap_contact_deleted.json");
rememberWildcardType("6-ROID");
assertAboutJson()
.that(generateActualJsonWithHandle("6-ROID"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder().addContact("6-ROID").load("rdap_contact_deleted.json")));
verifyMetrics(1);
}
@@ -1000,13 +1033,12 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testHandleMatchContact_found_deletedWhenLoggedInAsAdmin() {
loginAsAdmin();
action.includeDeletedParam = Optional.of(true);
runSuccessfulHandleTest(
"6-ROID",
"6-ROID",
"",
"inactive",
"",
"rdap_contact_deleted.json");
rememberWildcardType("6-ROID");
assertAboutJson()
.that(generateActualJsonWithHandle("6-ROID"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder().addContact("6-ROID").load("rdap_contact_deleted.json")));
verifyMetrics(1);
}
@@ -1029,13 +1061,12 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testHandleMatchContact_found_deletedWildcardWhenLoggedInAsSameRegistrar() {
login("2-Registrar");
action.includeDeletedParam = Optional.of(true);
runSuccessfulHandleTest(
"6-ROI*",
"6-ROID",
"",
"inactive",
"",
"rdap_contact_deleted.json");
rememberWildcardType("6-ROI*");
assertAboutJson()
.that(generateActualJsonWithHandle("6-ROI*"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder().addContact("6-ROID").load("rdap_contact_deleted.json")));
verifyMetrics(1);
}
@@ -1043,33 +1074,53 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testHandleMatchContact_found_deletedWildcardWhenLoggedInAsAdmin() {
loginAsAdmin();
action.includeDeletedParam = Optional.of(true);
runSuccessfulHandleTest(
"6-ROI*",
"6-ROID",
"",
"inactive",
"",
"rdap_contact_deleted.json");
rememberWildcardType("6-ROI*");
assertAboutJson()
.that(generateActualJsonWithHandle("6-ROI*"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder().addContact("6-ROID").load("rdap_contact_deleted.json")));
verifyMetrics(1);
}
@Test
void testHandleMatchRegistrar_found() {
runSuccessfulHandleTest("20", "20", "Yes Virginia <script>", "rdap_registrar.json");
rememberWildcardType("20");
assertAboutJson()
.that(generateActualJsonWithHandle("20"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullRegistrar("20", "Yes Virginia <script>", "active", null)
.load("rdap_registrar.json")));
verifyMetrics(0);
}
@Test
void testHandleMatchRegistrar_found_subtypeAll() {
action.subtypeParam = Optional.of("all");
runSuccessfulHandleTest("20", "20", "Yes Virginia <script>", "rdap_registrar.json");
rememberWildcardType("20");
assertAboutJson()
.that(generateActualJsonWithHandle("20"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullRegistrar("20", "Yes Virginia <script>", "active", null)
.load("rdap_registrar.json")));
verifyMetrics(0);
}
@Test
void testHandleMatchRegistrar_found_subtypeRegistrars() {
action.subtypeParam = Optional.of("registrars");
runSuccessfulHandleTest("20", "20", "Yes Virginia <script>", "rdap_registrar.json");
rememberWildcardType("20");
assertAboutJson()
.that(generateActualJsonWithHandle("20"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullRegistrar("20", "Yes Virginia <script>", "active", null)
.load("rdap_registrar.json")));
verifyMetrics(0);
}
@@ -1083,7 +1134,14 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
@Test
void testHandleMatchRegistrar_found_specifyingSameRegistrar() {
action.registrarParam = Optional.of("2-Registrar");
runSuccessfulHandleTest("20", "20", "Yes Virginia <script>", "rdap_registrar.json");
rememberWildcardType("20");
assertAboutJson()
.that(generateActualJsonWithHandle("20"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullRegistrar("20", "Yes Virginia <script>", "active", null)
.load("rdap_registrar.json")));
verifyMetrics(0);
}
@@ -1098,14 +1156,28 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testHandleMatchContact_found_wildcardWithResultSetSizeOne() {
login("2-RegistrarTest");
action.rdapResultSetMaxSize = 1;
runSuccessfulHandleTestWithBlinky("2-R*", "rdap_contact.json");
rememberWildcardType("2-R*");
assertAboutJson()
.that(generateActualJsonWithHandle("2-R*"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullContact("2-ROID", "active", BINKY_FULL_NAME, BINKY_ADDRESS)
.load("rdap_contact.json")));
verifyMetrics(1);
}
@Test
void testHandleMatchContact_found_wildcard() {
login("2-RegistrarTest");
runSuccessfulHandleTestWithBlinky("2-RO*", "rdap_contact.json");
rememberWildcardType("2-R*");
assertAboutJson()
.that(generateActualJsonWithHandle("2-R*"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullContact("2-ROID", "active", BINKY_FULL_NAME, BINKY_ADDRESS)
.load("rdap_contact.json")));
verifyMetrics(1);
}
@@ -1113,7 +1185,14 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testHandleMatchContact_found_wildcardSpecifyingSameRegistrar() {
action.registrarParam = Optional.of("2-RegistrarTest");
login("2-RegistrarTest");
runSuccessfulHandleTestWithBlinky("2-RO*", "rdap_contact.json");
rememberWildcardType("2-RO*");
assertAboutJson()
.that(generateActualJsonWithHandle("2-RO*"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullContact("2-ROID", "active", BINKY_FULL_NAME, BINKY_ADDRESS)
.load("rdap_contact.json")));
verifyMetrics(1);
}
@@ -1128,7 +1207,14 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
@Test
void testHandleMatchContact_found_deleted() {
login("2-RegistrarTest");
runSuccessfulHandleTestWithBlinky("2-RO*", "rdap_contact.json");
rememberWildcardType("2-R*");
assertAboutJson()
.that(generateActualJsonWithHandle("2-R*"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullContact("2-ROID", "active", BINKY_FULL_NAME, BINKY_ADDRESS)
.load("rdap_contact.json")));
verifyMetrics(1);
}
@@ -1245,7 +1331,14 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testHandleMatchRegistrar_found_inactiveAsSameRegistrar() {
action.includeDeletedParam = Optional.of(true);
login("2-RegistrarInact");
runSuccessfulHandleTest("21", "21", "No Way", "inactive", null, "rdap_registrar.json");
rememberWildcardType("21");
assertAboutJson()
.that(generateActualJsonWithHandle("21"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullRegistrar("21", "No Way", "inactive", null)
.load("rdap_registrar.json")));
verifyMetrics(0);
}
@@ -1253,7 +1346,14 @@ class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEntitySear
void testHandleMatchRegistrar_found_inactiveAsAdmin() {
action.includeDeletedParam = Optional.of(true);
loginAsAdmin();
runSuccessfulHandleTest("21", "21", "No Way", "inactive", null, "rdap_registrar.json");
rememberWildcardType("21");
assertAboutJson()
.that(generateActualJsonWithHandle("21"))
.isEqualTo(
addBoilerplate(
jsonFileBuilder()
.addFullRegistrar("21", "No Way", "inactive", null)
.load("rdap_registrar.json")));
verifyMetrics(0);
}
}
@@ -48,13 +48,15 @@ class RdapHelpActionTest extends RdapActionBaseTestCase<RdapHelpAction> {
@Test
void testHelpActionDefault_getsIndex() {
assertThat(generateActualJson("")).isEqualTo(loadJsonFile("rdap_help_index.json"));
assertThat(generateActualJson(""))
.isEqualTo(loadJsonFile("rdap_help_index.json", "POSSIBLE_SLASH", ""));
assertThat(response.getStatus()).isEqualTo(200);
}
@Test
void testHelpActionSlash_getsIndex() {
assertThat(generateActualJson("/")).isEqualTo(loadJsonFile("rdap_help_index.json"));
assertThat(generateActualJson("/"))
.isEqualTo(loadJsonFile("rdap_help_index.json", "POSSIBLE_SLASH", "/"));
assertThat(response.getStatus()).isEqualTo(200);
}
@@ -462,12 +462,12 @@ class RdapJsonFormatterTest {
}
@Test
void testGetLastHistoryEntryByType() {
void testGetLastHistoryByType() {
// Expected data are from "rdapjson_domain_summary.json"
assertThat(
Maps.transformValues(
rdapJsonFormatter.getLastHistoryEntryByType(domainFull),
HistoryEntry::getModificationTime))
RdapJsonFormatter.getLastHistoryByType(domainFull),
RdapJsonFormatter.HistoryTimeAndRegistrar::modificationTime))
.containsExactlyEntriesIn(
ImmutableMap.of(TRANSFER, DateTime.parse("1999-12-01T00:00:00.000Z")));
}
@@ -15,15 +15,12 @@
package google.registry.rdap;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.rdap.RdapTestHelper.loadJsonFile;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrar;
import static google.registry.testing.GsonSubject.assertAboutJson;
import static org.mockito.Mockito.verify;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonObject;
import google.registry.model.registrar.Registrar;
import google.registry.rdap.RdapMetrics.EndpointType;
import google.registry.rdap.RdapMetrics.SearchType;
@@ -32,7 +29,6 @@ import google.registry.rdap.RdapSearchResults.IncompletenessWarningType;
import google.registry.request.Action;
import google.registry.testing.FullFieldsTestEntityHelper;
import java.util.Optional;
import javax.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -72,37 +68,6 @@ class RdapNameserverActionTest extends RdapActionBaseTestCase<RdapNameserverActi
"ns1.domain.external", "9.10.11.12", clock.nowUtc().minusYears(1));
}
private JsonObject generateExpectedJson(
String name,
@Nullable ImmutableMap<String, String> otherSubstitutions,
String expectedOutputFile) {
ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<>();
builder.put("TYPE", "nameserver");
builder.put("NAME", name);
boolean punycodeSet = false;
if (otherSubstitutions != null) {
builder.putAll(otherSubstitutions);
if (otherSubstitutions.containsKey("PUNYCODENAME")) {
punycodeSet = true;
}
}
if (!punycodeSet) {
builder.put("PUNYCODENAME", name);
}
return loadJsonFile(
expectedOutputFile,
builder.build());
}
private JsonObject generateExpectedJsonWithTopLevelEntries(
String name,
@Nullable ImmutableMap<String, String> otherSubstitutions,
String expectedOutputFile) {
JsonObject obj = generateExpectedJson(name, otherSubstitutions, expectedOutputFile);
RdapTestHelper.addNonDomainBoilerplateNotices(obj, "https://example.tld/rdap/");
return obj;
}
@Test
void testInvalidNameserver_returns400() {
assertAboutJson()
@@ -126,14 +91,11 @@ class RdapNameserverActionTest extends RdapActionBaseTestCase<RdapNameserverActi
assertAboutJson()
.that(generateActualJson("ns1.cat.lol"))
.isEqualTo(
generateExpectedJsonWithTopLevelEntries(
"ns1.cat.lol",
ImmutableMap.of(
"HANDLE", "2-ROID",
"ADDRESSTYPE", "v4",
"ADDRESS", "1.2.3.4",
"STATUS", "active"),
"rdap_host.json"));
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addNameserver("ns1.cat.lol", "2-ROID")
.putAll("ADDRESSTYPE", "v4", "ADDRESS", "1.2.3.4", "STATUS", "active")
.load("rdap_host.json")));
assertThat(response.getStatus()).isEqualTo(200);
}
@@ -142,14 +104,11 @@ class RdapNameserverActionTest extends RdapActionBaseTestCase<RdapNameserverActi
assertAboutJson()
.that(generateActualJson("ns1.cat.lol."))
.isEqualTo(
generateExpectedJsonWithTopLevelEntries(
"ns1.cat.lol",
ImmutableMap.of(
"HANDLE", "2-ROID",
"ADDRESSTYPE", "v4",
"ADDRESS", "1.2.3.4",
"STATUS", "active"),
"rdap_host.json"));
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addNameserver("ns1.cat.lol", "2-ROID")
.putAll("ADDRESSTYPE", "v4", "ADDRESS", "1.2.3.4", "STATUS", "active")
.load("rdap_host.json")));
assertThat(response.getStatus()).isEqualTo(200);
}
@@ -158,14 +117,11 @@ class RdapNameserverActionTest extends RdapActionBaseTestCase<RdapNameserverActi
assertAboutJson()
.that(generateActualJson("Ns1.CaT.lOl."))
.isEqualTo(
generateExpectedJsonWithTopLevelEntries(
"ns1.cat.lol",
ImmutableMap.of(
"HANDLE", "2-ROID",
"ADDRESSTYPE", "v4",
"ADDRESS", "1.2.3.4",
"STATUS", "active"),
"rdap_host.json"));
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addNameserver("ns1.cat.lol", "2-ROID")
.putAll("ADDRESSTYPE", "v4", "ADDRESS", "1.2.3.4", "STATUS", "active")
.load("rdap_host.json")));
assertThat(response.getStatus()).isEqualTo(200);
}
@@ -174,14 +130,11 @@ class RdapNameserverActionTest extends RdapActionBaseTestCase<RdapNameserverActi
assertAboutJson()
.that(generateActualJson("ns1.cat.lol?key=value"))
.isEqualTo(
generateExpectedJsonWithTopLevelEntries(
"ns1.cat.lol",
ImmutableMap.of(
"HANDLE", "2-ROID",
"ADDRESSTYPE", "v4",
"ADDRESS", "1.2.3.4",
"STATUS", "active"),
"rdap_host.json"));
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addNameserver("ns1.cat.lol", "2-ROID")
.putAll("ADDRESSTYPE", "v4", "ADDRESS", "1.2.3.4", "STATUS", "active")
.load("rdap_host.json")));
assertThat(response.getStatus()).isEqualTo(200);
}
@@ -190,15 +143,11 @@ class RdapNameserverActionTest extends RdapActionBaseTestCase<RdapNameserverActi
assertAboutJson()
.that(generateActualJson("ns1.cat.みんな"))
.isEqualTo(
generateExpectedJsonWithTopLevelEntries(
"ns1.cat.みんな",
ImmutableMap.of(
"PUNYCODENAME", "ns1.cat.xn--q9jyb4c",
"HANDLE", "5-ROID",
"ADDRESSTYPE", "v6",
"ADDRESS", "bad:f00d:cafe::15:beef",
"STATUS", "active"),
"rdap_host_unicode.json"));
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addNameserver("ns1.cat.みんな", "5-ROID")
.putAll("ADDRESSTYPE", "v6", "ADDRESS", "bad:f00d:cafe::15:beef")
.load("rdap_host_unicode.json")));
assertThat(response.getStatus()).isEqualTo(200);
}
@@ -207,15 +156,11 @@ class RdapNameserverActionTest extends RdapActionBaseTestCase<RdapNameserverActi
assertAboutJson()
.that(generateActualJson("ns1.cat.xn--q9jyb4c"))
.isEqualTo(
generateExpectedJsonWithTopLevelEntries(
"ns1.cat.みんな",
ImmutableMap.of(
"PUNYCODENAME", "ns1.cat.xn--q9jyb4c",
"HANDLE", "5-ROID",
"ADDRESSTYPE", "v6",
"ADDRESS", "bad:f00d:cafe::15:beef",
"STATUS", "active"),
"rdap_host_unicode.json"));
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addNameserver("ns1.cat.みんな", "5-ROID")
.putAll("ADDRESSTYPE", "v6", "ADDRESS", "bad:f00d:cafe::15:beef")
.load("rdap_host_unicode.json")));
assertThat(response.getStatus()).isEqualTo(200);
}
@@ -224,14 +169,11 @@ class RdapNameserverActionTest extends RdapActionBaseTestCase<RdapNameserverActi
assertAboutJson()
.that(generateActualJson("ns1.domain.1.tld"))
.isEqualTo(
generateExpectedJsonWithTopLevelEntries(
"ns1.domain.1.tld",
ImmutableMap.of(
"HANDLE", "8-ROID",
"ADDRESSTYPE", "v4",
"ADDRESS", "5.6.7.8",
"STATUS", "active"),
"rdap_host.json"));
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addNameserver("ns1.domain.1.tld", "8-ROID")
.putAll("ADDRESSTYPE", "v4", "ADDRESS", "5.6.7.8", "STATUS", "active")
.load("rdap_host.json")));
assertThat(response.getStatus()).isEqualTo(200);
}
@@ -240,14 +182,11 @@ class RdapNameserverActionTest extends RdapActionBaseTestCase<RdapNameserverActi
assertAboutJson()
.that(generateActualJson("ns1.domain.external"))
.isEqualTo(
generateExpectedJsonWithTopLevelEntries(
"ns1.domain.external",
ImmutableMap.of(
"HANDLE", "C-ROID",
"ADDRESSTYPE", "v4",
"ADDRESS", "9.10.11.12",
"STATUS", "active"),
"rdap_host.json"));
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addNameserver("ns1.domain.external", "C-ROID")
.putAll("ADDRESSTYPE", "v4", "ADDRESS", "9.10.11.12", "STATUS", "active")
.load("rdap_host.json")));
assertThat(response.getStatus()).isEqualTo(200);
}
@@ -287,14 +226,11 @@ class RdapNameserverActionTest extends RdapActionBaseTestCase<RdapNameserverActi
assertAboutJson()
.that(generateActualJson("nsdeleted.cat.lol"))
.isEqualTo(
generateExpectedJsonWithTopLevelEntries(
"nsdeleted.cat.lol",
ImmutableMap.of(
"HANDLE", "A-ROID",
"ADDRESSTYPE", "v4",
"ADDRESS", "1.2.3.4",
"STATUS", "inactive"),
"rdap_host.json"));
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addNameserver("nsdeleted.cat.lol", "A-ROID")
.putAll("ADDRESSTYPE", "v4", "ADDRESS", "1.2.3.4", "STATUS", "inactive")
.load("rdap_host.json")));
assertThat(response.getStatus()).isEqualTo(200);
}
@@ -305,14 +241,11 @@ class RdapNameserverActionTest extends RdapActionBaseTestCase<RdapNameserverActi
assertAboutJson()
.that(generateActualJson("nsdeleted.cat.lol"))
.isEqualTo(
generateExpectedJsonWithTopLevelEntries(
"nsdeleted.cat.lol",
ImmutableMap.of(
"HANDLE", "A-ROID",
"ADDRESSTYPE", "v4",
"ADDRESS", "1.2.3.4",
"STATUS", "inactive"),
"rdap_host.json"));
addPermanentBoilerplateNotices(
jsonFileBuilder()
.addNameserver("nsdeleted.cat.lol", "A-ROID")
.putAll("ADDRESSTYPE", "v4", "ADDRESS", "1.2.3.4", "STATUS", "inactive")
.load("rdap_host.json")));
assertThat(response.getStatus()).isEqualTo(200);
}

Some files were not shown because too many files have changed in this diff Show More