blob: fb1f051db995b500f4121497d26d7c0fe9d3e0b6 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import {assert} from 'chai';
6import sinon from 'sinon';
7import {prpcClient} from 'prpc-client-instance.js';
8import * as projectV0 from './projectV0.js';
9import {store} from './base.js';
10import * as example from 'shared/test/constants-projectV0.js';
11import {restrictionLabelsForPermissions} from 'shared/convertersV0.js';
12import {fieldTypes, SITEWIDE_DEFAULT_COLUMNS} from 'shared/issue-fields.js';
13
14describe('project reducers', () => {
15 it('root reducer initial state', () => {
16 const actual = projectV0.reducer(undefined, {type: null});
17 const expected = {
18 name: null,
19 configs: {},
20 presentationConfigs: {},
21 customPermissions: {},
22 visibleMembers: {},
23 templates: {},
24 requests: {
25 fetchConfig: {
26 error: null,
27 requesting: false,
28 },
29 fetchCustomPermissions: {
30 error: null,
31 requesting: false,
32 },
33 fetchMembers: {
34 error: null,
35 requesting: false,
36 },
37 fetchPresentationConfig: {
38 error: null,
39 requesting: false,
40 },
41 fetchTemplates: {
42 error: null,
43 requesting: false,
44 },
45 },
46 };
47 assert.deepEqual(actual, expected);
48 });
49
50 it('name', () => {
51 const action = {type: projectV0.SELECT, projectName: example.PROJECT_NAME};
52 assert.deepEqual(projectV0.nameReducer(null, action), example.PROJECT_NAME);
53 });
54
55 it('configs updates when fetching Config', () => {
56 const action = {
57 type: projectV0.FETCH_CONFIG_SUCCESS,
58 projectName: example.PROJECT_NAME,
59 config: example.CONFIG,
60 };
61 const expected = {[example.PROJECT_NAME]: example.CONFIG};
62 assert.deepEqual(projectV0.configsReducer({}, action), expected);
63 });
64
65 it('customPermissions', () => {
66 const action = {
67 type: projectV0.FETCH_CUSTOM_PERMISSIONS_SUCCESS,
68 projectName: example.PROJECT_NAME,
69 permissions: example.CUSTOM_PERMISSIONS,
70 };
71 const expected = {[example.PROJECT_NAME]: example.CUSTOM_PERMISSIONS};
72 assert.deepEqual(projectV0.customPermissionsReducer({}, action), expected);
73 });
74
75 it('presentationConfigs', () => {
76 const action = {
77 type: projectV0.FETCH_PRESENTATION_CONFIG_SUCCESS,
78 projectName: example.PROJECT_NAME,
79 presentationConfig: example.PRESENTATION_CONFIG,
80 };
81 const expected = {[example.PROJECT_NAME]: example.PRESENTATION_CONFIG};
82 assert.deepEqual(projectV0.presentationConfigsReducer({}, action),
83 expected);
84 });
85
86 it('visibleMembers', () => {
87 const action = {
88 type: projectV0.FETCH_VISIBLE_MEMBERS_SUCCESS,
89 projectName: example.PROJECT_NAME,
90 visibleMembers: example.VISIBLE_MEMBERS,
91 };
92 const expected = {[example.PROJECT_NAME]: example.VISIBLE_MEMBERS};
93 assert.deepEqual(projectV0.visibleMembersReducer({}, action), expected);
94 });
95
96 it('templates', () => {
97 const action = {
98 type: projectV0.FETCH_TEMPLATES_SUCCESS,
99 projectName: example.PROJECT_NAME,
100 templates: [example.TEMPLATE_DEF],
101 };
102 const expected = {[example.PROJECT_NAME]: [example.TEMPLATE_DEF]};
103 assert.deepEqual(projectV0.templatesReducer({}, action), expected);
104 });
105});
106
107describe('project selectors', () => {
108 it('viewedProjectName', () => {
109 const actual = projectV0.viewedProjectName(example.STATE);
110 assert.deepEqual(actual, example.PROJECT_NAME);
111 });
112
113 it('viewedVisibleMembers', () => {
114 assert.deepEqual(projectV0.viewedVisibleMembers({}), {});
115 assert.deepEqual(projectV0.viewedVisibleMembers({projectV0: {}}), {});
116 assert.deepEqual(projectV0.viewedVisibleMembers(
117 {projectV0: {visibleMembers: {}}}), {});
118 const actual = projectV0.viewedVisibleMembers(example.STATE);
119 assert.deepEqual(actual, example.VISIBLE_MEMBERS);
120 });
121
122 it('viewedCustomPermissions', () => {
123 assert.deepEqual(projectV0.viewedCustomPermissions({}), []);
124 assert.deepEqual(projectV0.viewedCustomPermissions({projectV0: {}}), []);
125 assert.deepEqual(projectV0.viewedCustomPermissions(
126 {projectV0: {customPermissions: {}}}), []);
127 const actual = projectV0.viewedCustomPermissions(example.STATE);
128 assert.deepEqual(actual, example.CUSTOM_PERMISSIONS);
129 });
130
131 it('viewedPresentationConfig', () => {
132 assert.deepEqual(projectV0.viewedPresentationConfig({}), {});
133 assert.deepEqual(projectV0.viewedPresentationConfig({projectV0: {}}), {});
134 const actual = projectV0.viewedPresentationConfig(example.STATE);
135 assert.deepEqual(actual, example.PRESENTATION_CONFIG);
136 });
137
138 it('defaultColumns', () => {
139 assert.deepEqual(projectV0.defaultColumns({}), SITEWIDE_DEFAULT_COLUMNS);
140 assert.deepEqual(
141 projectV0.defaultColumns({projectV0: {}}), SITEWIDE_DEFAULT_COLUMNS);
142 assert.deepEqual(
143 projectV0.defaultColumns({projectV0: {presentationConfig: {}}}),
144 SITEWIDE_DEFAULT_COLUMNS);
145 const expected = ['ID', 'Summary', 'AllLabels'];
146 assert.deepEqual(projectV0.defaultColumns(example.STATE), expected);
147 });
148
149 it('defaultQuery', () => {
150 assert.deepEqual(projectV0.defaultQuery({}), '');
151 assert.deepEqual(projectV0.defaultQuery({projectV0: {}}), '');
152 const actual = projectV0.defaultQuery(example.STATE);
153 assert.deepEqual(actual, example.DEFAULT_QUERY);
154 });
155
156 it('fieldDefs', () => {
157 assert.deepEqual(projectV0.fieldDefs({projectV0: {}}), []);
158 assert.deepEqual(projectV0.fieldDefs({projectV0: {config: {}}}), []);
159 const actual = projectV0.fieldDefs(example.STATE);
160 assert.deepEqual(actual, example.FIELD_DEFS);
161 });
162
163 it('labelDefMap', () => {
164 const labelDefs = (permissions) =>
165 restrictionLabelsForPermissions(permissions).map((labelDef) =>
166 [labelDef.label.toLowerCase(), labelDef]);
167
168 assert.deepEqual(
169 projectV0.labelDefMap({projectV0: {}}), new Map(labelDefs([])));
170 assert.deepEqual(
171 projectV0.labelDefMap({projectV0: {config: {}}}), new Map(labelDefs([])));
172 const expected = new Map([
173 ['one', {label: 'One'}],
174 ['enum', {label: 'EnUm'}],
175 ['enum-options', {label: 'eNuM-Options'}],
176 ['hello-world', {label: 'hello-world', docstring: 'hmmm'}],
177 ['hello-me', {label: 'hello-me', docstring: 'hmmm'}],
178 ...labelDefs(example.CUSTOM_PERMISSIONS),
179 ]);
180 assert.deepEqual(projectV0.labelDefMap(example.STATE), expected);
181 });
182
183 it('labelPrefixValueMap', () => {
184 const builtInLabelPrefixes = [
185 ['Restrict', new Set(['View-EditIssue', 'AddIssueComment-EditIssue'])],
186 ['Restrict-View', new Set(['EditIssue'])],
187 ['Restrict-AddIssueComment', new Set(['EditIssue'])],
188 ];
189 assert.deepEqual(projectV0.labelPrefixValueMap({projectV0: {}}),
190 new Map(builtInLabelPrefixes));
191
192 assert.deepEqual(projectV0.labelPrefixValueMap(
193 {projectV0: {config: {}}}), new Map(builtInLabelPrefixes));
194
195 const expected = new Map([
196 ['Restrict', new Set(['View-Google', 'View-Security', 'EditIssue-Google',
197 'EditIssue-Security', 'AddIssueComment-Google',
198 'AddIssueComment-Security', 'DeleteIssue-Google',
199 'DeleteIssue-Security', 'FlagSpam-Google', 'FlagSpam-Security',
200 'View-EditIssue', 'AddIssueComment-EditIssue'])],
201 ['Restrict-View', new Set(['Google', 'Security', 'EditIssue'])],
202 ['Restrict-EditIssue', new Set(['Google', 'Security'])],
203 ['Restrict-AddIssueComment', new Set(['Google', 'Security', 'EditIssue'])],
204 ['Restrict-DeleteIssue', new Set(['Google', 'Security'])],
205 ['Restrict-FlagSpam', new Set(['Google', 'Security'])],
206 ['eNuM', new Set(['Options'])],
207 ['hello', new Set(['world', 'me'])],
208 ]);
209 assert.deepEqual(projectV0.labelPrefixValueMap(example.STATE), expected);
210 });
211
212 it('labelPrefixFields', () => {
213 const fields1 = projectV0.labelPrefixFields({projectV0: {}});
214 assert.deepEqual(fields1, ['Restrict']);
215 const fields2 = projectV0.labelPrefixFields({projectV0: {config: {}}});
216 assert.deepEqual(fields2, ['Restrict']);
217 const expected = [
218 'hello', 'Restrict', 'Restrict-View', 'Restrict-EditIssue',
219 'Restrict-AddIssueComment', 'Restrict-DeleteIssue', 'Restrict-FlagSpam'
220 ];
221 assert.deepEqual(projectV0.labelPrefixFields(example.STATE), expected);
222 });
223
224 it('enumFieldDefs', () => {
225 assert.deepEqual(projectV0.enumFieldDefs({projectV0: {}}), []);
226 assert.deepEqual(projectV0.enumFieldDefs({projectV0: {config: {}}}), []);
227 const expected = [example.FIELD_DEF_ENUM];
228 assert.deepEqual(projectV0.enumFieldDefs(example.STATE), expected);
229 });
230
231 it('optionsPerEnumField', () => {
232 assert.deepEqual(projectV0.optionsPerEnumField({projectV0: {}}), new Map());
233 const expected = new Map([
234 ['enum', [
235 {label: 'eNuM-Options', optionName: 'Options'},
236 ]],
237 ]);
238 assert.deepEqual(projectV0.optionsPerEnumField(example.STATE), expected);
239 });
240
241 it('viewedPresentationConfigLoaded', () => {
242 const loadConfigAction = {
243 type: projectV0.FETCH_PRESENTATION_CONFIG_SUCCESS,
244 projectName: example.PROJECT_NAME,
245 presentationConfig: example.PRESENTATION_CONFIG,
246 };
247 const selectProjectAction = {
248 type: projectV0.SELECT,
249 projectName: example.PROJECT_NAME,
250 };
251 let projectState = {};
252
253 assert.equal(false, projectV0.viewedPresentationConfigLoaded(
254 {projectV0: projectState}));
255
256 projectState = projectV0.reducer(projectState, selectProjectAction);
257 projectState = projectV0.reducer(projectState, loadConfigAction);
258
259 assert.equal(true, projectV0.viewedPresentationConfigLoaded(
260 {projectV0: projectState}));
261 });
262
263 it('fetchingPresentationConfig', () => {
264 const projectState = projectV0.reducer(undefined, {type: null});
265 assert.equal(false,
266 projectState.requests.fetchPresentationConfig.requesting);
267 });
268
269 describe('extractTypeForFieldName', () => {
270 let typeExtractor;
271
272 describe('built-in fields', () => {
273 beforeEach(() => {
274 typeExtractor = projectV0.extractTypeForFieldName({});
275 });
276
277 it('not case sensitive', () => {
278 assert.deepEqual(typeExtractor('id'), fieldTypes.ISSUE_TYPE);
279 assert.deepEqual(typeExtractor('iD'), fieldTypes.ISSUE_TYPE);
280 assert.deepEqual(typeExtractor('Id'), fieldTypes.ISSUE_TYPE);
281 });
282
283 it('gets type for ID', () => {
284 assert.deepEqual(typeExtractor('ID'), fieldTypes.ISSUE_TYPE);
285 });
286
287 it('gets type for Project', () => {
288 assert.deepEqual(typeExtractor('Project'), fieldTypes.PROJECT_TYPE);
289 });
290
291 it('gets type for Attachments', () => {
292 assert.deepEqual(typeExtractor('Attachments'), fieldTypes.INT_TYPE);
293 });
294
295 it('gets type for AllLabels', () => {
296 assert.deepEqual(typeExtractor('AllLabels'), fieldTypes.LABEL_TYPE);
297 });
298
299 it('gets type for AllLabels', () => {
300 assert.deepEqual(typeExtractor('AllLabels'), fieldTypes.LABEL_TYPE);
301 });
302
303 it('gets type for Blocked', () => {
304 assert.deepEqual(typeExtractor('Blocked'), fieldTypes.STR_TYPE);
305 });
306
307 it('gets type for BlockedOn', () => {
308 assert.deepEqual(typeExtractor('BlockedOn'), fieldTypes.ISSUE_TYPE);
309 });
310
311 it('gets type for Blocking', () => {
312 assert.deepEqual(typeExtractor('Blocking'), fieldTypes.ISSUE_TYPE);
313 });
314
315 it('gets type for CC', () => {
316 assert.deepEqual(typeExtractor('CC'), fieldTypes.USER_TYPE);
317 });
318
319 it('gets type for Closed', () => {
320 assert.deepEqual(typeExtractor('Closed'), fieldTypes.TIME_TYPE);
321 });
322
323 it('gets type for Component', () => {
324 assert.deepEqual(typeExtractor('Component'), fieldTypes.COMPONENT_TYPE);
325 });
326
327 it('gets type for ComponentModified', () => {
328 assert.deepEqual(typeExtractor('ComponentModified'),
329 fieldTypes.TIME_TYPE);
330 });
331
332 it('gets type for MergedInto', () => {
333 assert.deepEqual(typeExtractor('MergedInto'), fieldTypes.ISSUE_TYPE);
334 });
335
336 it('gets type for Modified', () => {
337 assert.deepEqual(typeExtractor('Modified'), fieldTypes.TIME_TYPE);
338 });
339
340 it('gets type for Reporter', () => {
341 assert.deepEqual(typeExtractor('Reporter'), fieldTypes.USER_TYPE);
342 });
343
344 it('gets type for Stars', () => {
345 assert.deepEqual(typeExtractor('Stars'), fieldTypes.INT_TYPE);
346 });
347
348 it('gets type for Status', () => {
349 assert.deepEqual(typeExtractor('Status'), fieldTypes.STATUS_TYPE);
350 });
351
352 it('gets type for StatusModified', () => {
353 assert.deepEqual(typeExtractor('StatusModified'), fieldTypes.TIME_TYPE);
354 });
355
356 it('gets type for Summary', () => {
357 assert.deepEqual(typeExtractor('Summary'), fieldTypes.STR_TYPE);
358 });
359
360 it('gets type for Type', () => {
361 assert.deepEqual(typeExtractor('Type'), fieldTypes.ENUM_TYPE);
362 });
363
364 it('gets type for Owner', () => {
365 assert.deepEqual(typeExtractor('Owner'), fieldTypes.USER_TYPE);
366 });
367
368 it('gets type for OwnerModified', () => {
369 assert.deepEqual(typeExtractor('OwnerModified'), fieldTypes.TIME_TYPE);
370 });
371
372 it('gets type for Opened', () => {
373 assert.deepEqual(typeExtractor('Opened'), fieldTypes.TIME_TYPE);
374 });
375 });
376
377 it('gets types for custom fields', () => {
378 typeExtractor = projectV0.extractTypeForFieldName({projectV0: {
379 name: example.PROJECT_NAME,
380 configs: {[example.PROJECT_NAME]: {fieldDefs: [
381 {fieldRef: {fieldName: 'CustomIntField', type: 'INT_TYPE'}},
382 {fieldRef: {fieldName: 'CustomStrField', type: 'STR_TYPE'}},
383 {fieldRef: {fieldName: 'CustomUserField', type: 'USER_TYPE'}},
384 {fieldRef: {fieldName: 'CustomEnumField', type: 'ENUM_TYPE'}},
385 {fieldRef: {fieldName: 'CustomApprovalField',
386 type: 'APPROVAL_TYPE'}},
387 ]}},
388 }});
389
390 assert.deepEqual(typeExtractor('CustomIntField'), fieldTypes.INT_TYPE);
391 assert.deepEqual(typeExtractor('CustomStrField'), fieldTypes.STR_TYPE);
392 assert.deepEqual(typeExtractor('CustomUserField'), fieldTypes.USER_TYPE);
393 assert.deepEqual(typeExtractor('CustomEnumField'), fieldTypes.ENUM_TYPE);
394 assert.deepEqual(typeExtractor('CustomApprovalField'),
395 fieldTypes.APPROVAL_TYPE);
396 });
397
398 it('defaults to string type for other fields', () => {
399 typeExtractor = projectV0.extractTypeForFieldName({projectV0: {
400 name: example.PROJECT_NAME,
401 configs: {[example.PROJECT_NAME]: {fieldDefs: [
402 {fieldRef: {fieldName: 'CustomIntField', type: 'INT_TYPE'}},
403 {fieldRef: {fieldName: 'CustomUserField', type: 'USER_TYPE'}},
404 ]}},
405 }});
406
407 assert.deepEqual(typeExtractor('FakeUserField'), fieldTypes.STR_TYPE);
408 assert.deepEqual(typeExtractor('NotOwner'), fieldTypes.STR_TYPE);
409 });
410 });
411
412 describe('extractFieldValuesFromIssue', () => {
413 let clock;
414 let issue;
415 let fieldExtractor;
416
417 describe('built-in fields', () => {
418 beforeEach(() => {
419 // Built-in fields will always act the same, regardless of
420 // project config.
421 fieldExtractor = projectV0.extractFieldValuesFromIssue({});
422
423 // Set clock to some specified date for relative time.
424 const initialTime = 365 * 24 * 60 * 60;
425
426 issue = {
427 localId: 33,
428 projectName: 'chromium',
429 summary: 'Test summary',
430 attachmentCount: 22,
431 starCount: 2,
432 componentRefs: [{path: 'Infra'}, {path: 'Monorail>UI'}],
433 blockedOnIssueRefs: [{localId: 30, projectName: 'chromium'}],
434 blockingIssueRefs: [{localId: 60, projectName: 'chromium'}],
435 labelRefs: [{label: 'Restrict-View-Google'}, {label: 'Type-Defect'}],
436 reporterRef: {displayName: 'test@example.com'},
437 ccRefs: [{displayName: 'test@example.com'}],
438 ownerRef: {displayName: 'owner@example.com'},
439 closedTimestamp: initialTime - 120, // 2 minutes ago
440 modifiedTimestamp: initialTime - 60, // a minute ago
441 openedTimestamp: initialTime - 24 * 60 * 60, // a day ago
442 componentModifiedTimestamp: initialTime - 60, // a minute ago
443 statusModifiedTimestamp: initialTime - 60, // a minute ago
444 ownerModifiedTimestamp: initialTime - 60, // a minute ago
445 statusRef: {status: 'Duplicate'},
446 mergedIntoIssueRef: {localId: 31, projectName: 'chromium'},
447 };
448
449 clock = sinon.useFakeTimers({
450 now: new Date(initialTime * 1000),
451 shouldAdvanceTime: false,
452 });
453 });
454
455 afterEach(() => {
456 clock.restore();
457 });
458
459 it('computes strings for ID', () => {
460 const fieldName = 'ID';
461
462 assert.deepEqual(fieldExtractor(issue, fieldName),
463 ['chromium:33']);
464 });
465
466 it('computes strings for Project', () => {
467 const fieldName = 'Project';
468
469 assert.deepEqual(fieldExtractor(issue, fieldName),
470 ['chromium']);
471 });
472
473 it('computes strings for Attachments', () => {
474 const fieldName = 'Attachments';
475
476 assert.deepEqual(fieldExtractor(issue, fieldName),
477 ['22']);
478 });
479
480 it('computes strings for AllLabels', () => {
481 const fieldName = 'AllLabels';
482
483 assert.deepEqual(fieldExtractor(issue, fieldName),
484 ['Restrict-View-Google', 'Type-Defect']);
485 });
486
487 it('computes strings for Blocked when issue is blocked', () => {
488 const fieldName = 'Blocked';
489
490 assert.deepEqual(fieldExtractor(issue, fieldName),
491 ['Yes']);
492 });
493
494 it('computes strings for Blocked when issue is not blocked', () => {
495 const fieldName = 'Blocked';
496 issue.blockedOnIssueRefs = [];
497
498 assert.deepEqual(fieldExtractor(issue, fieldName),
499 ['No']);
500 });
501
502 it('computes strings for BlockedOn', () => {
503 const fieldName = 'BlockedOn';
504
505 assert.deepEqual(fieldExtractor(issue, fieldName),
506 ['chromium:30']);
507 });
508
509 it('computes strings for Blocking', () => {
510 const fieldName = 'Blocking';
511
512 assert.deepEqual(fieldExtractor(issue, fieldName),
513 ['chromium:60']);
514 });
515
516 it('computes strings for CC', () => {
517 const fieldName = 'CC';
518
519 assert.deepEqual(fieldExtractor(issue, fieldName),
520 ['test@example.com']);
521 });
522
523 it('computes strings for Closed', () => {
524 const fieldName = 'Closed';
525
526 assert.deepEqual(fieldExtractor(issue, fieldName),
527 ['2 minutes ago']);
528 });
529
530 it('computes strings for Component', () => {
531 const fieldName = 'Component';
532
533 assert.deepEqual(fieldExtractor(issue, fieldName),
534 ['Infra', 'Monorail>UI']);
535 });
536
537 it('computes strings for ComponentModified', () => {
538 const fieldName = 'ComponentModified';
539
540 assert.deepEqual(fieldExtractor(issue, fieldName),
541 ['a minute ago']);
542 });
543
544 it('computes strings for MergedInto', () => {
545 const fieldName = 'MergedInto';
546
547 assert.deepEqual(fieldExtractor(issue, fieldName),
548 ['chromium:31']);
549 });
550
551 it('computes strings for Modified', () => {
552 const fieldName = 'Modified';
553
554 assert.deepEqual(fieldExtractor(issue, fieldName),
555 ['a minute ago']);
556 });
557
558 it('computes strings for Reporter', () => {
559 const fieldName = 'Reporter';
560
561 assert.deepEqual(fieldExtractor(issue, fieldName),
562 ['test@example.com']);
563 });
564
565 it('computes strings for Stars', () => {
566 const fieldName = 'Stars';
567
568 assert.deepEqual(fieldExtractor(issue, fieldName),
569 ['2']);
570 });
571
572 it('computes strings for Status', () => {
573 const fieldName = 'Status';
574
575 assert.deepEqual(fieldExtractor(issue, fieldName),
576 ['Duplicate']);
577 });
578
579 it('computes strings for StatusModified', () => {
580 const fieldName = 'StatusModified';
581
582 assert.deepEqual(fieldExtractor(issue, fieldName),
583 ['a minute ago']);
584 });
585
586 it('computes strings for Summary', () => {
587 const fieldName = 'Summary';
588
589 assert.deepEqual(fieldExtractor(issue, fieldName),
590 ['Test summary']);
591 });
592
593 it('computes strings for Type', () => {
594 const fieldName = 'Type';
595
596 assert.deepEqual(fieldExtractor(issue, fieldName),
597 ['Defect']);
598 });
599
600 it('computes strings for Owner', () => {
601 const fieldName = 'Owner';
602
603 assert.deepEqual(fieldExtractor(issue, fieldName),
604 ['owner@example.com']);
605 });
606
607 it('computes strings for OwnerModified', () => {
608 const fieldName = 'OwnerModified';
609
610 assert.deepEqual(fieldExtractor(issue, fieldName),
611 ['a minute ago']);
612 });
613
614 it('computes strings for Opened', () => {
615 const fieldName = 'Opened';
616
617 assert.deepEqual(fieldExtractor(issue, fieldName),
618 ['a day ago']);
619 });
620 });
621
622 describe('custom approval fields', () => {
623 beforeEach(() => {
624 const fieldDefs = [
625 {fieldRef: {type: 'APPROVAL_TYPE', fieldName: 'Goose-Approval'}},
626 {fieldRef: {type: 'APPROVAL_TYPE', fieldName: 'Chicken-Approval'}},
627 {fieldRef: {type: 'APPROVAL_TYPE', fieldName: 'Dodo-Approval'}},
628 ];
629 fieldExtractor = projectV0.extractFieldValuesFromIssue({
630 projectV0: {
631 name: example.PROJECT_NAME,
632 configs: {
633 [example.PROJECT_NAME]: {
634 projectName: 'chromium',
635 fieldDefs,
636 },
637 },
638 },
639 });
640
641 issue = {
642 localId: 33,
643 projectName: 'bird',
644 approvalValues: [
645 {fieldRef: {type: 'APPROVAL_TYPE', fieldName: 'Goose-Approval'},
646 approverRefs: []},
647 {fieldRef: {type: 'APPROVAL_TYPE', fieldName: 'Chicken-Approval'},
648 status: 'APPROVED'},
649 {fieldRef: {type: 'APPROVAL_TYPE', fieldName: 'Dodo-Approval'},
650 status: 'NEED_INFO', approverRefs: [
651 {displayName: 'kiwi@bird.test'},
652 {displayName: 'mini-dino@bird.test'},
653 ],
654 },
655 ],
656 };
657 });
658
659 it('handles approval approver columns', () => {
660 assert.deepEqual(fieldExtractor(issue, 'goose-approval-approver'), []);
661 assert.deepEqual(fieldExtractor(issue, 'chicken-approval-approver'),
662 []);
663 assert.deepEqual(fieldExtractor(issue, 'dodo-approval-approver'),
664 ['kiwi@bird.test', 'mini-dino@bird.test']);
665 });
666
667 it('handles approval value columns', () => {
668 assert.deepEqual(fieldExtractor(issue, 'goose-approval'), ['NotSet']);
669 assert.deepEqual(fieldExtractor(issue, 'chicken-approval'),
670 ['Approved']);
671 assert.deepEqual(fieldExtractor(issue, 'dodo-approval'),
672 ['NeedInfo']);
673 });
674 });
675
676 describe('custom fields', () => {
677 beforeEach(() => {
678 const fieldDefs = [
679 {fieldRef: {type: 'STR_TYPE', fieldName: 'aString'}},
680 {fieldRef: {type: 'ENUM_TYPE', fieldName: 'ENUM'}},
681 {fieldRef: {type: 'INT_TYPE', fieldName: 'Cow-Number'},
682 bool_is_phase_field: true, is_multivalued: true},
683 ];
684 // As a label prefix, aString conflicts with the custom field named
685 // "aString". In this case, Monorail gives precedence to the
686 // custom field.
687 const labelDefs = [
688 {label: 'aString-ignore'},
689 {label: 'aString-two'},
690 ];
691 fieldExtractor = projectV0.extractFieldValuesFromIssue({
692 projectV0: {
693 name: example.PROJECT_NAME,
694 configs: {
695 [example.PROJECT_NAME]: {
696 projectName: 'chromium',
697 fieldDefs,
698 labelDefs,
699 },
700 },
701 },
702 });
703
704 const fieldValues = [
705 {fieldRef: {type: 'STR_TYPE', fieldName: 'aString'},
706 value: 'test'},
707 {fieldRef: {type: 'STR_TYPE', fieldName: 'aString'},
708 value: 'test2'},
709 {fieldRef: {type: 'ENUM_TYPE', fieldName: 'ENUM'},
710 value: 'a-value'},
711 {fieldRef: {type: 'INT_TYPE', fieldId: '6', fieldName: 'Cow-Number'},
712 phaseRef: {phaseName: 'Cow-Phase'}, value: '55'},
713 {fieldRef: {type: 'INT_TYPE', fieldId: '6', fieldName: 'Cow-Number'},
714 phaseRef: {phaseName: 'Cow-Phase'}, value: '54'},
715 {fieldRef: {type: 'INT_TYPE', fieldId: '6', fieldName: 'Cow-Number'},
716 phaseRef: {phaseName: 'MilkCow-Phase'}, value: '56'},
717 ];
718
719 issue = {
720 localId: 33,
721 projectName: 'chromium',
722 fieldValues,
723 };
724 });
725
726 it('gets values for custom fields', () => {
727 assert.deepEqual(fieldExtractor(issue, 'aString'), ['test', 'test2']);
728 assert.deepEqual(fieldExtractor(issue, 'enum'), ['a-value']);
729 assert.deepEqual(fieldExtractor(issue, 'cow-phase.cow-number'),
730 ['55', '54']);
731 assert.deepEqual(fieldExtractor(issue, 'milkcow-phase.cow-number'),
732 ['56']);
733 });
734
735 it('custom fields get precedence over label fields', () => {
736 issue.labelRefs = [{label: 'aString-ignore'}];
737 assert.deepEqual(fieldExtractor(issue, 'aString'),
738 ['test', 'test2']);
739 });
740 });
741
742 describe('label prefix fields', () => {
743 beforeEach(() => {
744 issue = {
745 localId: 33,
746 projectName: 'chromium',
747 labelRefs: [
748 {label: 'test-label'},
749 {label: 'test-label-2'},
750 {label: 'ignore-me'},
751 {label: 'Milestone-UI'},
752 {label: 'Milestone-Goodies'},
753 ],
754 };
755
756 fieldExtractor = projectV0.extractFieldValuesFromIssue({
757 projectV0: {
758 name: example.PROJECT_NAME,
759 configs: {
760 [example.PROJECT_NAME]: {
761 projectName: 'chromium',
762 labelDefs: [
763 {label: 'test-1'},
764 {label: 'test-2'},
765 {label: 'milestone-1'},
766 {label: 'milestone-2'},
767 ],
768 },
769 },
770 },
771 });
772 });
773
774 it('gets values for label prefixes', () => {
775 assert.deepEqual(fieldExtractor(issue, 'test'), ['label', 'label-2']);
776 assert.deepEqual(fieldExtractor(issue, 'Milestone'), ['UI', 'Goodies']);
777 });
778 });
779 });
780
781 it('fieldDefsByApprovalName', () => {
782 assert.deepEqual(projectV0.fieldDefsByApprovalName({projectV0: {}}),
783 new Map());
784
785 assert.deepEqual(projectV0.fieldDefsByApprovalName({projectV0: {
786 name: example.PROJECT_NAME,
787 configs: {[example.PROJECT_NAME]: {
788 fieldDefs: [
789 {fieldRef: {fieldName: 'test', type: fieldTypes.INT_TYPE}},
790 {fieldRef: {fieldName: 'ignoreMe', type: fieldTypes.APPROVAL_TYPE}},
791 {fieldRef: {fieldName: 'yay', approvalName: 'ThisIsAnApproval'}},
792 {fieldRef: {fieldName: 'ImAField', approvalName: 'ThisIsAnApproval'}},
793 {fieldRef: {fieldName: 'TalkToALawyer', approvalName: 'Legal'}},
794 ],
795 }},
796 }}), new Map([
797 ['ThisIsAnApproval', [
798 {fieldRef: {fieldName: 'yay', approvalName: 'ThisIsAnApproval'}},
799 {fieldRef: {fieldName: 'ImAField', approvalName: 'ThisIsAnApproval'}},
800 ]],
801 ['Legal', [
802 {fieldRef: {fieldName: 'TalkToALawyer', approvalName: 'Legal'}},
803 ]],
804 ]));
805 });
806});
807
808let dispatch;
809
810describe('project action creators', () => {
811 beforeEach(() => {
812 sinon.stub(prpcClient, 'call');
813
814 dispatch = sinon.stub();
815 });
816
817 afterEach(() => {
818 prpcClient.call.restore();
819 });
820
821 it('select', () => {
822 projectV0.select('project-name')(dispatch);
823 const action = {type: projectV0.SELECT, projectName: 'project-name'};
824 sinon.assert.calledWith(dispatch, action);
825 });
826
827 it('fetchCustomPermissions', async () => {
828 const action = projectV0.fetchCustomPermissions('chromium');
829
830 prpcClient.call.returns(Promise.resolve({permissions: ['google']}));
831
832 await action(dispatch);
833
834 sinon.assert.calledWith(dispatch,
835 {type: projectV0.FETCH_CUSTOM_PERMISSIONS_START});
836
837 sinon.assert.calledWith(
838 prpcClient.call,
839 'monorail.Projects',
840 'GetCustomPermissions',
841 {projectName: 'chromium'});
842
843 sinon.assert.calledWith(dispatch, {
844 type: projectV0.FETCH_CUSTOM_PERMISSIONS_SUCCESS,
845 projectName: 'chromium',
846 permissions: ['google'],
847 });
848 });
849
850 it('fetchPresentationConfig', async () => {
851 const action = projectV0.fetchPresentationConfig('chromium');
852
853 prpcClient.call.returns(Promise.resolve({projectThumbnailUrl: 'test'}));
854
855 await action(dispatch);
856
857 sinon.assert.calledWith(dispatch,
858 {type: projectV0.FETCH_PRESENTATION_CONFIG_START});
859
860 sinon.assert.calledWith(
861 prpcClient.call,
862 'monorail.Projects',
863 'GetPresentationConfig',
864 {projectName: 'chromium'});
865
866 sinon.assert.calledWith(dispatch, {
867 type: projectV0.FETCH_PRESENTATION_CONFIG_SUCCESS,
868 projectName: 'chromium',
869 presentationConfig: {projectThumbnailUrl: 'test'},
870 });
871 });
872
873 it('fetchVisibleMembers', async () => {
874 const action = projectV0.fetchVisibleMembers('chromium');
875
876 prpcClient.call.returns(Promise.resolve({userRefs: [{userId: '123'}]}));
877
878 await action(dispatch);
879
880 sinon.assert.calledWith(dispatch,
881 {type: projectV0.FETCH_VISIBLE_MEMBERS_START});
882
883 sinon.assert.calledWith(
884 prpcClient.call,
885 'monorail.Projects',
886 'GetVisibleMembers',
887 {projectName: 'chromium'});
888
889 sinon.assert.calledWith(dispatch, {
890 type: projectV0.FETCH_VISIBLE_MEMBERS_SUCCESS,
891 projectName: 'chromium',
892 visibleMembers: {userRefs: [{userId: '123'}]},
893 });
894 });
895});
896
897describe('helpers', () => {
898 beforeEach(() => {
899 sinon.stub(prpcClient, 'call');
900 });
901
902 afterEach(() => {
903 prpcClient.call.restore();
904 });
905
906 describe('fetchFieldPerms', () => {
907 it('fetch field permissions', async () => {
908 const projectName = 'proj';
909 const fieldDefs = [
910 {
911 fieldRef: {
912 fieldName: 'testField',
913 fieldId: 1,
914 type: 'ENUM_TYPE',
915 },
916 },
917 ];
918 const response = {};
919 prpcClient.call.returns(Promise.resolve(response));
920
921 await store.dispatch(projectV0.fetchFieldPerms(projectName, fieldDefs));
922
923 const args = {names: ['projects/proj/fieldDefs/1']};
924 sinon.assert.calledWith(
925 prpcClient.call, 'monorail.v3.Permissions',
926 'BatchGetPermissionSets', args);
927 });
928
929 it('fetch with no fieldDefs', async () => {
930 const config = {projectName: 'proj'};
931 const response = {};
932 prpcClient.call.returns(Promise.resolve(response));
933
934 // fieldDefs will be undefined.
935 await store.dispatch(projectV0.fetchFieldPerms(
936 config.projectName, config.fieldDefs));
937
938 const args = {names: []};
939 sinon.assert.calledWith(
940 prpcClient.call, 'monorail.v3.Permissions',
941 'BatchGetPermissionSets', args);
942 });
943 });
944});