Skip to content

Commit b06d0d6

Browse files
[Test Improver] Add unit tests for role-template and cron-schedule validators (#17681)
* test: add unit tests for role-template and cron-schedule validators Add 19 unit tests covering: - roleTemplateRules: all branching logic including RBAC.ROLE-specific checks, noResourceAndNonResource, missingVerb, missingOneResource, and default parameter handling - cronSchedule / cronScheduleRule: valid/invalid cron expressions, cronstrue mock, default parameter, error accumulation Coverage: 0% → 100% stmts/branches/fns/lines for both files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: trigger checks --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 386c1c1 commit b06d0d6

2 files changed

Lines changed: 228 additions & 0 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import cronstrue from 'cronstrue';
2+
import { cronSchedule, cronScheduleRule } from '@shell/utils/validators/cron-schedule';
3+
4+
jest.mock('cronstrue', () => ({ toString: jest.fn() }));
5+
6+
const mockCronstrue = cronstrue as jest.Mocked<typeof cronstrue>;
7+
const mockGetters = { 'i18n/t': (key: string) => key };
8+
9+
describe('cronScheduleRule', () => {
10+
it('calls cronstrue.toString with verbose:true', () => {
11+
mockCronstrue.toString.mockReturnValue('every minute');
12+
cronScheduleRule.validation('* * * * *');
13+
14+
expect(mockCronstrue.toString).toHaveBeenCalledWith('* * * * *', { verbose: true });
15+
});
16+
17+
it('has the correct message key', () => {
18+
expect(cronScheduleRule.message).toStrictEqual('validation.invalidCron');
19+
});
20+
});
21+
22+
describe('cronSchedule', () => {
23+
beforeEach(() => {
24+
jest.clearAllMocks();
25+
});
26+
27+
it('does not add an error for a valid cron expression', () => {
28+
mockCronstrue.toString.mockReturnValue('every minute');
29+
const errors: string[] = [];
30+
31+
cronSchedule('* * * * *', mockGetters, errors);
32+
33+
expect(errors).toStrictEqual([]);
34+
});
35+
36+
it('adds an error for an invalid cron expression', () => {
37+
mockCronstrue.toString.mockImplementation(() => {
38+
throw new Error('Invalid cron expression');
39+
});
40+
const errors: string[] = [];
41+
42+
cronSchedule('not-a-cron', mockGetters, errors);
43+
44+
expect(errors).toStrictEqual(['validation.invalidCron']);
45+
});
46+
47+
it('uses default empty string for schedule when not provided', () => {
48+
mockCronstrue.toString.mockImplementation(() => {
49+
throw new Error('Invalid cron expression');
50+
});
51+
const errors: string[] = [];
52+
53+
cronSchedule(undefined as any, mockGetters, errors);
54+
55+
expect(errors).toStrictEqual(['validation.invalidCron']);
56+
});
57+
58+
it('does not push multiple errors for a single invalid schedule', () => {
59+
mockCronstrue.toString.mockImplementation(() => {
60+
throw new Error('Invalid');
61+
});
62+
const errors: string[] = [];
63+
64+
cronSchedule('bad', mockGetters, errors);
65+
66+
expect(errors).toHaveLength(1);
67+
});
68+
69+
it('preserves existing errors when adding a new one', () => {
70+
mockCronstrue.toString.mockImplementation(() => {
71+
throw new Error('Invalid');
72+
});
73+
const errors = ['existing error'];
74+
75+
cronSchedule('bad', mockGetters, errors);
76+
77+
expect(errors).toStrictEqual(['existing error', 'validation.invalidCron']);
78+
});
79+
});
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { roleTemplateRules } from '@shell/utils/validators/role-template';
2+
import { RBAC } from '@shell/config/types';
3+
4+
const mockGetters = { 'i18n/t': (key: string) => key };
5+
6+
describe('roleTemplateRules', () => {
7+
it('adds no errors for valid rules with no type', () => {
8+
const rules = [{
9+
verbs: ['get'], resources: ['pods'], apiGroups: ['']
10+
}];
11+
const errors: string[] = [];
12+
13+
roleTemplateRules(rules, mockGetters, errors);
14+
15+
expect(errors).toStrictEqual([]);
16+
});
17+
18+
it('adds missingVerb error when a rule has empty verbs', () => {
19+
const rules = [{
20+
verbs: [], resources: ['pods'], apiGroups: ['']
21+
}];
22+
const errors: string[] = [];
23+
24+
roleTemplateRules(rules, mockGetters, errors);
25+
26+
expect(errors).toStrictEqual(['validation.roleTemplate.roleTemplateRules.missingVerb']);
27+
});
28+
29+
it('adds noResourceAndNonResource error when a rule has both resources and nonResourceURLs', () => {
30+
const rules = [{
31+
verbs: ['get'],
32+
resources: ['pods'],
33+
nonResourceURLs: ['/healthz'],
34+
apiGroups: [''],
35+
}];
36+
const errors: string[] = [];
37+
38+
roleTemplateRules(rules, mockGetters, errors);
39+
40+
expect(errors).toStrictEqual(['validation.roleTemplate.roleTemplateRules.noResourceAndNonResource']);
41+
});
42+
43+
it('adds missingResource error for RBAC.ROLE type when resources are empty', () => {
44+
const rules = [{
45+
verbs: ['get'], resources: [], nonResourceURLs: ['/healthz'], apiGroups: ['']
46+
}];
47+
const errors: string[] = [];
48+
49+
roleTemplateRules(rules, mockGetters, errors, [RBAC.ROLE]);
50+
51+
expect(errors).toStrictEqual(['validation.roleTemplate.roleTemplateRules.missingResource']);
52+
});
53+
54+
it('adds missingApiGroup error for RBAC.ROLE type when apiGroups are empty', () => {
55+
const rules = [{
56+
verbs: ['get'], resources: ['pods'], apiGroups: []
57+
}];
58+
const errors: string[] = [];
59+
60+
roleTemplateRules(rules, mockGetters, errors, [RBAC.ROLE]);
61+
62+
expect(errors).toStrictEqual(['validation.roleTemplate.roleTemplateRules.missingApiGroup']);
63+
});
64+
65+
it('adds noResourceAndNonResource error for non-RBAC.ROLE when rule has resources and nonResourceUrls', () => {
66+
const rules = [{
67+
verbs: ['get'],
68+
resources: ['pods'],
69+
nonResourceUrls: ['/healthz'],
70+
apiGroups: [''],
71+
}];
72+
const errors: string[] = [];
73+
74+
roleTemplateRules(rules, mockGetters, errors, [RBAC.CLUSTER_ROLE]);
75+
76+
expect(errors).toStrictEqual(['validation.roleTemplate.roleTemplateRules.noResourceAndNonResource']);
77+
});
78+
79+
it('adds missingOneResource error when rule has neither resources nor nonResourceURLs', () => {
80+
const rules = [{
81+
verbs: ['get'], resources: [], nonResourceURLs: [], apiGroups: []
82+
}];
83+
const errors: string[] = [];
84+
85+
roleTemplateRules(rules, mockGetters, errors, [RBAC.CLUSTER_ROLE]);
86+
87+
expect(errors).toStrictEqual(['validation.roleTemplate.roleTemplateRules.missingOneResource']);
88+
});
89+
90+
it('adds multiple errors when multiple rules are invalid', () => {
91+
const rules = [
92+
{
93+
verbs: [], resources: ['pods'], apiGroups: ['']
94+
},
95+
{
96+
verbs: ['get'], resources: [], nonResourceURLs: [], apiGroups: []
97+
},
98+
];
99+
const errors: string[] = [];
100+
101+
roleTemplateRules(rules, mockGetters, errors, [RBAC.CLUSTER_ROLE]);
102+
103+
expect(errors).toStrictEqual([
104+
'validation.roleTemplate.roleTemplateRules.missingVerb',
105+
'validation.roleTemplate.roleTemplateRules.missingOneResource',
106+
]);
107+
});
108+
109+
it('handles empty rules array with no errors', () => {
110+
const errors: string[] = [];
111+
112+
roleTemplateRules([], mockGetters, errors);
113+
114+
expect(errors).toStrictEqual([]);
115+
});
116+
117+
it('uses default empty array for rules when not provided', () => {
118+
const errors: string[] = [];
119+
120+
roleTemplateRules(undefined as any, mockGetters, errors);
121+
122+
expect(errors).toStrictEqual([]);
123+
});
124+
125+
it('adds both missingResource and missingApiGroup errors for RBAC.ROLE with empty resources and apiGroups', () => {
126+
const rules = [{
127+
verbs: ['get'], resources: [], nonResourceURLs: ['/healthz'], apiGroups: []
128+
}];
129+
const errors: string[] = [];
130+
131+
roleTemplateRules(rules, mockGetters, errors, [RBAC.ROLE]);
132+
133+
expect(errors).toStrictEqual([
134+
'validation.roleTemplate.roleTemplateRules.missingResource',
135+
'validation.roleTemplate.roleTemplateRules.missingApiGroup',
136+
]);
137+
});
138+
139+
it('does not add RBAC.ROLE-specific errors when type is not RBAC.ROLE', () => {
140+
const rules = [{
141+
verbs: ['get'], resources: ['pods'], apiGroups: ['']
142+
}];
143+
const errors: string[] = [];
144+
145+
roleTemplateRules(rules, mockGetters, errors, [RBAC.CLUSTER_ROLE]);
146+
147+
expect(errors).toStrictEqual([]);
148+
});
149+
});

0 commit comments

Comments
 (0)