mirror of
https://github.com/google/nomulus
synced 2026-07-26 18:12:38 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55d5f8c6f8 | ||
|
|
9006312253 | ||
|
|
e5e2370923 |
@@ -49,15 +49,14 @@ import { SettingsWidgetComponent } from './home/widgets/settingsWidget.component
|
||||
import { UserDataService } from './shared/services/userData.service';
|
||||
import WhoisComponent from './settings/whois/whois.component';
|
||||
import { SnackBarModule } from './snackbar.module';
|
||||
import {
|
||||
RegistrarDetailsComponent,
|
||||
RegistrarDetailsWrapperComponent,
|
||||
} from './registrar/registrarDetails.component';
|
||||
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
|
||||
import { DomainListComponent } from './domains/domainList.component';
|
||||
import { DialogBottomSheetWrapper } from './shared/components/dialogBottomSheet.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
DialogBottomSheetWrapper,
|
||||
BillingWidgetComponent,
|
||||
ContactDetailsDialogComponent,
|
||||
ContactWidgetComponent,
|
||||
@@ -70,7 +69,6 @@ import { DomainListComponent } from './domains/domainList.component';
|
||||
PromotionsWidgetComponent,
|
||||
RegistrarComponent,
|
||||
RegistrarDetailsComponent,
|
||||
RegistrarDetailsWrapperComponent,
|
||||
RegistrarSelectorComponent,
|
||||
ResourcesWidgetComponent,
|
||||
SecurityComponent,
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<div class="console-domains">
|
||||
<mat-form-field>
|
||||
<mat-label>Filter</mat-label>
|
||||
<input matInput (keyup)="applyFilter($event)" #input />
|
||||
<input
|
||||
type="search"
|
||||
matInput
|
||||
[(ngModel)]="searchTerm"
|
||||
(ngModelChange)="sendInput()"
|
||||
#input
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<div *ngIf="isLoading; else domains_content" class="console-domains__loading">
|
||||
|
||||
@@ -18,6 +18,7 @@ import { BackendService } from '../shared/services/backend.service';
|
||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { Domain, DomainListService } from './domainList.service';
|
||||
import { Subject, debounceTime } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-domain-list',
|
||||
@@ -27,6 +28,7 @@ import { Domain, DomainListService } from './domainList.service';
|
||||
})
|
||||
export class DomainListComponent {
|
||||
public static PATH = 'domain-list';
|
||||
private readonly DEBOUNCE_MS = 500;
|
||||
|
||||
displayedColumns: string[] = [
|
||||
'domainName',
|
||||
@@ -38,6 +40,9 @@ export class DomainListComponent {
|
||||
dataSource: MatTableDataSource<Domain> = new MatTableDataSource();
|
||||
isLoading = true;
|
||||
|
||||
searchTermSubject = new Subject<string>();
|
||||
searchTerm?: string;
|
||||
|
||||
pageNumber?: number;
|
||||
resultsPerPage = 50;
|
||||
totalResults?: number;
|
||||
@@ -52,13 +57,28 @@ export class DomainListComponent {
|
||||
|
||||
ngOnInit() {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
// Don't spam the server unnecessarily while the user is typing
|
||||
this.searchTermSubject
|
||||
.pipe(debounceTime(this.DEBOUNCE_MS))
|
||||
.subscribe((searchTermValue) => {
|
||||
this.reloadData();
|
||||
});
|
||||
this.reloadData();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.searchTermSubject.complete();
|
||||
}
|
||||
|
||||
reloadData() {
|
||||
this.isLoading = true;
|
||||
this.domainListService
|
||||
.retrieveDomains(this.pageNumber, this.resultsPerPage, this.totalResults)
|
||||
.retrieveDomains(
|
||||
this.pageNumber,
|
||||
this.resultsPerPage,
|
||||
this.totalResults,
|
||||
this.searchTerm
|
||||
)
|
||||
.subscribe((domainListResult) => {
|
||||
this.dataSource.data = domainListResult.domains;
|
||||
this.totalResults = domainListResult.totalResults;
|
||||
@@ -66,10 +86,8 @@ export class DomainListComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/** TODO: the backend will need to accept a filter string. */
|
||||
applyFilter(event: KeyboardEvent) {
|
||||
// const filterValue = (event.target as HTMLInputElement).value;
|
||||
this.reloadData();
|
||||
sendInput() {
|
||||
this.searchTermSubject.next(this.searchTerm!);
|
||||
}
|
||||
|
||||
onPageChange(event: PageEvent) {
|
||||
|
||||
@@ -47,7 +47,8 @@ export class DomainListService {
|
||||
retrieveDomains(
|
||||
pageNumber?: number,
|
||||
resultsPerPage?: number,
|
||||
totalResults?: number
|
||||
totalResults?: number,
|
||||
searchTerm?: string
|
||||
) {
|
||||
return this.backendService
|
||||
.getDomains(
|
||||
@@ -55,7 +56,8 @@ export class DomainListService {
|
||||
this.checkpointTime,
|
||||
pageNumber,
|
||||
resultsPerPage,
|
||||
totalResults
|
||||
totalResults,
|
||||
searchTerm
|
||||
)
|
||||
.pipe(
|
||||
tap((domainListResult: DomainListResult) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="registrarDetails">
|
||||
<div class="registrarDetails" *ngIf="registrarInEdit">
|
||||
<h3 mat-dialog-title>Edit Registrar: {{ registrarInEdit.registrarId }}</h3>
|
||||
<div mat-dialog-content>
|
||||
<form (ngSubmit)="saveAndClose($event)">
|
||||
<form (ngSubmit)="saveAndClose()">
|
||||
<mat-form-field class="registrarDetails__input">
|
||||
<mat-label>Registry Lock:</mat-label>
|
||||
<mat-select
|
||||
@@ -32,7 +32,7 @@
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="onCancel($event)">Cancel</button>
|
||||
<button mat-button (click)="this.params?.close()">Cancel</button>
|
||||
<button type="submit" mat-button color="primary">Save</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
|
||||
@@ -12,61 +12,38 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Injector } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { Registrar, RegistrarService } from './registrar.service';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import {
|
||||
MAT_BOTTOM_SHEET_DATA,
|
||||
MatBottomSheet,
|
||||
MatBottomSheetRef,
|
||||
} from '@angular/material/bottom-sheet';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialog,
|
||||
MatDialogRef,
|
||||
} from '@angular/material/dialog';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { DialogBottomSheetContent } from '../shared/components/dialogBottomSheet.component';
|
||||
|
||||
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
|
||||
type RegistrarDetailsParams = {
|
||||
close: Function;
|
||||
data: {
|
||||
registrar: Registrar;
|
||||
};
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-registrar-details',
|
||||
templateUrl: './registrarDetails.component.html',
|
||||
styleUrls: ['./registrarDetails.component.scss'],
|
||||
})
|
||||
export class RegistrarDetailsComponent {
|
||||
export class RegistrarDetailsComponent implements DialogBottomSheetContent {
|
||||
registrarInEdit!: Registrar;
|
||||
private elementRef:
|
||||
| MatBottomSheetRef<RegistrarDetailsComponent>
|
||||
| MatDialogRef<RegistrarDetailsComponent>;
|
||||
params?: RegistrarDetailsParams;
|
||||
|
||||
constructor(
|
||||
protected registrarService: RegistrarService,
|
||||
private injector: Injector
|
||||
) {
|
||||
// We only inject one, either Dialog or Bottom Sheet data
|
||||
// so one of the injectors is expected to fail
|
||||
try {
|
||||
var params = this.injector.get(MAT_DIALOG_DATA);
|
||||
this.elementRef = this.injector.get(MatDialogRef);
|
||||
} catch (e) {
|
||||
var params = this.injector.get(MAT_BOTTOM_SHEET_DATA);
|
||||
this.elementRef = this.injector.get(MatBottomSheetRef);
|
||||
}
|
||||
this.registrarInEdit = JSON.parse(JSON.stringify(params.registrar));
|
||||
constructor(protected registrarService: RegistrarService) {}
|
||||
|
||||
init(params: RegistrarDetailsParams) {
|
||||
this.params = params;
|
||||
this.registrarInEdit = JSON.parse(
|
||||
JSON.stringify(this.params.data.registrar)
|
||||
);
|
||||
}
|
||||
|
||||
onCancel(e: MouseEvent) {
|
||||
if (this.elementRef instanceof MatBottomSheetRef) {
|
||||
this.elementRef.dismiss();
|
||||
} else if (this.elementRef instanceof MatDialogRef) {
|
||||
this.elementRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
saveAndClose(e: MouseEvent) {
|
||||
// TODO: Implement save call to API
|
||||
this.onCancel(e);
|
||||
saveAndClose() {
|
||||
this.params?.close();
|
||||
}
|
||||
|
||||
addTLD(e: MatChipInputEvent) {
|
||||
@@ -82,24 +59,3 @@ export class RegistrarDetailsComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-registrar-details-wrapper',
|
||||
template: '',
|
||||
})
|
||||
export class RegistrarDetailsWrapperComponent {
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
private bottomSheet: MatBottomSheet,
|
||||
protected breakpointObserver: BreakpointObserver
|
||||
) {}
|
||||
|
||||
open(registrar: Registrar) {
|
||||
const config = { data: { registrar } };
|
||||
if (this.breakpointObserver.isMatched(MOBILE_LAYOUT_BREAKPOINT)) {
|
||||
this.bottomSheet.open(RegistrarDetailsComponent, config);
|
||||
} else {
|
||||
this.dialog.open(RegistrarDetailsComponent, config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
[pageSizeOptions]="[5, 10, 20]"
|
||||
showFirstLastButtons
|
||||
></mat-paginator>
|
||||
<app-registrar-details-wrapper
|
||||
<app-dialog-bottom-sheet-wrapper
|
||||
#registrarDetailsView
|
||||
></app-registrar-details-wrapper>
|
||||
></app-dialog-bottom-sheet-wrapper>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,8 @@ import { Registrar, RegistrarService } from './registrar.service';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { RegistrarDetailsWrapperComponent } from './registrarDetails.component';
|
||||
import { RegistrarDetailsComponent } from './registrarDetails.component';
|
||||
import { DialogBottomSheetWrapper } from '../shared/components/dialogBottomSheet.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-registrar',
|
||||
@@ -82,7 +83,7 @@ export class RegistrarComponent {
|
||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
@ViewChild('registrarDetailsView')
|
||||
detailsComponentWrapper!: RegistrarDetailsWrapperComponent;
|
||||
detailsComponentWrapper!: DialogBottomSheetWrapper;
|
||||
|
||||
constructor(protected registrarService: RegistrarService) {
|
||||
this.dataSource = new MatTableDataSource<Registrar>(
|
||||
@@ -97,7 +98,10 @@ export class RegistrarComponent {
|
||||
|
||||
openDetails(event: MouseEvent, registrar: Registrar) {
|
||||
event.stopPropagation();
|
||||
this.detailsComponentWrapper.open(registrar);
|
||||
this.detailsComponentWrapper.open<RegistrarDetailsComponent>(
|
||||
RegistrarDetailsComponent,
|
||||
{ registrar }
|
||||
);
|
||||
}
|
||||
|
||||
applyFilter(event: Event) {
|
||||
|
||||
@@ -41,4 +41,7 @@
|
||||
<mat-icon>add</mat-icon>Create a Contact
|
||||
</button>
|
||||
</div>
|
||||
<app-dialog-bottom-sheet-wrapper
|
||||
#contactDetailsWrapper
|
||||
></app-dialog-bottom-sheet-wrapper>
|
||||
</div>
|
||||
|
||||
@@ -12,21 +12,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import {
|
||||
MatDialog,
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogRef,
|
||||
} from '@angular/material/dialog';
|
||||
import {
|
||||
MatBottomSheet,
|
||||
MAT_BOTTOM_SHEET_DATA,
|
||||
MatBottomSheetRef,
|
||||
} from '@angular/material/bottom-sheet';
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { Contact, ContactService } from './contact.service';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import {
|
||||
DialogBottomSheetContent,
|
||||
DialogBottomSheetWrapper,
|
||||
} from 'src/app/shared/components/dialogBottomSheet.component';
|
||||
|
||||
enum Operations {
|
||||
DELETE,
|
||||
@@ -40,7 +33,13 @@ interface GroupedContacts {
|
||||
contacts: Array<Contact>;
|
||||
}
|
||||
|
||||
let isMobile: boolean;
|
||||
type ContactDetailsParams = {
|
||||
close: Function;
|
||||
data: {
|
||||
contact: Contact;
|
||||
operation: Operations;
|
||||
};
|
||||
};
|
||||
|
||||
const contactTypes: Array<GroupedContacts> = [
|
||||
{ value: 'ADMIN', label: 'Primary contact', contacts: [] },
|
||||
@@ -52,72 +51,46 @@ const contactTypes: Array<GroupedContacts> = [
|
||||
{ value: 'WHOIS', label: 'WHOIS-Inquiry contact', contacts: [] },
|
||||
];
|
||||
|
||||
class ContactDetailsEventsResponder {
|
||||
private ref?: MatDialogRef<any> | MatBottomSheetRef;
|
||||
constructor() {
|
||||
this.onClose = this.onClose.bind(this);
|
||||
}
|
||||
|
||||
setRef(ref: MatDialogRef<any> | MatBottomSheetRef) {
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
onClose() {
|
||||
if (this.ref == undefined) {
|
||||
throw "Reference to ContactDetailsDialogComponent hasn't been set. ";
|
||||
}
|
||||
if (this.ref instanceof MatBottomSheetRef) {
|
||||
this.ref.dismiss();
|
||||
} else if (this.ref instanceof MatDialogRef) {
|
||||
this.ref.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-contact-details-dialog',
|
||||
templateUrl: 'contactDetails.component.html',
|
||||
styleUrls: ['./contact.component.scss'],
|
||||
})
|
||||
export class ContactDetailsDialogComponent {
|
||||
contact: Contact;
|
||||
export class ContactDetailsDialogComponent implements DialogBottomSheetContent {
|
||||
contact?: Contact;
|
||||
contactTypes = contactTypes;
|
||||
operation: Operations;
|
||||
contactIndex: number;
|
||||
onCloseCallback: Function;
|
||||
contactIndex?: number;
|
||||
|
||||
params?: ContactDetailsParams;
|
||||
|
||||
constructor(
|
||||
public contactService: ContactService,
|
||||
private _snackBar: MatSnackBar,
|
||||
@Inject(isMobile ? MAT_BOTTOM_SHEET_DATA : MAT_DIALOG_DATA)
|
||||
public data: {
|
||||
onClose: Function;
|
||||
contact: Contact;
|
||||
operation: Operations;
|
||||
}
|
||||
) {
|
||||
this.onCloseCallback = data.onClose;
|
||||
this.contactIndex = contactService.contacts.findIndex(
|
||||
(c) => c === data.contact
|
||||
private _snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
init(params: ContactDetailsParams) {
|
||||
this.params = params;
|
||||
this.contactIndex = this.contactService.contacts.findIndex(
|
||||
(c) => c === params.data.contact
|
||||
);
|
||||
this.contact = JSON.parse(JSON.stringify(data.contact));
|
||||
this.operation = data.operation;
|
||||
this.contact = JSON.parse(JSON.stringify(params.data.contact));
|
||||
}
|
||||
|
||||
onClose(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
this.onCloseCallback.call(this);
|
||||
close() {
|
||||
this.params?.close();
|
||||
}
|
||||
|
||||
saveAndClose(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
if (!this.contact || this.contactIndex === undefined) return;
|
||||
if (!(e.target as HTMLFormElement).checkValidity()) {
|
||||
return;
|
||||
}
|
||||
const operation = this.params?.data.operation;
|
||||
let operationObservable;
|
||||
if (this.operation === Operations.ADD) {
|
||||
if (operation === Operations.ADD) {
|
||||
operationObservable = this.contactService.addContact(this.contact);
|
||||
} else if (this.operation === Operations.UPDATE) {
|
||||
} else if (operation === Operations.UPDATE) {
|
||||
operationObservable = this.contactService.updateContact(
|
||||
this.contactIndex,
|
||||
this.contact
|
||||
@@ -127,7 +100,7 @@ export class ContactDetailsDialogComponent {
|
||||
}
|
||||
|
||||
operationObservable.subscribe({
|
||||
complete: this.onCloseCallback.bind(this),
|
||||
complete: () => this.close(),
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error);
|
||||
},
|
||||
@@ -143,11 +116,11 @@ export class ContactDetailsDialogComponent {
|
||||
export default class ContactComponent {
|
||||
public static PATH = 'contact';
|
||||
|
||||
@ViewChild('contactDetailsWrapper')
|
||||
detailsComponentWrapper!: DialogBottomSheetWrapper;
|
||||
|
||||
loading: boolean = false;
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
private bottomSheet: MatBottomSheet,
|
||||
private breakpointObserver: BreakpointObserver,
|
||||
public contactService: ContactService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {
|
||||
@@ -195,20 +168,9 @@ export default class ContactComponent {
|
||||
operation: Operations = Operations.UPDATE
|
||||
) {
|
||||
e.preventDefault();
|
||||
// TODO: handle orientation change
|
||||
isMobile = this.breakpointObserver.isMatched('(max-width: 599px)');
|
||||
const responder = new ContactDetailsEventsResponder();
|
||||
const config = { data: { onClose: responder.onClose, contact, operation } };
|
||||
|
||||
if (isMobile) {
|
||||
const bottomSheetRef = this.bottomSheet.open(
|
||||
ContactDetailsDialogComponent,
|
||||
config
|
||||
);
|
||||
responder.setRef(bottomSheetRef);
|
||||
} else {
|
||||
const dialogRef = this.dialog.open(ContactDetailsDialogComponent, config);
|
||||
responder.setRef(dialogRef);
|
||||
}
|
||||
this.detailsComponentWrapper.open<ContactDetailsDialogComponent>(
|
||||
ContactDetailsDialogComponent,
|
||||
{ contact, operation }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export class ContactService {
|
||||
return this.backend
|
||||
.getContacts(this.registrarService.activeRegistrarId)
|
||||
.pipe(
|
||||
tap((contacts) => {
|
||||
tap((contacts = []) => {
|
||||
this.contacts = contacts;
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<h3 mat-dialog-title>Contact details</h3>
|
||||
<div mat-dialog-content>
|
||||
<div mat-dialog-content *ngIf="contact">
|
||||
<form (ngSubmit)="saveAndClose($event)">
|
||||
<p>
|
||||
<mat-form-field class="contact-details__input">
|
||||
@@ -97,7 +97,7 @@
|
||||
>
|
||||
</section>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="onClose($event)">Cancel</button>
|
||||
<button mat-button (click)="close()">Cancel</button>
|
||||
<button type="submit" mat-button>Save</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { ComponentType } from '@angular/cdk/portal';
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
MatBottomSheet,
|
||||
MatBottomSheetRef,
|
||||
} from '@angular/material/bottom-sheet';
|
||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
|
||||
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
|
||||
|
||||
export interface DialogBottomSheetContent {
|
||||
init(data: Object): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps up a child component in an Angular Material Dalog for desktop or a Bottom Sheet
|
||||
* component for mobile depending on a screen resolution, with Breaking Point being 599px.
|
||||
* Child component is required to implement @see DialogBottomSheetContent interface
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-dialog-bottom-sheet-wrapper',
|
||||
template: '',
|
||||
})
|
||||
export class DialogBottomSheetWrapper {
|
||||
private elementRef?: MatBottomSheetRef | MatDialogRef<any>;
|
||||
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
private bottomSheet: MatBottomSheet,
|
||||
protected breakpointObserver: BreakpointObserver
|
||||
) {}
|
||||
|
||||
open<T extends DialogBottomSheetContent>(
|
||||
component: ComponentType<T>,
|
||||
data: any
|
||||
) {
|
||||
const config = { data, close: () => this.close() };
|
||||
if (this.breakpointObserver.isMatched(MOBILE_LAYOUT_BREAKPOINT)) {
|
||||
this.elementRef = this.bottomSheet.open(component);
|
||||
this.elementRef.instance.init(config);
|
||||
} else {
|
||||
this.elementRef = this.dialog.open(component);
|
||||
this.elementRef.componentInstance.init(config);
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.elementRef instanceof MatBottomSheetRef) {
|
||||
this.elementRef.dismiss();
|
||||
} else if (this.elementRef instanceof MatDialogRef) {
|
||||
this.elementRef.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,8 @@ export class BackendService {
|
||||
checkpointTime?: string,
|
||||
pageNumber?: number,
|
||||
resultsPerPage?: number,
|
||||
totalResults?: number
|
||||
totalResults?: number,
|
||||
searchTerm?: string
|
||||
): Observable<DomainListResult> {
|
||||
var url = `/console-api/domain-list?registrarId=${registrarId}`;
|
||||
if (checkpointTime) {
|
||||
@@ -84,6 +85,9 @@ export class BackendService {
|
||||
if (totalResults) {
|
||||
url += `&totalResults=${totalResults}`;
|
||||
}
|
||||
if (searchTerm) {
|
||||
url += `&searchTerm=${searchTerm}`;
|
||||
}
|
||||
return this.http
|
||||
.get<DomainListResult>(url)
|
||||
.pipe(catchError((err) => this.errorCatcher<DomainListResult>(err)));
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.joda.time.DateTime;
|
||||
* <p>The label is valid (wrt IDN) in at least one TLD.
|
||||
*/
|
||||
@Entity
|
||||
public final class BsaLabel {
|
||||
final class BsaLabel {
|
||||
|
||||
@Id String label;
|
||||
|
||||
@@ -52,7 +52,7 @@ public final class BsaLabel {
|
||||
}
|
||||
|
||||
/** Returns the label to be blocked. */
|
||||
public String getLabel() {
|
||||
String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.bsa.persistence;
|
||||
|
||||
import static google.registry.config.RegistryConfig.getEppResourceCachingDuration;
|
||||
import static google.registry.config.RegistryConfig.getEppResourceMaxCachedEntries;
|
||||
import static google.registry.model.CacheUtils.newCacheBuilder;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.CacheLoader;
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Helpers for {@link BsaLabel}. */
|
||||
public final class BsaLabelUtils {
|
||||
|
||||
private BsaLabelUtils() {}
|
||||
|
||||
static final CacheLoader<VKey<BsaLabel>, Optional<BsaLabel>> CACHE_LOADER =
|
||||
new CacheLoader<VKey<BsaLabel>, Optional<BsaLabel>>() {
|
||||
|
||||
@Override
|
||||
public Optional<BsaLabel> load(VKey<BsaLabel> key) {
|
||||
return replicaTm().reTransact(() -> replicaTm().loadByKeyIfPresent(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<VKey<BsaLabel>, Optional<BsaLabel>> loadAll(
|
||||
Iterable<? extends VKey<BsaLabel>> keys) {
|
||||
// TODO(b/309173359): need this for DomainCheckFlow
|
||||
throw new UnsupportedOperationException(
|
||||
"LoadAll not supported by the BsaLabel cache loader.");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A limited size, limited expiry cache of BSA labels.
|
||||
*
|
||||
* <p>BSA labels are used by the domain creation flow to verify that the requested domain name is
|
||||
* not blocked by the BSA program. Label caching is mainly a defense against two scenarios, the
|
||||
* initial rush and drop-catching, when clients run back-to-back domain creation requests around
|
||||
* the time when a domain becomes available.
|
||||
*
|
||||
* <p>Because of caching and the use of the replica database, new BSA labels installed in the
|
||||
* database will not take effect immediately. A blocked domain may be created due to race
|
||||
* condition. A `refresh` job will detect such domains and report them to BSA as unblockable
|
||||
* domains.
|
||||
*
|
||||
* <p>Since the cached BSA labels have the same usage pattern as the cached EppResources, the
|
||||
* cache configuration for the latter are reused here.
|
||||
*/
|
||||
private static LoadingCache<VKey<BsaLabel>, Optional<BsaLabel>> cacheBsaLabels =
|
||||
createBsaLabelsCache(getEppResourceCachingDuration());
|
||||
|
||||
private static LoadingCache<VKey<BsaLabel>, Optional<BsaLabel>> createBsaLabelsCache(
|
||||
Duration expiry) {
|
||||
return newCacheBuilder(expiry)
|
||||
.maximumSize(getEppResourceMaxCachedEntries())
|
||||
.build(CACHE_LOADER);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void clearCache() {
|
||||
cacheBsaLabels.invalidateAll();
|
||||
}
|
||||
|
||||
/** Checks if the {@code domainLabel} (the leading `part` of a domain name) is blocked by BSA. */
|
||||
public static boolean isLabelBlocked(String domainLabel) {
|
||||
return cacheBsaLabels.get(BsaLabel.vKey(domainLabel)).isPresent();
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import static google.registry.flows.domain.DomainFlowUtils.verifyClaimsNoticeIfA
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyClaimsPeriodNotEnded;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyLaunchPhaseMatchesRegistryPhase;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyNoCodeMarks;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyNotBlockedByBsa;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyNotReserved;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyPremiumNameIsNotBlocked;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyRegistrarIsActive;
|
||||
@@ -168,6 +169,7 @@ import org.joda.time.Duration;
|
||||
* @error {@link DomainFlowUtils.CurrencyUnitMismatchException}
|
||||
* @error {@link DomainFlowUtils.CurrencyValueScaleException}
|
||||
* @error {@link DomainFlowUtils.DashesInThirdAndFourthException}
|
||||
* @error {@link DomainFlowUtils.DomainLabelBlockedByBsaException}
|
||||
* @error {@link DomainFlowUtils.DomainLabelTooLongException}
|
||||
* @error {@link DomainFlowUtils.DomainReservedException}
|
||||
* @error {@link DomainFlowUtils.DuplicateContactForRoleException}
|
||||
@@ -328,6 +330,7 @@ public final class DomainCreateFlow implements MutatingFlow {
|
||||
.verifySignedMarks(launchCreate.get().getSignedMarks(), domainLabel, now)
|
||||
.getId();
|
||||
}
|
||||
verifyNotBlockedByBsa(domainLabel, tld, now);
|
||||
flowCustomLogic.afterValidation(
|
||||
DomainCreateFlowCustomLogic.AfterValidationParameters.newBuilder()
|
||||
.setDomainName(domainName)
|
||||
|
||||
@@ -25,11 +25,13 @@ import static com.google.common.collect.Iterables.any;
|
||||
import static com.google.common.collect.Sets.difference;
|
||||
import static com.google.common.collect.Sets.intersection;
|
||||
import static com.google.common.collect.Sets.union;
|
||||
import static google.registry.bsa.persistence.BsaLabelUtils.isLabelBlocked;
|
||||
import static google.registry.model.domain.Domain.MAX_REGISTRATION_YEARS;
|
||||
import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY;
|
||||
import static google.registry.model.tld.Tld.TldState.PREDELEGATION;
|
||||
import static google.registry.model.tld.Tld.TldState.QUIET_PERIOD;
|
||||
import static google.registry.model.tld.Tld.TldState.START_DATE_SUNRISE;
|
||||
import static google.registry.model.tld.Tld.isEnrolledWithBsa;
|
||||
import static google.registry.model.tld.Tlds.findTldForName;
|
||||
import static google.registry.model.tld.Tlds.getTlds;
|
||||
import static google.registry.model.tld.label.ReservationType.ALLOWED_IN_SUNRISE;
|
||||
@@ -259,6 +261,19 @@ public class DomainFlowUtils {
|
||||
return idnTableName.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the {@code domainLabel} is not blocked by any BSA block label for the given
|
||||
* {@code tld} at the specified time.
|
||||
*
|
||||
* @throws DomainLabelBlockedByBsaException
|
||||
*/
|
||||
public static void verifyNotBlockedByBsa(String domainLabel, Tld tld, DateTime now)
|
||||
throws DomainLabelBlockedByBsaException {
|
||||
if (isEnrolledWithBsa(tld, now) && isLabelBlocked(domainLabel)) {
|
||||
throw new DomainLabelBlockedByBsaException();
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns whether a given domain create request is for a valid anchor tenant. */
|
||||
public static boolean isAnchorTenant(
|
||||
InternetDomainName domainName,
|
||||
@@ -1742,4 +1757,12 @@ public class DomainFlowUtils {
|
||||
super("Registrar must be active in order to perform this operation");
|
||||
}
|
||||
}
|
||||
|
||||
/** Domain label is blocked by the Brand Safety Alliance. */
|
||||
static class DomainLabelBlockedByBsaException extends ParameterValuePolicyErrorException {
|
||||
public DomainLabelBlockedByBsaException() {
|
||||
// TODO(b/309174065): finalize the exception message.
|
||||
super("Domain label is blocked by the Brand Safety Alliance");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,4 +41,15 @@ public class BsaLabelTest {
|
||||
assertThat(persisted.getLabel()).isEqualTo("label");
|
||||
assertThat(persisted.creationTime).isEqualTo(fakeClock.nowUtc());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isLabelBlocked_no() {
|
||||
assertThat(tm().transact(() -> BsaLabelUtils.isLabelBlocked("abc"))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isLabelBlocked_yes() {
|
||||
tm().transact(() -> tm().put(new BsaLabel("abc", fakeClock.nowUtc())));
|
||||
assertThat(tm().transact(() -> BsaLabelUtils.isLabelBlocked("abc"))).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.bsa.persistence;
|
||||
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Testing utils for users of {@link BsaLabel}. */
|
||||
public final class BsaLabelTestingUtils {
|
||||
|
||||
private BsaLabelTestingUtils() {}
|
||||
|
||||
public static void persistBsaLabel(String domainLabel, DateTime creationTime) {
|
||||
tm().transact(() -> tm().put(new BsaLabel(domainLabel, creationTime)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.bsa.persistence;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.bsa.persistence.BsaLabelTestingUtils.persistBsaLabel;
|
||||
import static google.registry.bsa.persistence.BsaLabelUtils.isLabelBlocked;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.setJpaTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.setReplicaJpaTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
import static org.joda.time.Duration.millis;
|
||||
import static org.joda.time.Duration.standardMinutes;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationWithCoverageExtension;
|
||||
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||
import google.registry.testing.FakeClock;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
/** Unit tests for {@link BsaLabelUtils}. */
|
||||
public class BsaLabelUtilsTest {
|
||||
|
||||
protected FakeClock fakeClock = new FakeClock(DateTime.now(UTC));
|
||||
|
||||
@RegisterExtension
|
||||
final JpaIntegrationWithCoverageExtension jpa =
|
||||
new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();
|
||||
|
||||
@Test
|
||||
void isLabelBlocked_yes() {
|
||||
persistBsaLabel("abc", fakeClock.nowUtc());
|
||||
assertThat(isLabelBlocked("abc")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isLabelBlocked_no() {
|
||||
assertThat(isLabelBlocked("abc")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isLabelBlocked_isCacheUsed_withReplica() throws Throwable {
|
||||
JpaTransactionManager primaryTmSave = tm();
|
||||
JpaTransactionManager replicaTmSave = replicaTm();
|
||||
|
||||
JpaTransactionManager primaryTm = mock(JpaTransactionManager.class);
|
||||
JpaTransactionManager replicaTm = mock(JpaTransactionManager.class);
|
||||
setJpaTm(() -> primaryTm);
|
||||
setReplicaJpaTm(() -> replicaTm);
|
||||
when(replicaTm.loadByKey(any())).thenReturn(new BsaLabel("abc", fakeClock.nowUtc()));
|
||||
try {
|
||||
assertThat(isLabelBlocked("abc")).isTrue();
|
||||
assertThat(isLabelBlocked("abc")).isTrue();
|
||||
verify(replicaTm, times(1)).loadByKey(any());
|
||||
verify(primaryTm, never()).loadByKey(any());
|
||||
} catch (Throwable e) {
|
||||
setJpaTm(() -> primaryTmSave);
|
||||
setReplicaJpaTm(() -> replicaTmSave);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void isLabelBlocked_isCacheUsed_withOneMinuteExpiry() throws Throwable {
|
||||
JpaTransactionManager replicaTmSave = replicaTm();
|
||||
JpaTransactionManager replicaTm = mock(JpaTransactionManager.class);
|
||||
setReplicaJpaTm(() -> replicaTm);
|
||||
when(replicaTm.loadByKey(any())).thenReturn(new BsaLabel("abc", fakeClock.nowUtc()));
|
||||
try {
|
||||
assertThat(isLabelBlocked("abc")).isTrue();
|
||||
/**
|
||||
* If test fails, check and fix cache expiry in the config file. Do not increase the duration
|
||||
* on the line below without proper discussion.
|
||||
*/
|
||||
fakeClock.advanceBy(standardMinutes(1).plus(millis(1)));
|
||||
assertThat(isLabelBlocked("abc")).isTrue();
|
||||
verify(replicaTm, times(2)).loadByKey(any());
|
||||
} catch (Throwable e) {
|
||||
setReplicaJpaTm(() -> replicaTmSave);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.io.BaseEncoding.base16;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth8.assertThat;
|
||||
import static google.registry.bsa.persistence.BsaLabelTestingUtils.persistBsaLabel;
|
||||
import static google.registry.flows.FlowTestCase.UserPrivileges.SUPERUSER;
|
||||
import static google.registry.model.billing.BillingBase.Flag.ANCHOR_TENANT;
|
||||
import static google.registry.model.billing.BillingBase.Flag.RESERVED;
|
||||
@@ -30,6 +31,7 @@ import static google.registry.model.domain.token.AllocationToken.TokenType.BULK_
|
||||
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.model.eppcommon.EppXmlTransformer.marshal;
|
||||
import static google.registry.model.eppcommon.StatusValue.PENDING_DELETE;
|
||||
import static google.registry.model.eppcommon.StatusValue.SERVER_HOLD;
|
||||
import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY;
|
||||
@@ -96,6 +98,7 @@ import google.registry.flows.domain.DomainFlowUtils.ClaimsPeriodEndedException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.CurrencyUnitMismatchException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.CurrencyValueScaleException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.DashesInThirdAndFourthException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.DomainLabelBlockedByBsaException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.DomainLabelTooLongException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.DomainNameExistsAsTldException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.DomainReservedException;
|
||||
@@ -165,6 +168,9 @@ import google.registry.model.domain.secdns.DomainDsData;
|
||||
import google.registry.model.domain.token.AllocationToken;
|
||||
import google.registry.model.domain.token.AllocationToken.RegistrationBehavior;
|
||||
import google.registry.model.domain.token.AllocationToken.TokenStatus;
|
||||
import google.registry.model.eppcommon.Trid;
|
||||
import google.registry.model.eppoutput.EppOutput;
|
||||
import google.registry.model.eppoutput.EppResponse;
|
||||
import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
|
||||
import google.registry.model.poll.PollMessage;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
@@ -183,7 +189,9 @@ import google.registry.tmch.LordnTaskUtils.LordnPhase;
|
||||
import google.registry.tmch.SmdrlCsvParser;
|
||||
import google.registry.tmch.TmchData;
|
||||
import google.registry.tmch.TmchTestData;
|
||||
import google.registry.xml.ValidationMode;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
@@ -2562,6 +2570,53 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_bsaLabelMatch_notEnrolled() throws Exception {
|
||||
persistResource(Tld.get("tld").asBuilder().setBsaEnrollStartTime(Optional.empty()).build());
|
||||
persistBsaLabel("example", clock.nowUtc());
|
||||
persistContactsAndHosts();
|
||||
doSuccessfulTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_bsaLabelMatch_notEnrolledYet() throws Exception {
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setBsaEnrollStartTime(Optional.of(clock.nowUtc().plusSeconds(1)))
|
||||
.build());
|
||||
persistBsaLabel("example", clock.nowUtc());
|
||||
persistContactsAndHosts();
|
||||
doSuccessfulTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_blockedByBsa() throws Exception {
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setBsaEnrollStartTime(Optional.of(clock.nowUtc().minusSeconds(1)))
|
||||
.build());
|
||||
persistBsaLabel("example", clock.nowUtc());
|
||||
persistContactsAndHosts();
|
||||
EppException thrown = assertThrows(DomainLabelBlockedByBsaException.class, this::runFlow);
|
||||
assertAboutEppExceptions()
|
||||
.that(thrown)
|
||||
.marshalsToXml()
|
||||
.and()
|
||||
.hasMessage("Domain label is blocked by the Brand Safety Alliance");
|
||||
byte[] responseXmlBytes =
|
||||
marshal(
|
||||
EppOutput.create(
|
||||
new EppResponse.Builder()
|
||||
.setTrid(Trid.create(null, "server-trid"))
|
||||
.setResult(thrown.getResult())
|
||||
.build()),
|
||||
ValidationMode.STRICT);
|
||||
assertThat(new String(responseXmlBytes, StandardCharsets.UTF_8))
|
||||
.isEqualTo(loadFile("domain_create_blocked_by_bsa.xml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_uppercase() {
|
||||
doFailingDomainNameTest("Example.tld", BadDomainNameCharacterException.class);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<epp xmlns:domain="urn:ietf:params:xml:ns:domain-1.0" xmlns:contact="urn:ietf:params:xml:ns:contact-1.0" xmlns:fee="urn:ietf:params:xml:ns:fee-0.6" xmlns="urn:ietf:params:xml:ns:epp-1.0" xmlns:rgp="urn:ietf:params:xml:ns:rgp-1.0" xmlns:bulkToken="urn:google:params:xml:ns:bulkToken-1.0" xmlns:fee11="urn:ietf:params:xml:ns:fee-0.11" xmlns:fee12="urn:ietf:params:xml:ns:fee-0.12" xmlns:launch="urn:ietf:params:xml:ns:launch-1.0" xmlns:secDNS="urn:ietf:params:xml:ns:secDNS-1.1" xmlns:host="urn:ietf:params:xml:ns:host-1.0">
|
||||
<response>
|
||||
<result code="2306">
|
||||
<msg>Domain label is blocked by the Brand Safety Alliance</msg>
|
||||
</result>
|
||||
<trID>
|
||||
<svTRID>server-trid</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
</epp>
|
||||
@@ -384,6 +384,7 @@ An EPP flow that creates a new domain resource.
|
||||
* The requested fees cannot be provided in the requested currency.
|
||||
* Non-IDN domain names cannot contain hyphens in the third or fourth
|
||||
position.
|
||||
* Domain label is blocked by the Brand Safety Alliance.
|
||||
* Domain labels cannot be longer than 63 characters.
|
||||
* More than one contact for a given role is not allowed.
|
||||
* No part of a domain name can be empty.
|
||||
|
||||
Reference in New Issue
Block a user