Skip to content

Commit

Permalink
NAS-124298: Add sudo enabled dialog to replication form and unit tests (
Browse files Browse the repository at this point in the history
  • Loading branch information
denysbutenko authored Oct 3, 2023
1 parent 3ee4812 commit 6d152d6
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatButtonHarness } from '@angular/material/button/testing';
import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { MockComponents, MockInstance } from 'ng-mocks';
import { of } from 'rxjs';
import { mockCall, mockWebsocket } from 'app/core/testing/utils/mock-websocket.utils';
import { Direction } from 'app/enums/direction.enum';
import { SnapshotNamingOption } from 'app/enums/snapshot-naming-option.enum';
import { TransportMode } from 'app/enums/transport-mode.enum';
import helptext from 'app/helptext/data-protection/replication/replication-wizard';
import { KeychainCredential } from 'app/interfaces/keychain-credential.interface';
import { ReplicationTask } from 'app/interfaces/replication-task.interface';
import { IxSlideInRef } from 'app/modules/ix-forms/components/ix-slide-in/ix-slide-in-ref';
import { SLIDE_IN_DATA } from 'app/modules/ix-forms/components/ix-slide-in/ix-slide-in.token';
Expand Down Expand Up @@ -36,6 +39,7 @@ import {
ReplicationWizardComponent,
} from 'app/pages/data-protection/replication/replication-wizard/replication-wizard.component';
import { DatasetService } from 'app/services/dataset-service/dataset.service';
import { DialogService } from 'app/services/dialog.service';
import { IxSlideInService } from 'app/services/ix-slide-in.service';
import { ReplicationService } from 'app/services/replication.service';
import { WebSocketService } from 'app/services/ws.service';
Expand All @@ -50,6 +54,7 @@ describe('ReplicationFormComponent', () => {
name: new FormControl('dataset'),
direction: new FormControl(Direction.Pull),
transport: new FormControl(TransportMode.Ssh),
sudo: new FormControl(false),
});
const transportForm = new FormGroup({
ssh_credentials: new FormControl(5),
Expand Down Expand Up @@ -119,7 +124,19 @@ describe('ReplicationFormComponent', () => {
}),
mockCall('replication.create'),
mockCall('replication.update'),
mockCall('keychaincredential.query', [
{
id: 123,
name: 'non-root-ssh-connection',
attributes: {
username: 'user1',
},
},
] as KeychainCredential[]),
]),
mockProvider(DialogService, {
confirm: jest.fn(() => of()),
}),
mockProvider(IxSlideInService),
mockProvider(SnackbarService),
mockProvider(IxSlideInRef),
Expand Down Expand Up @@ -174,6 +191,7 @@ describe('ReplicationFormComponent', () => {
target_dataset: '/tank/target',
transport: TransportMode.Ssh,
auto: true,
sudo: false,
}]);
expect(spectator.inject(IxSlideInRef).close).toHaveBeenCalled();
});
Expand Down Expand Up @@ -205,6 +223,7 @@ describe('ReplicationFormComponent', () => {
target_dataset: '/tank/target',
transport: TransportMode.Ssh,
auto: true,
sudo: false,
},
]);
expect(spectator.inject(IxSlideInRef).close).toHaveBeenCalled();
Expand Down Expand Up @@ -253,4 +272,25 @@ describe('ReplicationFormComponent', () => {
expect(spectator.query(TargetSectionComponent).nodeProvider).toBe(localNodeProvider);
}));
});

