Skip to content

Commit 63925bf

Browse files
github-actions[bot]Copilotnwmac
authored
[Test Improver] test: add unit tests for shell/utils/validators/service.js (#17614)
* test: add unit tests for shell/utils/validators/service.js 30 tests covering servicePort, clusterIp, and externalName validators: - servicePort: ExternalName bypass, empty ports, multi-port name requirement, nodePort/port/targetPort validation (int, range, IANA service name), DNS label name validation - clusterIp: type-based bypass (ExternalName, unsupported types) - externalName: missing name, valid/invalid hostnames, error merging Coverage: 97.05% stmts, 93.75% branches, 100% fns, 97.05% lines Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Bump ci/cd --------- 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: Neil MacDougall <nmacdougall@suse.com>
1 parent 54cd1d2 commit 63925bf

1 file changed

Lines changed: 283 additions & 0 deletions

File tree

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
import { servicePort, clusterIp, externalName } from '@shell/utils/validators/service';
2+
3+
const mockGetters = {
4+
'i18n/t': (key: string, args?: object) => (args ? `${ key }:${ JSON.stringify(args) }` : key),
5+
'i18n/exists': () => false,
6+
};
7+
8+
describe('validators/service', () => {
9+
describe('servicePort', () => {
10+
it('returns errors unchanged when serviceType is ExternalName', () => {
11+
const errors: string[] = [];
12+
const result = servicePort({ type: 'ExternalName', ports: [] }, mockGetters, errors, {});
13+
14+
expect(result).toStrictEqual([]);
15+
});
16+
17+
it('adds required error when ports is empty', () => {
18+
const errors: string[] = [];
19+
const result = servicePort({ type: 'ClusterIP', ports: [] }, mockGetters, errors, {});
20+
21+
expect(result).toStrictEqual(['validation.required:{"key":"Port Rules"}']);
22+
});
23+
24+
it('adds required error when ports is null', () => {
25+
const errors: string[] = [];
26+
const result = servicePort({ type: 'ClusterIP', ports: null }, mockGetters, errors, {});
27+
28+
expect(result).toContain('validation.required:{"key":"Port Rules"}');
29+
});
30+
31+
it('requires port name when there are multiple ports', () => {
32+
const errors: string[] = [];
33+
const ports = [
34+
{
35+
name: '', port: 80, targetPort: 8080, nodePort: null
36+
},
37+
{
38+
name: '', port: 443, targetPort: 8443, nodePort: null
39+
},
40+
];
41+
const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
42+
43+
expect(result).toContain('validation.service.ports.name.required:{"position":1}');
44+
expect(result).toContain('validation.service.ports.name.required:{"position":2}');
45+
});
46+
47+
it('does not require name when only one port', () => {
48+
const errors: string[] = [];
49+
const ports = [{
50+
name: '', port: 80, targetPort: 8080
51+
}];
52+
const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
53+
54+
expect(result).not.toContain('validation.service.ports.name.required:{"position":1}');
55+
});
56+
57+
it('adds error when nodePort is not a valid integer', () => {
58+
const errors: string[] = [];
59+
const ports = [{
60+
name: 'http', nodePort: 'abc', port: 80, targetPort: 8080
61+
}];
62+
const result = servicePort({ type: 'NodePort', ports }, mockGetters, errors, {});
63+
64+
expect(result).toContain('validation.service.ports.nodePort.requiredInt:{"position":1}');
65+
});
66+
67+
it('does not add nodePort error when nodePort is a valid integer string', () => {
68+
const errors: string[] = [];
69+
const ports = [{
70+
name: 'http', nodePort: '30000', port: 80, targetPort: 8080
71+
}];
72+
const result = servicePort({ type: 'NodePort', ports }, mockGetters, errors, {});
73+
74+
expect(result).not.toContain('validation.service.ports.nodePort.requiredInt:{"position":1}');
75+
});
76+
77+
it('does not add nodePort error when nodePort is falsy', () => {
78+
const errors: string[] = [];
79+
const ports = [{
80+
name: 'http', nodePort: null, port: 80, targetPort: 8080
81+
}];
82+
const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
83+
84+
expect(result).not.toContain('validation.service.ports.nodePort.requiredInt:{"position":1}');
85+
});
86+
87+
it('adds error when port is not a valid integer', () => {
88+
const errors: string[] = [];
89+
const ports = [{
90+
name: 'http', port: 'notanum', targetPort: 8080
91+
}];
92+
const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
93+
94+
expect(result).toContain('validation.service.ports.port.requiredInt:{"position":1}');
95+
});
96+
97+
it('adds required error when port is missing', () => {
98+
const errors: string[] = [];
99+
const ports = [{
100+
name: 'http', port: null, targetPort: 8080
101+
}];
102+
const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
103+
104+
expect(result).toContain('validation.service.ports.port.required:{"position":1}');
105+
});
106+
107+
it('adds required error when targetPort is missing', () => {
108+
const errors: string[] = [];
109+
const ports = [{
110+
name: 'http', port: 80, targetPort: null
111+
}];
112+
const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
113+
114+
expect(result).toContain('validation.service.ports.targetPort.required:{"position":1}');
115+
});
116+
117+
it('does not add error for valid numeric targetPort within range', () => {
118+
const errors: string[] = [];
119+
const ports = [{
120+
name: 'http', port: 80, targetPort: '8080'
121+
}];
122+
const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
123+
124+
expect(result).not.toContain('validation.service.ports.targetPort.between:{"position":1}');
125+
});
126+
127+
it('adds error when numeric targetPort is out of range (below 1)', () => {
128+
const errors: string[] = [];
129+
const ports = [{
130+
name: 'http', port: 80, targetPort: '0'
131+
}];
132+
const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
133+
134+
expect(result).toContain('validation.service.ports.targetPort.between:{"position":1}');
135+
});
136+
137+
it('adds error when numeric targetPort is out of range (above 65535)', () => {
138+
const errors: string[] = [];
139+
const ports = [{
140+
name: 'http', port: 80, targetPort: '65536'
141+
}];
142+
const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
143+
144+
expect(result).toContain('validation.service.ports.targetPort.between:{"position":1}');
145+
});
146+
147+
it('validates IANA service name for non-numeric targetPort', () => {
148+
const errors: string[] = [];
149+
// valid IANA name: alphanumeric+hyphen, not starting/ending with hyphen, contains letter
150+
const ports = [{
151+
name: 'http', port: 80, targetPort: 'valid-name'
152+
}];
153+
const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
154+
155+
// valid IANA name should not add errors for targetPort
156+
const targetPortErrors = result.filter((e: string) => e.includes('targetPort'));
157+
158+
expect(targetPortErrors).toHaveLength(0);
159+
});
160+
161+
it('adds error for invalid IANA service name (too long)', () => {
162+
const errors: string[] = [];
163+
const ports = [{
164+
name: 'http', port: 80, targetPort: 'a-very-long-name-here'
165+
}]; // > 15 chars
166+
const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
167+
168+
const targetPortErrors = result.filter((e: string) => e.includes('targetPort') || e.includes('length'));
169+
170+
expect(targetPortErrors.length).toBeGreaterThan(0);
171+
});
172+
173+
it('validates port name using DNS label rules', () => {
174+
const errors: string[] = [];
175+
const ports = [{
176+
name: '-invalid', port: 80, targetPort: 8080
177+
}];
178+
const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
179+
180+
const nameErrors = result.filter((e: string) => e.includes('startHyphen') || e.includes('name'));
181+
182+
expect(nameErrors.length).toBeGreaterThan(0);
183+
});
184+
185+
it('returns no errors for a valid single port spec', () => {
186+
const errors: string[] = [];
187+
const ports = [{
188+
name: 'http', port: 80, targetPort: '8080'
189+
}];
190+
const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
191+
192+
expect(result).toStrictEqual([]);
193+
});
194+
});
195+
196+
describe('clusterIp', () => {
197+
it('returns errors unchanged for ExternalName service type', () => {
198+
const errors = ['existing'];
199+
const result = clusterIp({ type: 'ExternalName' }, mockGetters, errors, {});
200+
201+
expect(result).toStrictEqual(['existing']);
202+
});
203+
204+
it('returns errors unchanged for ClusterIP type (no additional validation)', () => {
205+
const errors: string[] = [];
206+
const result = clusterIp({ type: 'ClusterIP' }, mockGetters, errors, {});
207+
208+
expect(result).toStrictEqual([]);
209+
});
210+
211+
it('returns errors unchanged for NodePort type', () => {
212+
const errors: string[] = [];
213+
const result = clusterIp({ type: 'NodePort' }, mockGetters, errors, {});
214+
215+
expect(result).toStrictEqual([]);
216+
});
217+
218+
it('returns errors unchanged for LoadBalancer type', () => {
219+
const errors: string[] = [];
220+
const result = clusterIp({ type: 'LoadBalancer' }, mockGetters, errors, {});
221+
222+
expect(result).toStrictEqual([]);
223+
});
224+
225+
it('skips validation for unsupported service types', () => {
226+
const errors = ['pre-existing'];
227+
const result = clusterIp({ type: 'Headless' }, mockGetters, errors, {});
228+
229+
expect(result).toStrictEqual(['pre-existing']);
230+
});
231+
});
232+
233+
describe('externalName', () => {
234+
it('returns errors unchanged when serviceType is not ExternalName', () => {
235+
const errors: string[] = [];
236+
const result = externalName({ type: 'ClusterIP', externalName: '' }, mockGetters, errors, {});
237+
238+
expect(result).toStrictEqual([]);
239+
});
240+
241+
it('returns errors unchanged when spec.type is undefined', () => {
242+
const errors: string[] = [];
243+
const result = externalName({ type: undefined }, mockGetters, errors, {});
244+
245+
expect(result).toStrictEqual([]);
246+
});
247+
248+
it('adds error when externalName is missing for ExternalName service', () => {
249+
const errors: string[] = [];
250+
const result = externalName({ type: 'ExternalName', externalName: '' }, mockGetters, errors, {});
251+
252+
expect(result).toContain('validation.service.externalName.none');
253+
});
254+
255+
it('adds error when externalName is null for ExternalName service', () => {
256+
const errors: string[] = [];
257+
const result = externalName({ type: 'ExternalName', externalName: null }, mockGetters, errors, {});
258+
259+
expect(result).toContain('validation.service.externalName.none');
260+
});
261+
262+
it('returns no errors for valid hostname in ExternalName service', () => {
263+
const errors: string[] = [];
264+
const result = externalName({ type: 'ExternalName', externalName: 'my-service.example.com' }, mockGetters, errors, {});
265+
266+
expect(result).toStrictEqual([]);
267+
});
268+
269+
it('returns errors for invalid hostname in ExternalName service', () => {
270+
const errors: string[] = [];
271+
const result = externalName({ type: 'ExternalName', externalName: '-invalid-.example.com' }, mockGetters, errors, {});
272+
273+
expect(result.length).toBeGreaterThan(0);
274+
});
275+
276+
it('preserves pre-existing errors when adding new hostname errors', () => {
277+
const errors = ['pre-existing-error'];
278+
const result = externalName({ type: 'ExternalName', externalName: '-bad' }, mockGetters, errors, {});
279+
280+
expect(result).toContain('pre-existing-error');
281+
});
282+
});
283+
});

0 commit comments

Comments
 (0)