Skip to content

Commit 606cc74

Browse files
authored
RI-6382: Test multiple source connections (#4389)
* enhance client to make multiple requests as only one source connection can be tested in single request * display multiple sources * have mixed state for successful and failed connections * extract to decorator * add direction transform options * add test for transform options
1 parent 31c3bfe commit 606cc74

File tree

12 files changed

+506
-121
lines changed

12 files changed

+506
-121
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import * as classTransformer from 'class-transformer';
2+
import { TransformToMap } from './transform-to-map.decorator';
3+
4+
const { Expose, classToPlain, plainToClass } = classTransformer;
5+
6+
class DummyClass {
7+
@Expose()
8+
value: string;
9+
}
10+
11+
class TestDto {
12+
@TransformToMap(DummyClass)
13+
@Expose()
14+
data: Record<string, DummyClass>;
15+
}
16+
17+
describe('TransformToMap decorator', () => {
18+
it('should transform each map value into an instance of DummyClass', () => {
19+
const input = {
20+
data: {
21+
key1: { value: 'test1' },
22+
key2: { value: 'test2' },
23+
},
24+
};
25+
26+
const instance = plainToClass(TestDto, input);
27+
28+
expect(instance.data).toBeDefined();
29+
expect(instance.data.key1).toBeInstanceOf(DummyClass);
30+
expect(instance.data.key2).toBeInstanceOf(DummyClass);
31+
expect(instance.data.key1.value).toEqual('test1');
32+
expect(instance.data.key2.value).toEqual('test2');
33+
});
34+
35+
it('should handle empty objects gracefully', () => {
36+
const input = { data: {} };
37+
const instance = plainToClass(TestDto, input);
38+
39+
expect(instance.data).toEqual({});
40+
});
41+
42+
it('should handle undefined values gracefully', () => {
43+
const input = { data: undefined };
44+
const instance = plainToClass(TestDto, input);
45+
46+
expect(instance.data).toEqual(undefined);
47+
});
48+
49+
it('should convert a class instance to a plain object', () => {
50+
const dummy1 = new DummyClass();
51+
dummy1.value = 'test1';
52+
const dummy2 = new DummyClass();
53+
dummy2.value = 'test2';
54+
55+
const dataMap = {
56+
key1: { value: 'test1' },
57+
key2: { value: 'test2' },
58+
};
59+
60+
const instance = new TestDto();
61+
instance.data = dataMap;
62+
63+
const plain = classToPlain(instance);
64+
65+
expect(plain).toHaveProperty('data');
66+
expect(plain.data).toEqual({
67+
key1: { value: 'test1' },
68+
key2: { value: 'test2' },
69+
});
70+
});
71+
72+
it('should handle an empty Map gracefully on reverse conversion', () => {
73+
const instance = new TestDto();
74+
instance.data = {};
75+
76+
const plain = classToPlain(instance);
77+
78+
expect(plain.data).toEqual({});
79+
});
80+
81+
it('should handle undefined values gracefully on reverse conversion', () => {
82+
const instance = new TestDto();
83+
instance.data = undefined;
84+
85+
const plain = classToPlain(instance);
86+
87+
expect(plain.data).toEqual(undefined);
88+
});
89+
90+
it('should trigger plainToClass without triggering classToPlain', () => {
91+
const spyClassToPlain = jest.spyOn(classTransformer, 'classToPlain');
92+
93+
const input = {
94+
data: {
95+
key1: { value: 'test1' },
96+
key2: { value: 'test2' },
97+
},
98+
};
99+
100+
const instance = plainToClass(TestDto, input, {
101+
excludeExtraneousValues: true,
102+
});
103+
104+
expect(instance.data).toBeDefined();
105+
expect(instance.data.key1).toBeInstanceOf(DummyClass);
106+
expect(instance.data.key1.value).toEqual('test1');
107+
108+
expect(spyClassToPlain).not.toHaveBeenCalled();
109+
110+
spyClassToPlain.mockRestore();
111+
});
112+
113+
it('should trigger classToPlain without triggering plainToClass', () => {
114+
const spyPlainToClass = jest.spyOn(classTransformer, 'plainToClass');
115+
116+
const dummy1 = new DummyClass();
117+
dummy1.value = 'test1';
118+
const dummy2 = new DummyClass();
119+
dummy2.value = 'test2';
120+
121+
const instance = new TestDto();
122+
instance.data = {
123+
key1: dummy1,
124+
key2: dummy2,
125+
};
126+
127+
const plain = classToPlain(instance);
128+
129+
expect(plain).toHaveProperty('data');
130+
expect(plain.data).toEqual({
131+
key1: { value: 'test1' },
132+
key2: { value: 'test2' },
133+
});
134+
135+
expect(spyPlainToClass).not.toHaveBeenCalled();
136+
137+
spyPlainToClass.mockRestore();
138+
});
139+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { applyDecorators } from '@nestjs/common';
2+
import { Transform, classToPlain, plainToClass } from 'class-transformer';
3+
import { ClassType } from 'class-transformer/ClassTransformer';
4+
5+
export function TransformToMap<T>(targetClass: ClassType<T>) {
6+
return applyDecorators(
7+
Transform(
8+
(value) => {
9+
if (!value) {
10+
return value;
11+
}
12+
13+
return Object.fromEntries(
14+
Object.entries(value).map(([key, val]) => [
15+
key,
16+
plainToClass(targetClass, val),
17+
]),
18+
);
19+
},
20+
{ toClassOnly: true },
21+
),
22+
23+
Transform(
24+
(value) => {
25+
if (!value) {
26+
return value;
27+
}
28+
29+
return Object.fromEntries(
30+
Object.entries(value).map(([key, instance]) => [
31+
key,
32+
classToPlain(instance),
33+
]),
34+
);
35+
},
36+
{ toPlainOnly: true },
37+
),
38+
);
39+
}

redisinsight/api/src/modules/rdi/client/api.rdi.client.spec.ts

Lines changed: 135 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,32 @@ const mockedAxios = axios as jest.Mocked<typeof axios>;
1818
jest.mock('axios');
1919
mockedAxios.create = jest.fn(() => mockedAxios);
2020

21+
const createMockPostImplementation = (
22+
targetsResponses: (any | Error)[],
23+
sourcesResponses: (any | Error)[],
24+
) => {
25+
let targetsCallCount = 0;
26+
let sourcesCallCount = 0;
27+
28+
return (url: string) => {
29+
if (url === RdiUrl.TestTargetsConnections) {
30+
// eslint-disable-next-line no-plusplus
31+
const response = targetsResponses[targetsCallCount++];
32+
return response instanceof Error
33+
? Promise.reject(response)
34+
: Promise.resolve(response);
35+
}
36+
if (url === RdiUrl.TestSourcesConnections) {
37+
// eslint-disable-next-line no-plusplus
38+
const response = sourcesResponses[sourcesCallCount++];
39+
return response instanceof Error
40+
? Promise.reject(response)
41+
: Promise.resolve(response);
42+
}
43+
return Promise.reject(new Error(`Unexpected URL: ${url}`));
44+
};
45+
};
46+
2147
describe('ApiRdiClient', () => {
2248
let client: ApiRdiClient;
2349

@@ -274,7 +300,7 @@ describe('ApiRdiClient', () => {
274300
});
275301

276302
describe('testConnections', () => {
277-
const config = { sources: {} };
303+
const config = { sources: { source1: {} } };
278304

279305
it('should return a successful response', async () => {
280306
const expectedTargetsResponse = {
@@ -285,24 +311,72 @@ describe('ApiRdiClient', () => {
285311
},
286312
};
287313
const expectedSourcesResponse = {
288-
connected: true,
289-
error: '',
314+
source1: {
315+
connected: true,
316+
error: '',
317+
},
290318
};
291319

292-
mockedAxios.post
293-
.mockResolvedValueOnce({ data: expectedTargetsResponse })
294-
.mockResolvedValueOnce({ data: expectedSourcesResponse });
320+
const targetsResponses = [{ data: expectedTargetsResponse }];
295321

296-
const response = await client.testConnections(config);
322+
const sourcesResponses = [
323+
{ data: expectedSourcesResponse.source1 },
324+
];
297325

298-
expect(mockedAxios.post).toHaveBeenCalledWith(
299-
RdiUrl.TestTargetsConnections,
300-
config,
326+
mockedAxios.post.mockImplementation(
327+
createMockPostImplementation(targetsResponses, sourcesResponses),
301328
);
302-
expect(mockedAxios.post).toHaveBeenCalledWith(
303-
RdiUrl.TestSourcesConnections,
304-
{},
329+
330+
const response = await client.testConnections(config);
331+
332+
expect(mockedAxios.post).toHaveBeenCalledTimes(2);
333+
334+
expect(response).toEqual({
335+
sources: expectedSourcesResponse,
336+
...expectedTargetsResponse,
337+
});
338+
});
339+
340+
it('should return a successful response with multiple sources', async () => {
341+
const expectedTargetsResponse = {
342+
targets: {
343+
target: {
344+
status: 'success',
345+
},
346+
},
347+
};
348+
349+
const expectedSourcesResponse = {
350+
source1: {
351+
connected: true,
352+
error: '',
353+
},
354+
source2: {
355+
connected: false,
356+
error: 'Connection failed',
357+
},
358+
};
359+
360+
const targetsResponses = [{ data: expectedTargetsResponse }];
361+
362+
const sourcesResponses = [
363+
{ data: { connected: true, error: '' } },
364+
{ data: { connected: false, error: 'Connection failed' } },
365+
];
366+
367+
mockedAxios.post.mockImplementation(
368+
createMockPostImplementation(targetsResponses, sourcesResponses),
305369
);
370+
371+
const response = await client.testConnections({
372+
sources: {
373+
source1: {},
374+
source2: {},
375+
},
376+
});
377+
378+
expect(mockedAxios.post).toHaveBeenCalledTimes(3);
379+
306380
expect(response).toEqual({
307381
sources: expectedSourcesResponse,
308382
...expectedTargetsResponse,
@@ -323,22 +397,66 @@ describe('ApiRdiClient', () => {
323397

324398
const loggerErrorSpy = jest.spyOn(client['logger'], 'error').mockImplementation();
325399

326-
mockedAxios.post
327-
.mockResolvedValueOnce({ data: expectedTargetsResponse })
328-
.mockRejectedValueOnce(new Error('Sources request failed'));
400+
const targetsResponses = [{ data: expectedTargetsResponse }];
401+
402+
const sourcesResponses = [
403+
new Error('Sources request failed'),
404+
];
405+
406+
mockedAxios.post.mockImplementation(
407+
createMockPostImplementation(targetsResponses, sourcesResponses),
408+
);
329409

330410
const response = await client.testConnections(config);
331411

332412
expect(response).toEqual({
333413
targets: expectedTargetsResponse.targets,
334-
sources: { connected: false, error: 'Failed to fetch sources' },
414+
sources: {},
335415
});
336416

337417
expect(mockedAxios.post).toHaveBeenCalledTimes(2);
338418

339419
expect(loggerErrorSpy).toHaveBeenCalledWith('Failed to fetch sources', expect.any(Error));
340420
loggerErrorSpy.mockRestore();
341421
});
422+
423+
it('should return targets data even if TestSourcesConnections fails, asd', async () => {
424+
const expectedTargetsResponse = {
425+
targets: { target1: { status: 'success' } },
426+
};
427+
428+
const loggerErrorSpy = jest.spyOn(client['logger'], 'error').mockImplementation();
429+
430+
const targetsResponses = [{ data: expectedTargetsResponse }];
431+
432+
const sourcesResponses = [
433+
{ data: { connected: true, error: '' } },
434+
new Error('Sources request failed'),
435+
];
436+
437+
mockedAxios.post.mockImplementation(
438+
createMockPostImplementation(targetsResponses, sourcesResponses),
439+
);
440+
441+
const response = await client.testConnections({
442+
sources: {
443+
source1: {},
444+
source2: {},
445+
},
446+
});
447+
448+
expect(response).toEqual({
449+
targets: expectedTargetsResponse.targets,
450+
sources: {
451+
source1: { connected: true, error: '' },
452+
},
453+
});
454+
455+
expect(mockedAxios.post).toHaveBeenCalledTimes(3);
456+
457+
expect(loggerErrorSpy).toHaveBeenCalledWith('Failed to fetch sources', expect.any(Error));
458+
loggerErrorSpy.mockRestore();
459+
});
342460
});
343461

344462
describe('getPipelineStatus', () => {

0 commit comments

Comments
 (0)