1
0
mirror of https://github.com/google/nomulus synced 2026-07-19 06:22:33 +00:00

Compare commits

...

3 Commits

Author SHA1 Message Date
Pavlo Tkach 9006312253 Create reusable dialog / bottom sheet component (#2237) 2023-12-08 17:52:57 -05:00
gbrodman e5e2370923 Debouncedly use a search term in console domain list (#2242) 2023-12-08 15:37:30 -05:00
sarahcaseybot b3b0efd47e Add a dryrun tag to UpdatePremiumListCommand and early exit command if no new changes to the list (#2246)
* Add a dryrun tag to UpdatePremiumListCommand and early exit command if no new changes to the list

* Change prompt string when no change to list to reflect that there is no actual prompted user input

* Add camelCase and correct flag name
2023-12-08 14:35:05 -05:00
18 changed files with 255 additions and 173 deletions
+3 -5
View File
@@ -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)));
@@ -72,9 +72,9 @@ public class ConfigureTldCommand extends MutatingCommand {
boolean breakglass;
@Parameter(
names = {"-d", "--dryrun"},
names = {"-d", "--dry_run"},
description = "Does not execute the entity mutation")
boolean dryrun;
boolean dryRun;
@Inject ObjectMapper mapper;
@@ -138,7 +138,7 @@ public class ConfigureTldCommand extends MutatingCommand {
@Override
protected boolean dontRunCommand() {
if (dryrun) {
if (dryRun) {
return true;
}
if (!newDiff) {
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.util.ListNamingUtils.convertFilePathToName;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.base.Strings;
import google.registry.model.tld.label.PremiumList;
@@ -29,6 +30,14 @@ import java.nio.file.Files;
@Parameters(separators = " =", commandDescription = "Update a PremiumList in Database.")
class UpdatePremiumListCommand extends CreateOrUpdatePremiumListCommand {
@Parameter(
names = {"-d", "--dry_run"},
description = "Does not execute the entity mutation")
boolean dryRun;
// indicates if there is a new change made by this command
private boolean newChange = false;
@Override
protected String prompt() throws Exception {
name = Strings.isNullOrEmpty(name) ? convertFilePathToName(inputFile) : name;
@@ -43,8 +52,23 @@ class UpdatePremiumListCommand extends CreateOrUpdatePremiumListCommand {
checkArgument(!inputData.isEmpty(), "New premium list data cannot be empty");
currency = existingList.getCurrency();
PremiumList updatedPremiumList = PremiumListUtils.parseToPremiumList(name, currency, inputData);
return String.format(
"Update premium list for %s?\n Old List: %s\n New List: %s",
name, existingList, updatedPremiumList);
if (!existingList
.getLabelsToPrices()
.entrySet()
.equals(updatedPremiumList.getLabelsToPrices().entrySet())) {
newChange = true;
return String.format(
"Update premium list for %s?\n Old List: %s\n New List: %s",
name, existingList, updatedPremiumList);
} else {
return String.format(
"This update contains no changes to the premium list for %s.\n List Contents: %s",
name, existingList);
}
}
@Override
protected boolean dontRunCommand() {
return dryRun || !newChange;
}
}
@@ -593,7 +593,7 @@ public class ConfigureTldCommandTest extends CommandTestCase<ConfigureTldCommand
void testSuccess_dryRunOnCreate_noChanges() throws Exception {
File tldFile = tmpDir.resolve("tld.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "tld.yaml"));
runCommandForced("--input=" + tldFile, "--dryrun");
runCommandForced("--input=" + tldFile, "--dry_run");
assertThrows(TldNotFoundException.class, () -> Tld.get("tld"));
}
@@ -63,10 +63,19 @@ class UpdatePremiumListCommandTest<C extends UpdatePremiumListCommand>
UpdatePremiumListCommand command = new UpdatePremiumListCommand();
command.inputFile = Paths.get(tmpFile.getPath());
command.name = TLD_TEST;
command.prompt();
assertThat(command.prompt()).contains("Update premium list for prime?");
}
@Test
void commandPrompt_successStageNoChange() throws Exception {
File tmpFile = tmpDir.resolve(String.format("%s.txt", TLD_TEST)).toFile();
UpdatePremiumListCommand command = new UpdatePremiumListCommand();
command.inputFile = Paths.get(tmpFile.getPath());
command.name = TLD_TEST;
assertThat(command.prompt())
.contains("This update contains no changes to the premium list for prime.");
}
@Test
void commandRun_successUpdateList() throws Exception {
File tmpFile = tmpDir.resolve(String.format("%s.txt", TLD_TEST)).toFile();
@@ -83,6 +92,18 @@ class UpdatePremiumListCommandTest<C extends UpdatePremiumListCommand>
.containsExactly(PremiumEntry.create(0L, new BigDecimal("9999.00"), "eth"));
}
@Test
void commandRun_successNoChange() throws Exception {
File tmpFile = tmpDir.resolve(String.format("%s.txt", TLD_TEST)).toFile();
UpdatePremiumListCommand command = new UpdatePremiumListCommand();
command.inputFile = Paths.get(tmpFile.getPath());
runCommandForced("--name=" + TLD_TEST, "--input=" + command.inputFile);
assertThat(PremiumListDao.loadAllPremiumEntries(TLD_TEST))
.containsExactly(PremiumEntry.create(0L, new BigDecimal("9090.00"), "doge"));
}
@Test
void commandRun_successUpdateList_whenExistingListIsEmpty() throws Exception {
File existingPremiumFile = tmpDir.resolve(TLD_TEST + ".txt").toFile();
@@ -169,4 +190,19 @@ class UpdatePremiumListCommandTest<C extends UpdatePremiumListCommand>
.hasMessageThat()
.isEqualTo("Could not update premium list random3 because it doesn't exist");
}
@Test
void commandDryRun_noChangesMade() throws Exception {
File tmpFile = tmpDir.resolve(String.format("%s.txt", TLD_TEST)).toFile();
String newPremiumListData = "eth,USD 9999";
Files.asCharSink(tmpFile, UTF_8).write(newPremiumListData);
UpdatePremiumListCommand command = new UpdatePremiumListCommand();
command.inputFile = Paths.get(tmpFile.getPath());
runCommandForced("--name=" + TLD_TEST, "--input=" + command.inputFile, "--dry_run");
assertThat(PremiumListDao.loadAllPremiumEntries(TLD_TEST))
.comparingElementsUsing(immutableObjectCorrespondence("revisionId"))
.containsExactly(PremiumEntry.create(0L, new BigDecimal("9090.00"), "doge"));
}
}