Adds unit test for creating gateway service with encryption when enabled.
Fixes: https://tracker.ceph.com/issues/77359
Signed-off-by: pujashahu <pshahu@redhat.com>
</div>
</div>
- <!-- Pool -->
- <div cdsRow
- class="form-item">
- <div cdsCol>
- <cds-select
- label="Block pool"
- helperText="An RBD application-enabled pool in which the gateway configuration can be managed."
- labelInputID="pool"
- id="pool"
- formControlName="pool"
- cdRequiredField="Block pool"
- [invalid]="groupForm.controls.pool.invalid && groupForm.controls.pool.dirty"
- [invalidText]="poolError"
- i18n-label
- i18n-helperText
- >
- <option *ngIf="poolsLoading"
- [ngValue]="null"
- i18n>Loading...</option>
- <option *ngIf="!poolsLoading && pools.length === 0"
- [ngValue]="null"
- i18n>-- No block pools available --</option>
- <option *ngFor="let pool of pools"
- [value]="pool.pool_name">{{ pool.pool_name }}</option>
- </cds-select>
- <ng-template #poolError>
- <span class="invalid-feedback"
- *ngIf="groupForm.showError('pool', formDir, 'required')"
- i18n>This field is required.</span>
- </ng-template>
- </div>
- </div>
-
+
<!-- CDS Checkbox -->
<div cdsRow
class="form-item">
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
-import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { NvmeofGroupFormComponent } from './nvmeof-group-form.component';
import { CheckboxModule, GridModule, InputModule, SelectModule } from 'carbon-components-angular';
-import { PoolService } from '~/app/shared/api/pool.service';
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
import { CephServiceService } from '~/app/shared/api/ceph-service.service';
import { FormHelper } from '~/testing/unit-test-helper';
let fixture: ComponentFixture<NvmeofGroupFormComponent>;
let form: CdFormGroup;
let formHelper: FormHelper;
- let poolService: PoolService;
let taskWrapperService: TaskWrapperService;
let cephServiceService: CephServiceService;
let router: Router;
- const mockPools = [
- { pool_name: 'rbd', application_metadata: ['rbd'] },
- { pool_name: 'rbd', application_metadata: ['rbd'] },
- { pool_name: 'pool2', application_metadata: ['rgw'] }
- ];
-
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [NvmeofGroupFormComponent],
fixture = TestBed.createComponent(NvmeofGroupFormComponent);
component = fixture.componentInstance;
- poolService = TestBed.inject(PoolService);
taskWrapperService = TestBed.inject(TaskWrapperService);
cephServiceService = TestBed.inject(CephServiceService);
router = TestBed.inject(Router);
- spyOn(poolService, 'list').and.returnValue(Promise.resolve(mockPools));
-
component.ngOnInit();
form = component.groupForm;
formHelper = new FormHelper(form);
it('should initialize form with empty fields', () => {
expect(form.controls.groupName.value).toBeNull();
expect(form.controls.unmanaged.value).toBe(false);
+ expect(form.controls.enableEncryption.value).toBe(false);
});
it('should set action to CREATE on init', () => {
formHelper.expectError('groupName', 'required');
});
- it('should require pool', () => {
- formHelper.setValue('pool', null);
- formHelper.expectError('pool', 'required');
- });
-
- it('should be valid when groupName and pool are set', () => {
+ it('should be valid when groupName is set', () => {
formHelper.setValue('groupName', 'test-group');
- formHelper.setValue('pool', 'rbd');
- expect(form.valid).toBe(true);
+ expect(form.controls.groupName.valid).toBe(true);
});
- });
- describe('loadPools', () => {
- it('should load pools and filter by rbd application metadata', fakeAsync(() => {
- component.loadPools();
- tick();
- expect(component.pools.length).toBe(2);
- expect(component.pools.map((p) => p.pool_name)).toEqual(['rbd', 'rbd']);
- }));
-
- it('should set default pool to rbd if available', fakeAsync(() => {
- component.groupForm.get('pool').setValue(null);
- component.loadPools();
- tick();
- expect(component.groupForm.get('pool').value).toBe('rbd');
- }));
-
- it('should set first pool if rbd is not available', fakeAsync(() => {
- component.groupForm.get('pool').setValue(null);
- const poolsWithoutRbd = [{ pool_name: 'custom-pool', application_metadata: ['rbd'] }];
- (poolService.list as jasmine.Spy).and.returnValue(Promise.resolve(poolsWithoutRbd));
- component.loadPools();
- tick();
- expect(component.groupForm.get('pool').value).toBe('custom-pool');
- }));
-
- it('should handle empty pools', fakeAsync(() => {
- (poolService.list as jasmine.Spy).and.returnValue(Promise.resolve([]));
- component.loadPools();
- tick();
- expect(component.pools.length).toBe(0);
- expect(component.poolsLoading).toBe(false);
- }));
-
- it('should handle pool loading error', fakeAsync(() => {
- (poolService.list as jasmine.Spy).and.returnValue(Promise.reject('error'));
- component.loadPools();
- tick();
- expect(component.pools).toEqual([]);
- expect(component.poolsLoading).toBe(false);
- }));
+ it('should validate groupName for invalid characters', () => {
+ formHelper.setValue('groupName', 'test@group');
+ formHelper.expectError('groupName', 'invalidChars');
+ });
});
describe('onSubmit', () => {
} as any;
component.groupForm.get('groupName').setValue('test-group');
- component.groupForm.get('pool').setValue('rbd');
component.onSubmit();
expect(cephServiceService.create).not.toHaveBeenCalled();
getSelectedHostnames: (): string[] => ['host1', 'host2']
} as any;
- component.groupForm.get('groupName').setValue('defalut');
- component.groupForm.get('pool').setValue('rbd');
+ component.groupForm.get('groupName').setValue('default');
component.groupForm.get('unmanaged').setValue(false);
component.onSubmit();
expect(cephServiceService.create).toHaveBeenCalledWith({
service_type: 'nvmeof',
- service_id: 'rbd.defalut',
- pool: 'rbd',
- group: 'defalut',
+ service_id: 'default',
+ group: 'default',
placement: {
hosts: ['host1', 'host2']
},
} as any;
component.groupForm.get('groupName').setValue('unmanaged-group');
- component.groupForm.get('pool').setValue('rbd');
component.groupForm.get('unmanaged').setValue(true);
component.onSubmit();
expect(cephServiceService.create).toHaveBeenCalledWith(
jasmine.objectContaining({
unmanaged: true,
- group: 'unmanaged-group',
- pool: 'rbd'
+ group: 'unmanaged-group'
+ })
+ );
+ });
+
+ it('should create service with encryption when enabled', () => {
+ component.gatewayNodeComponent = {
+ getSelectedHosts: (): any[] => [{ hostname: 'host1' }],
+ getSelectedHostnames: (): string[] => ['host1']
+ } as any;
+
+ component.groupForm.get('groupName').setValue('encrypted-group');
+ component.groupForm.get('enableEncryption').setValue(true);
+ component.groupForm.get('encryptionConfig').setValue('encryption-key-123');
+ component.onSubmit();
+
+ expect(cephServiceService.create).toHaveBeenCalledWith(
+ jasmine.objectContaining({
+ group: 'encrypted-group',
+ encryption_key: 'encryption-key-123'
})
);
});
} as any;
component.groupForm.get('groupName').setValue('test-group');
- component.groupForm.get('pool').setValue('rbd');
component.onSubmit();
expect(router.navigateByUrl).toHaveBeenCalledWith('/block/nvmeof/gateways');
import { Permission } from '~/app/shared/models/permissions';
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
-import { PoolService } from '~/app/shared/api/pool.service';
-import { Pool } from '../../pool/pool';
import { NvmeofGatewayNodeComponent } from '../nvmeof-gateway-node/nvmeof-gateway-node.component';
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
import { CephServiceService } from '~/app/shared/api/ceph-service.service';
action: string;
resource: string;
group: string;
- pools: Pool[] = [];
- poolsLoading = false;
pageURL: string;
hasAvailableNodes = true;
constructor(
private authStorageService: AuthStorageService,
public actionLabels: ActionLabelsI18n,
- private poolService: PoolService,
private taskWrapperService: TaskWrapperService,
private cephServiceService: CephServiceService,
private nvmeofService: NvmeofService,
ngOnInit() {
this.action = this.actionLabels.CREATE;
this.createForm();
- this.loadPools();
}
createForm() {
],
[CdValidators.unique(this.nvmeofService.exists, this.nvmeofService)]
),
- pool: new UntypedFormControl('rbd', {
- validators: [Validators.required]
- }),
unmanaged: new UntypedFormControl(false),
enableEncryption: new UntypedFormControl(false),
encryptionConfig: new UntypedFormControl(null)
return false;
}
- loadPools() {
- this.poolsLoading = true;
- this.poolService.list().then(
- (pools: Pool[]) => {
- this.pools = (pools || []).filter(
- (pool: Pool) => pool.application_metadata && pool.application_metadata.includes('rbd')
- );
- this.poolsLoading = false;
- if (this.pools.length >= 1) {
- const allPoolNames = this.pools.map((pool) => pool.pool_name);
- const poolName = allPoolNames.includes('rbd') ? 'rbd' : this.pools[0].pool_name;
- this.groupForm.patchValue({ pool: poolName });
- }
- },
- () => {
- this.pools = [];
- this.poolsLoading = false;
- }
- );
- }
-
onSubmit() {
if (this.groupForm.invalid) {
return;
return;
}
let taskUrl = `service/${URLVerbs.CREATE}`;
- const serviceName = `${formValues.pool}.${formValues.groupName}`;
+ const serviceName = `${formValues.groupName}`;
const serviceSpec: Record<string, any> = {
service_type: 'nvmeof',
service_id: serviceName,
- pool: formValues.pool,
group: formValues.groupName,
placement: {
hosts: selectedHostnames
</div>
}
- <!-- NVMe/TCP -->
- <!-- Block Pool -->
- @if (serviceForm.controls.service_type.value === 'nvmeof') {
- <div class="form-item">
- <cds-select id="pool"
- formControlName="pool"
- label="Block Pool"
- cdRequiredField="Block Pool"
- helperText="An RBD application-enabled pool in which the gateway configuration can be managed."
- i18n-helperText
- [invalid]="serviceForm.controls.pool.invalid && serviceForm.controls.pool.dirty"
- [invalidText]="requiredFieldPool"
- (change)="setNvmeServiceId()">
- @if (rbdPools === null) {
- <option [ngValue]="null"
- i18n>Loading...</option>
- }
- @if (rbdPools && rbdPools.length === 0) {
- <option [ngValue]="null"
- i18n>-- No block pools available --</option>
- }
- @if (rbdPools && rbdPools.length > 0) {
- <option [ngValue]="null"
- i18n>-- Select a pool --</option>
- }
- @for (pool of rbdPools; track pool) {
- <option [value]="pool.pool_name">{{ pool.pool_name }}</option>
- }
- </cds-select>
- <ng-template #requiredFieldPool>
-
- @if (serviceForm.showError('pool', frm, 'required')) {
- <span class="invalid-feedback"
- i18n>This field is required.</span>
- }
- </ng-template>
- </div>
- }
-
<!-- Group Name -->
@if (serviceForm.controls.service_type.value === 'nvmeof') {
<div class="form-item">
it('should set default values correctly onInit', () => {
expect(form.get('service_type').value).toBe('nvmeof');
expect(form.get('group').value).toBe('default');
- expect(form.get('pool').value).toBe('rbd');
- expect(form.get('service_id').value).toBe('rbd.default');
+ expect(form.get('service_id').value).toBe('default');
});
it('should reflect correct values on group change', () => {
- // Initially the group value should be 'default'
expect(component.serviceForm.get('group')?.value).toBe('default');
const groupInput = fixture.debugElement.query(By.css('#group')).nativeElement;
- // Simulate input value change
groupInput.value = 'foo';
- // Trigger the input event
groupInput.dispatchEvent(new Event('input'));
- // Trigger the change event
groupInput.dispatchEvent(new Event('change'));
fixture.detectChanges();
- // Verify values after change
expect(form.get('group').value).toBe('foo');
- expect(form.get('service_id').value).toBe('rbd.foo');
- });
-
- it('should reflect correct values on pool change', () => {
- // Initially the pool value should be 'rbd'
- expect(component.serviceForm.get('pool')?.value).toBe('rbd');
- const poolInput = fixture.debugElement.query(By.css('#pool')).nativeElement;
- // Simulate input value change
- form.get('pool').setValue('pool-2');
- // Trigger the input event
- poolInput.dispatchEvent(new Event('input'));
- // Trigger the change event
- poolInput.dispatchEvent(new Event('change'));
- fixture.detectChanges();
- // Verify values after change
- expect(form.get('pool').value).toBe('pool-2');
- expect(form.get('service_id').value).toBe('pool-2.default');
+ expect(form.get('service_id').value).toBe('foo');
});
- it('should throw error when there is no service id', () => {
- formHelper.expectErrorChange('service_id', '', 'required');
+ it('should hide pool selector in create mode', () => {
+ const poolEl = fixture.debugElement.query(By.css('#pool'));
+ expect(poolEl).toBeNull();
});
- it('should throw error when there is no pool', () => {
- formHelper.expectErrorChange('pool', '', 'required');
+ it('should throw error when there is no service id', () => {
+ formHelper.expectErrorChange('service_id', '', 'required');
});
it('should throw error when there is no group', () => {
component.onSubmit();
expect(cephServiceService.create).toHaveBeenCalledWith({
service_type: 'nvmeof',
- service_id: 'rbd.default',
+ service_id: 'default',
placement: {},
unmanaged: false,
- pool: 'rbd',
group: 'default',
enable_auth: false
});
component.onSubmit();
expect(cephServiceService.create).toHaveBeenCalledWith({
service_type: 'nvmeof',
- service_id: 'rbd.default',
+ service_id: 'default',
placement: {},
unmanaged: false,
- pool: 'rbd',
group: 'default',
enable_auth: true,
root_ca_cert: 'root_ca_cert',
expect(serviceId.disabled).toBeTruthy();
});
- it('should not edit pools for nvmeof service', () => {
- component.serviceType = 'nvmeof';
- formHelper.setValue('service_type', 'nvmeof');
- component.ngOnInit();
- fixture.detectChanges();
- const poolId = fixture.componentInstance.serviceForm.get('pool');
- expect(poolId.disabled).toBeTruthy();
- });
-
it('should not edit groups for nvmeof service', () => {
component.serviceType = 'nvmeof';
formHelper.setValue('service_type', 'nvmeof');
spyOn(cephServiceService, 'update').and.stub();
component.serviceType = 'nvmeof';
formHelper.setValue('service_type', 'nvmeof');
- formHelper.setValue('pool', 'rbd');
formHelper.setValue('group', 'default');
- // mTLS disabled
formHelper.setValue('enable_mtls', false);
component.onSubmit();
expect(cephServiceService.update).toHaveBeenCalledWith({
service_type: 'nvmeof',
placement: {},
unmanaged: false,
- pool: 'rbd',
group: 'default',
enable_auth: false
});
[
CdValidators.requiredIf({
service_type: 'iscsi'
- }),
- CdValidators.requiredIf({
- service_type: 'nvmeof'
})
]
],
}
break;
case 'nvmeof':
- this.serviceForm.get('pool').setValue(response[0].spec.pool);
this.serviceForm.get('group').setValue(response[0].spec.group);
this.serviceForm.get('enable_mtls').setValue(response[0].spec?.enable_auth);
this.serviceForm.get('root_ca_cert').setValue(response[0].spec?.root_ca_cert);
}
setNvmeServiceId() {
- const pool = this.serviceForm.get('pool').value;
const group = this.serviceForm.get('group').value;
- if (pool && group) {
- this.serviceForm.get('service_id').setValue(`${pool}.${group}`);
- } else if (pool) {
- this.serviceForm.get('service_id').setValue(pool);
- } else if (group) {
+ if (group) {
this.serviceForm.get('service_id').setValue(group);
} else {
this.serviceForm.get('service_id').setValue(null);
}
}
- setNvmeDefaultPool() {
- const defaultPool =
- this.rbdPools?.find((p: Pool) => p.pool_name === 'rbd')?.pool_name ||
- this.rbdPools?.[0].pool_name;
- this.serviceForm.get('pool').setValue(defaultPool);
- }
-
requiresServiceId(serviceType: string) {
return ['mds', 'rgw', 'nfs', 'iscsi', 'nvmeof', 'smb', 'ingress'].includes(serviceType);
}
setServiceId(serviceId: string): void {
const requiresServiceId: boolean = this.requiresServiceId(serviceId);
if (requiresServiceId && serviceId === 'nvmeof') {
- this.setNvmeDefaultPool();
this.setNvmeServiceId();
} else if (requiresServiceId) {
this.serviceForm.get('service_id').setValue(null);
this.serviceForm.get('backend_service').disable();
break;
case 'nvmeof':
- this.serviceForm.get('pool').disable();
this.serviceForm.get('group').disable();
break;
}
break;
case 'nvmeof':
- serviceSpec['pool'] = values['pool'];
serviceSpec['group'] = values['group'];
serviceSpec['enable_auth'] = values['enable_mtls'];
if (values['enable_mtls']) {