Skip to content

Commit 143ff28

Browse files
[Test Improver] Add unit tests for pod-affinity, prometheusrule, and misc validators (#17658)
* test: add tests for pod-affinity, prometheusrule, and misc validators Add 79 unit tests covering: - pod-affinity.js: affinity/anti-affinity preferred/required scheduling, weight validation (1-100), topologyKey validation, labelSelector matchExpression operators (In/NotIn/Exists/DoesNotExist), error context - prometheusrule.js: ruleGroups required validation, groupsAreValid (name, rules, alert/record/expr/labels fields, 1-based indexing) - container-images.js: podSpec path (template and cronjob jobTemplate), container image presence validation - flow-output.js: verifyLocal flag, local/global ref combinations - logging-outputs.js: logdna api_key validation - monitoring-route.js: matching spec validation, interval format validation 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 b4adb11 commit 143ff28

3 files changed

Lines changed: 839 additions & 0 deletions

File tree

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import { containerImages } from '@shell/utils/validators/container-images';
2+
import { flowOutput } from '@shell/utils/validators/flow-output';
3+
import { logdna } from '@shell/utils/validators/logging-outputs';
4+
import { matching, interval } from '@shell/utils/validators/monitoring-route';
5+
6+
const mockGetters = {
7+
'i18n/t': (key: string, args?: object) => (args ? `${ key }:${ JSON.stringify(args) }` : key),
8+
'i18n/exists': () => false,
9+
};
10+
11+
describe('validators/container-images', () => {
12+
describe('containerImages', () => {
13+
it('adds error when containers list is missing', () => {
14+
const errors: string[] = [];
15+
const spec = { template: { spec: {} } };
16+
17+
containerImages(spec, mockGetters, errors);
18+
expect(errors.some((e) => e.includes('validation.required'))).toBe(true);
19+
});
20+
21+
it('adds error when containers list is empty', () => {
22+
const errors: string[] = [];
23+
const spec = { template: { spec: { containers: [] } } };
24+
25+
containerImages(spec, mockGetters, errors);
26+
expect(errors.some((e) => e.includes('validation.required'))).toBe(true);
27+
});
28+
29+
it('adds error when a container has no image', () => {
30+
const errors: string[] = [];
31+
const spec = { template: { spec: { containers: [{ name: 'web', image: '' }] } } };
32+
33+
containerImages(spec, mockGetters, errors);
34+
expect(errors.some((e) => e.includes('workload.validation.containerImage'))).toBe(true);
35+
});
36+
37+
it('includes container name in error message', () => {
38+
const errors: string[] = [];
39+
const spec = { template: { spec: { containers: [{ name: 'my-container', image: '' }] } } };
40+
41+
containerImages(spec, mockGetters, errors);
42+
expect(errors.some((e) => e.includes('my-container'))).toBe(true);
43+
});
44+
45+
it('does not add error when all containers have images', () => {
46+
const errors: string[] = [];
47+
const spec = { template: { spec: { containers: [{ name: 'web', image: 'nginx:latest' }] } } };
48+
49+
containerImages(spec, mockGetters, errors);
50+
expect(errors).toStrictEqual([]);
51+
});
52+
53+
it('adds error for each container missing an image', () => {
54+
const errors: string[] = [];
55+
const spec = {
56+
template: {
57+
spec: {
58+
containers: [
59+
{ name: 'c1', image: '' },
60+
{ name: 'c2', image: 'nginx' },
61+
{ name: 'c3', image: '' },
62+
]
63+
}
64+
}
65+
};
66+
67+
containerImages(spec, mockGetters, errors);
68+
expect(errors.filter((e) => e.includes('workload.validation.containerImage'))).toHaveLength(2);
69+
});
70+
71+
it('reads podSpec from cronjob jobTemplate path', () => {
72+
const errors: string[] = [];
73+
const spec = { jobTemplate: { spec: { template: { spec: { containers: [] } } } } };
74+
75+
containerImages(spec, mockGetters, errors);
76+
expect(errors.some((e) => e.includes('validation.required'))).toBe(true);
77+
});
78+
79+
it('does not error for cronjob with valid containers', () => {
80+
const errors: string[] = [];
81+
const spec = { jobTemplate: { spec: { template: { spec: { containers: [{ name: 'job', image: 'busybox' }] } } } } };
82+
83+
containerImages(spec, mockGetters, errors);
84+
expect(errors).toStrictEqual([]);
85+
});
86+
});
87+
});
88+
89+
describe('validators/flow-output', () => {
90+
describe('flowOutput', () => {
91+
it('adds error when verifyLocal and both refs are empty', () => {
92+
const errors: string[] = [];
93+
94+
flowOutput({ localOutputRefs: [], globalOutputRefs: [] }, mockGetters, errors, ['verifyLocal']);
95+
expect(errors).toStrictEqual(['validation.flowOutput.both']);
96+
});
97+
98+
it('does not add error when verifyLocal and localOutputRefs is populated', () => {
99+
const errors: string[] = [];
100+
101+
flowOutput({ localOutputRefs: ['ref1'], globalOutputRefs: [] }, mockGetters, errors, ['verifyLocal']);
102+
expect(errors).toStrictEqual([]);
103+
});
104+
105+
it('does not add error when verifyLocal and globalOutputRefs is populated', () => {
106+
const errors: string[] = [];
107+
108+
flowOutput({ localOutputRefs: [], globalOutputRefs: ['ref1'] }, mockGetters, errors, ['verifyLocal']);
109+
expect(errors).toStrictEqual([]);
110+
});
111+
112+
it('adds error when not verifyLocal and globalOutputRefs is empty', () => {
113+
const errors: string[] = [];
114+
115+
flowOutput({ localOutputRefs: ['local'], globalOutputRefs: [] }, mockGetters, errors, []);
116+
expect(errors).toStrictEqual(['validation.flowOutput.global']);
117+
});
118+
119+
it('does not add error when not verifyLocal and globalOutputRefs is populated', () => {
120+
const errors: string[] = [];
121+
122+
flowOutput({ localOutputRefs: [], globalOutputRefs: ['global'] }, mockGetters, errors, []);
123+
expect(errors).toStrictEqual([]);
124+
});
125+
126+
it('uses default empty array when localOutputRefs is missing', () => {
127+
const errors: string[] = [];
128+
129+
flowOutput({ globalOutputRefs: [] }, mockGetters, errors, ['verifyLocal']);
130+
expect(errors).toStrictEqual(['validation.flowOutput.both']);
131+
});
132+
});
133+
});
134+
135+
describe('validators/logging-outputs', () => {
136+
describe('logdna', () => {
137+
it('returns without error when value is empty', () => {
138+
const errors: string[] = [];
139+
140+
logdna({}, mockGetters, errors, []);
141+
expect(errors).toStrictEqual([]);
142+
});
143+
144+
it('returns without error when value is null', () => {
145+
const errors: string[] = [];
146+
147+
logdna(null, mockGetters, errors, []);
148+
expect(errors).toStrictEqual([]);
149+
});
150+
151+
it('adds error when api_key is missing', () => {
152+
const errors: string[] = [];
153+
154+
logdna({ host: 'example.com' }, mockGetters, errors, []);
155+
expect(errors).toStrictEqual(['validation.output.logdna.apiKey']);
156+
});
157+
158+
it('adds error when api_key is empty string', () => {
159+
const errors: string[] = [];
160+
161+
logdna({ api_key: '' }, mockGetters, errors, []);
162+
expect(errors).toStrictEqual(['validation.output.logdna.apiKey']);
163+
});
164+
165+
it('does not add error when api_key is provided', () => {
166+
const errors: string[] = [];
167+
168+
logdna({ api_key: 'my-key-123' }, mockGetters, errors, []);
169+
expect(errors).toStrictEqual([]);
170+
});
171+
});
172+
});
173+
174+
describe('validators/monitoring-route', () => {
175+
describe('matching', () => {
176+
it('adds error when both match and match_re are missing', () => {
177+
const errors: string[] = [];
178+
179+
matching({}, mockGetters, errors, []);
180+
expect(errors).toStrictEqual(['validation.monitoring.route.match']);
181+
});
182+
183+
it('adds error when both match and match_re are empty', () => {
184+
const errors: string[] = [];
185+
186+
matching({ match: {}, match_re: {} }, mockGetters, errors, []);
187+
expect(errors).toStrictEqual(['validation.monitoring.route.match']);
188+
});
189+
190+
it('does not add error when match has entries', () => {
191+
const errors: string[] = [];
192+
193+
matching({ match: { alertname: 'myAlert' } }, mockGetters, errors, []);
194+
expect(errors).toStrictEqual([]);
195+
});
196+
197+
it('does not add error when match_re has entries', () => {
198+
const errors: string[] = [];
199+
200+
matching({ match_re: { alertname: '.*' } }, mockGetters, errors, []);
201+
expect(errors).toStrictEqual([]);
202+
});
203+
204+
it('does not add error when spec is null', () => {
205+
const errors: string[] = [];
206+
207+
matching(null, mockGetters, errors, []);
208+
expect(errors).toStrictEqual(['validation.monitoring.route.match']);
209+
});
210+
});
211+
212+
describe('interval', () => {
213+
it.each([
214+
['30s', true],
215+
['5m', true],
216+
['2h', true],
217+
['0s', true],
218+
])('accepts valid interval %s', (value, _valid) => {
219+
const errors: string[] = [];
220+
221+
interval(value, mockGetters, errors, [], 'Group Wait');
222+
expect(errors).toStrictEqual([]);
223+
});
224+
225+
it.each([
226+
['30'],
227+
['5min'],
228+
['2 h'],
229+
['abc'],
230+
[''],
231+
['1d'],
232+
])('rejects invalid interval %s', (value) => {
233+
const errors: string[] = [];
234+
235+
interval(value, mockGetters, errors, [], 'Group Wait');
236+
expect(errors.some((e) => e.includes('validation.monitoring.route.interval'))).toBe(true);
237+
});
238+
239+
it('includes displayKey in error message', () => {
240+
const errors: string[] = [];
241+
242+
interval('invalid', mockGetters, errors, [], 'RepeatInterval');
243+
expect(errors.some((e) => e.includes('RepeatInterval'))).toBe(true);
244+
});
245+
});
246+
});

0 commit comments

Comments
 (0)