describe('sudo enabled dialog', () => {
beforeEach(fakeAsync(() => {
spectator = createComponent();
tick();
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
}));

it('opens sudo enabled dialog when choosing to existing ssh credential', fakeAsync(() => {
transportForm.controls.ssh_credentials.setValue(123);
tick();
spectator.detectChanges();

expect(spectator.inject(DialogService).confirm).toHaveBeenCalledWith({
buttonText: 'Use Sudo For ZFS Commands',
hideCheckbox: true,
message: helptext.sudo_warning,
title: 'Sudo Enabled',
});
}));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import {
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { TranslateService } from '@ngx-translate/core';
import { merge } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { debounceTime, switchMap } from 'rxjs/operators';
import { Direction } from 'app/enums/direction.enum';
import { SnapshotNamingOption } from 'app/enums/snapshot-naming-option.enum';
import { TransportMode } from 'app/enums/transport-mode.enum';
import helptext from 'app/helptext/data-protection/replication/replication-wizard';
import { CountManualSnapshotsParams } from 'app/interfaces/count-manual-snapshots.interface';
import { KeychainSshCredentials } from 'app/interfaces/keychain-credential.interface';
import { ReplicationCreate, ReplicationTask } from 'app/interfaces/replication-task.interface';
import { WebsocketError } from 'app/interfaces/websocket-error.interface';
import { TreeNodeProvider } from 'app/modules/ix-forms/components/ix-explorer/tree-node-provider.interface';
Expand Down Expand Up @@ -38,6 +40,7 @@ import { DatasetService } from 'app/services/dataset-service/dataset.service';
import { DialogService } from 'app/services/dialog.service';
import { ErrorHandlerService } from 'app/services/error-handler.service';
import { IxSlideInService } from 'app/services/ix-slide-in.service';
import { KeychainCredentialService } from 'app/services/keychain-credential.service';
import { ReplicationService } from 'app/services/replication.service';
import { WebSocketService } from 'app/services/ws.service';

Expand All @@ -62,6 +65,8 @@ export class ReplicationFormComponent implements OnInit {

eligibleSnapshotsMessage = '';
isEligibleSnapshotsMessageRed = false;
isSudoDialogShown = false;
sshCredentials: KeychainSshCredentials[] = [];

constructor(
private ws: WebSocketService,
Expand All @@ -75,13 +80,15 @@ export class ReplicationFormComponent implements OnInit {
private replicationService: ReplicationService,
private slideInService: IxSlideInService,
private slideInRef: IxSlideInRef<ReplicationFormComponent>,
private keychainCredentials: KeychainCredentialService,
@Inject(SLIDE_IN_DATA) public existingReplication: ReplicationTask,
) {}

ngOnInit(): void {
this.countSnapshotsOnChanges();
this.updateExplorersOnChanges();
this.updateExplorers();
this.listenForSudoEnabled();

if (this.existingReplication) {
this.setForEdit();
Expand Down Expand Up @@ -278,4 +285,33 @@ export class ReplicationFormComponent implements OnInit {
this.targetNodeProvider = this.isPush && !this.isLocal ? remoteProvider : localProvider;
this.cdr.markForCheck();
}

private listenForSudoEnabled(): void {
this.keychainCredentials.getSshConnections()
.pipe(
switchMap((sshCredentials) => {
this.sshCredentials = sshCredentials;
return this.transportSection.form.controls.ssh_credentials.valueChanges;
}),
untilDestroyed(this),
)
.subscribe((credentialId: number) => {
const selectedCredential = this.sshCredentials.find((credential) => credential.id === credentialId);
const isRootUser = selectedCredential?.attributes?.username === 'root';

if (!selectedCredential || isRootUser || this.isSudoDialogShown) {
return;
}

this.dialog.confirm({
title: this.translate.instant('Sudo Enabled'),
message: helptext.sudo_warning,
hideCheckbox: true,
buttonText: this.translate.instant('Use Sudo For ZFS Commands'),
}).pipe(untilDestroyed(this)).subscribe((useSudo) => {
this.generalSection.form.controls.sudo.setValue(useSudo);
this.isSudoDialogShown = true;
});
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { EncryptionKeyFormat } from 'app/enums/encryption-key-format.enum';
import { mntPath } from 'app/enums/mnt-path.enum';
import { SnapshotNamingOption } from 'app/enums/snapshot-naming-option.enum';
import { TransportMode } from 'app/enums/transport-mode.enum';
import helptext from 'app/helptext/data-protection/replication/replication-wizard';
import { KeychainCredential } from 'app/interfaces/keychain-credential.interface';
import { ReplicationTask } from 'app/interfaces/replication-task.interface';
import { IxSlideInRef } from 'app/modules/ix-forms/components/ix-slide-in/ix-slide-in-ref';
Expand All @@ -21,6 +22,7 @@ import { IxFormHarness } from 'app/modules/ix-forms/testing/ix-form.harness';
import { ReplicationFormComponent } from 'app/pages/data-protection/replication/replication-form/replication-form.component';
import { ReplicationWhatAndWhereComponent } from 'app/pages/data-protection/replication/replication-wizard/steps/replication-what-and-where/replication-what-and-where.component';
import { DatasetService } from 'app/services/dataset-service/dataset.service';
import { DialogService } from 'app/services/dialog.service';
import { IxSlideInService } from 'app/services/ix-slide-in.service';

describe('ReplicationWhatAndWhereComponent', () => {
Expand Down Expand Up @@ -48,7 +50,13 @@ describe('ReplicationWhatAndWhereComponent', () => {
},
] as ReplicationTask[]),
mockCall('keychaincredential.query', [
{ id: 123, name: 'test_ssh' },
{
id: 123,
name: 'non-root-ssh-connection',
attributes: {
username: 'user1',
},
},
] as KeychainCredential[]),
mockCall('replication.count_eligible_manual_snapshots', { total: 0, eligible: 0 }),
]),
Expand All @@ -60,6 +68,9 @@ describe('ReplicationWhatAndWhereComponent', () => {
})),
}),
mockProvider(IxSlideInRef),
mockProvider(DialogService, {
confirm: jest.fn(() => of()),
}),
{ provide: SLIDE_IN_DATA, useValue: undefined },
],
});
Expand Down Expand Up @@ -141,6 +152,17 @@ describe('ReplicationWhatAndWhereComponent', () => {
expect(matDialog.open).toHaveBeenCalled();
});

it('opens sudo enabled dialog when choosing to existing ssh credential', async () => {
await form.fillForm({ 'Source Location': 'On a Different System' });
await form.fillForm({ 'SSH Connection': 'non-root-ssh-connection' });
expect(spectator.inject(DialogService).confirm).toHaveBeenCalledWith({
'buttonText': 'Use Sudo For ZFS Commands',
'hideCheckbox': true,
'message': helptext.sudo_warning,
'title': 'Sudo Enabled',
});
});

it('when an existing name is entered, the "Next" button is disabled', async () => {
const nextButton = await loader.getHarness(MatButtonHarness.with({ text: 'Next' }));

Expand Down

0 comments on commit 6d152d6

Please sign in to comment.