]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.spec.ts
84d0d266e27356e36b475f2c2d3b5a56f001205f
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / pool / crush-rule-form-modal / crush-rule-form-modal.component.spec.ts
1 import { HttpClientTestingModule } from '@angular/common/http/testing';
2 import { ComponentFixture, TestBed } from '@angular/core/testing';
3 import { RouterTestingModule } from '@angular/router/testing';
4
5 import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
6 import { BsModalRef } from 'ngx-bootstrap/modal';
7 import { ToastrModule } from 'ngx-toastr';
8 import { of } from 'rxjs';
9
10 import {
11 configureTestBed,
12 FixtureHelper,
13 FormHelper,
14 i18nProviders
15 } from '../../../../testing/unit-test-helper';
16 import { CrushRuleService } from '../../../shared/api/crush-rule.service';
17 import { CrushNode } from '../../../shared/models/crush-node';
18 import { CrushRuleConfig } from '../../../shared/models/crush-rule';
19 import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
20 import { PoolModule } from '../pool.module';
21 import { CrushRuleFormModalComponent } from './crush-rule-form-modal.component';
22
23 describe('CrushRuleFormComponent', () => {
24 let component: CrushRuleFormModalComponent;
25 let crushRuleService: CrushRuleService;
26 let fixture: ComponentFixture<CrushRuleFormModalComponent>;
27 let formHelper: FormHelper;
28 let fixtureHelper: FixtureHelper;
29 let data: { names: string[]; nodes: CrushNode[] };
30
31 // Object contains mock functions
32 const mock = {
33 node: (
34 name: string,
35 id: number,
36 type: string,
37 type_id: number,
38 children?: number[],
39 device_class?: string
40 ): CrushNode => {
41 return { name, type, type_id, id, children, device_class };
42 },
43 rule: (
44 name: string,
45 root: string,
46 failure_domain: string,
47 device_class?: string
48 ): CrushRuleConfig => ({
49 name,
50 root,
51 failure_domain,
52 device_class
53 })
54 };
55
56 // Object contains functions to get something
57 const get = {
58 nodeByName: (name: string): CrushNode => data.nodes.find((node) => node.name === name),
59 nodesByNames: (names: string[]): CrushNode[] => names.map(get.nodeByName)
60 };
61
62 // Expects that are used frequently
63 const assert = {
64 failureDomains: (nodes: CrushNode[], types: string[]) => {
65 const expectation = {};
66 types.forEach((type) => (expectation[type] = nodes.filter((node) => node.type === type)));
67 const keys = component.failureDomainKeys();
68 expect(keys).toEqual(types);
69 keys.forEach((key) => {
70 expect(component.failureDomains[key].length).toBe(expectation[key].length);
71 });
72 },
73 formFieldValues: (root: CrushNode, failureDomain: string, device: string) => {
74 expect(component.form.value).toEqual({
75 name: '',
76 root,
77 failure_domain: failureDomain,
78 device_class: device
79 });
80 },
81 valuesOnRootChange: (
82 rootName: string,
83 expectedFailureDomain: string,
84 expectedDevice: string
85 ) => {
86 const node = get.nodeByName(rootName);
87 formHelper.setValue('root', node);
88 assert.formFieldValues(node, expectedFailureDomain, expectedDevice);
89 },
90 creation: (rule: CrushRuleConfig) => {
91 formHelper.setValue('name', rule.name);
92 fixture.detectChanges();
93 component.onSubmit();
94 expect(crushRuleService.create).toHaveBeenCalledWith(rule);
95 }
96 };
97
98 configureTestBed({
99 imports: [
100 HttpClientTestingModule,
101 RouterTestingModule,
102 ToastrModule.forRoot(),
103 PoolModule,
104 NgBootstrapFormValidationModule.forRoot()
105 ],
106 providers: [CrushRuleService, BsModalRef, i18nProviders]
107 });
108
109 beforeEach(() => {
110 fixture = TestBed.createComponent(CrushRuleFormModalComponent);
111 fixtureHelper = new FixtureHelper(fixture);
112 component = fixture.componentInstance;
113 formHelper = new FormHelper(component.form);
114 crushRuleService = TestBed.get(CrushRuleService);
115 data = {
116 names: ['rule1', 'rule2'],
117 /**
118 * Create the following test crush map:
119 * > default
120 * --> ssd-host
121 * ----> 3x osd with ssd
122 * --> mix-host
123 * ----> hdd-rack
124 * ------> 2x osd-rack with hdd
125 * ----> ssd-rack
126 * ------> 2x osd-rack with ssd
127 */
128 nodes: [
129 // Root node
130 mock.node('default', -1, 'root', 11, [-2, -3]),
131 // SSD host
132 mock.node('ssd-host', -2, 'host', 1, [1, 0, 2]),
133 mock.node('osd.0', 0, 'osd', 0, undefined, 'ssd'),
134 mock.node('osd.1', 1, 'osd', 0, undefined, 'ssd'),
135 mock.node('osd.2', 2, 'osd', 0, undefined, 'ssd'),
136 // SSD and HDD mixed devices host
137 mock.node('mix-host', -3, 'host', 1, [-4, -5]),
138 // HDD rack
139 mock.node('hdd-rack', -4, 'rack', 3, [3, 4]),
140 mock.node('osd2.0', 3, 'osd-rack', 0, undefined, 'hdd'),
141 mock.node('osd2.1', 4, 'osd-rack', 0, undefined, 'hdd'),
142 // SSD rack
143 mock.node('ssd-rack', -5, 'rack', 3, [5, 6]),
144 mock.node('osd2.0', 5, 'osd-rack', 0, undefined, 'ssd'),
145 mock.node('osd2.1', 6, 'osd-rack', 0, undefined, 'ssd')
146 ]
147 };
148 spyOn(crushRuleService, 'getInfo').and.callFake(() => of(data));
149 fixture.detectChanges();
150 });
151
152 it('should create', () => {
153 expect(component).toBeTruthy();
154 });
155
156 it('calls listing to get rules on ngInit', () => {
157 expect(crushRuleService.getInfo).toHaveBeenCalled();
158 expect(component.names.length).toBe(2);
159 expect(component['nodes'].length).toBe(12);
160 });
161
162 describe('lists', () => {
163 afterEach(() => {
164 // The available buckets should not change
165 expect(component.buckets).toEqual(
166 get.nodesByNames(['default', 'hdd-rack', 'mix-host', 'ssd-host', 'ssd-rack'])
167 );
168 });
169
170 it('has the following lists after init', () => {
171 assert.failureDomains(data.nodes, ['host', 'osd', 'osd-rack', 'rack']); // Not root as root only exist once
172 expect(component.devices).toEqual(['hdd', 'ssd']);
173 });
174
175 it('has the following lists after selection of ssd-host', () => {
176 formHelper.setValue('root', get.nodeByName('ssd-host'));
177 assert.failureDomains(get.nodesByNames(['osd.0', 'osd.1', 'osd.2']), ['osd']); // Not host as it only exist once
178 expect(component.devices).toEqual(['ssd']);
179 });
180
181 it('has the following lists after selection of mix-host', () => {
182 formHelper.setValue('root', get.nodeByName('mix-host'));
183 expect(component.devices).toEqual(['hdd', 'ssd']);
184 assert.failureDomains(
185 get.nodesByNames(['hdd-rack', 'ssd-rack', 'osd2.0', 'osd2.1', 'osd2.0', 'osd2.1']),
186 ['osd-rack', 'rack']
187 );
188 });
189 });
190
191 describe('selection', () => {
192 it('selects the first root after init automatically', () => {
193 assert.formFieldValues(get.nodeByName('default'), 'osd-rack', '');
194 });
195
196 it('should select all values automatically by selecting "ssd-host" as root', () => {
197 assert.valuesOnRootChange('ssd-host', 'osd', 'ssd');
198 });
199
200 it('selects automatically the most common failure domain', () => {
201 // Select mix-host as mix-host has multiple failure domains (osd-rack and rack)
202 assert.valuesOnRootChange('mix-host', 'osd-rack', '');
203 });
204
205 it('should override automatic selections', () => {
206 assert.formFieldValues(get.nodeByName('default'), 'osd-rack', '');
207 assert.valuesOnRootChange('ssd-host', 'osd', 'ssd');
208 assert.valuesOnRootChange('mix-host', 'osd-rack', '');
209 });
210
211 it('should not override manual selections if possible', () => {
212 formHelper.setValue('failure_domain', 'rack', true);
213 formHelper.setValue('device_class', 'ssd', true);
214 assert.valuesOnRootChange('mix-host', 'rack', 'ssd');
215 });
216
217 it('should preselect device by domain selection', () => {
218 formHelper.setValue('failure_domain', 'osd', true);
219 assert.formFieldValues(get.nodeByName('default'), 'osd', 'ssd');
220 });
221 });
222
223 describe('form validation', () => {
224 it(`isn't valid if name is not set`, () => {
225 expect(component.form.invalid).toBeTruthy();
226 formHelper.setValue('name', 'someProfileName');
227 expect(component.form.valid).toBeTruthy();
228 });
229
230 it('sets name invalid', () => {
231 component.names = ['awesomeProfileName'];
232 formHelper.expectErrorChange('name', 'awesomeProfileName', 'uniqueName');
233 formHelper.expectErrorChange('name', 'some invalid text', 'pattern');
234 formHelper.expectErrorChange('name', null, 'required');
235 });
236
237 it(`should show all default form controls`, () => {
238 // name
239 // root (preselected(first root))
240 // failure_domain (preselected=type that is most common)
241 // device_class (preselected=any if multiple or some type if only one device type)
242 fixtureHelper.expectIdElementsVisible(
243 ['name', 'root', 'failure_domain', 'device_class'],
244 true
245 );
246 });
247 });
248
249 describe('submission', () => {
250 beforeEach(() => {
251 const taskWrapper = TestBed.get(TaskWrapperService);
252 spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
253 spyOn(crushRuleService, 'create').and.stub();
254 });
255
256 it('creates a rule with only required fields', () => {
257 assert.creation(mock.rule('default-rule', 'default', 'osd-rack'));
258 });
259
260 it('creates a rule with all fields', () => {
261 assert.valuesOnRootChange('ssd-host', 'osd', 'ssd');
262 assert.creation(mock.rule('ssd-host-rule', 'ssd-host', 'osd', 'ssd'));
263 });
264 });
265 });