mirror of
https://github.com/google/nomulus
synced 2026-06-09 16:33:02 +00:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6868b771b | |||
| f34aec8b56 | |||
| b27b077638 | |||
| 0e8cd75a58 | |||
| 2a1748ba9c | |||
| f4889191a4 | |||
| 9eddecf70f | |||
| d4bcff0c31 | |||
| 62065f88fb | |||
| c9ac9437fd | |||
| 1f6a09182d | |||
| a0eff00031 | |||
| 89698c6ed6 | |||
| a7696c3fac | |||
| 7ec599f849 | |||
| 70291af9ad | |||
| 5fb95f38ed | |||
| dfe8e24761 | |||
| bd30fcc81c | |||
| 8cecc8d3a8 | |||
| c5a39bccc5 | |||
| a90a117341 | |||
| b40ad54daf | |||
| b4d239c329 | |||
| daa7ab3bfa | |||
| 56cd2ad282 | |||
| 0472dda860 |
@@ -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();
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
+24
-34
@@ -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);
|
||||
|
||||
+180
-192
@@ -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 {
|
||||
|
||||
-4
@@ -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 IANA’s
|
||||
* 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
|
||||
|
||||
+27
-7
@@ -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) {
|
||||
|
||||
+36
@@ -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");
|
||||
|
||||
+264
-267
@@ -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 =
|
||||
|
||||
+2
@@ -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
Reference in New Issue
Block a user