Skip to content

Commit 16b17e7

Browse files
committed
fix(k8s-utils): add unit test for useK8sQueryWatch
1 parent a8e86db commit 16b17e7

File tree

1 file changed

+178
-0
lines changed

1 file changed

+178
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { renderHook, act } from '@testing-library/react-hooks';
2+
import { K8sModelCommon } from '../../../types/k8s';
3+
import { watchListResource, watchObjectResource } from '../../watch-utils';
4+
import { useK8sQueryWatch } from '../useK8sQueryWatch';
5+
6+
jest.mock('../../watch-utils', () => ({
7+
watchListResource: jest.fn(),
8+
watchObjectResource: jest.fn(),
9+
}));
10+
11+
const WEBSOCKET_RETRY_COUNT = 3;
12+
const WEBSOCKET_RETRY_DELAY = 2000;
13+
14+
describe('useK8sQueryWatch', () => {
15+
beforeEach(() => {
16+
// Clear all mocks before each test
17+
jest.clearAllMocks();
18+
// Clear the WS Map
19+
(global as unknown as { WS: unknown }).WS = new Map();
20+
});
21+
22+
afterEach(() => {
23+
jest.useRealTimers();
24+
});
25+
26+
const mockWebSocket = {
27+
destroy: jest.fn(),
28+
onClose: jest.fn(),
29+
onError: jest.fn(),
30+
};
31+
32+
const mockResourceInit = {
33+
model: { kind: 'Test', apiGroup: 'test.group', apiVersion: 'v1' } as K8sModelCommon,
34+
queryOptions: {},
35+
};
36+
37+
const mockOptions = { wsPrefix: '/test' };
38+
const mockHashedKey = 'test-key';
39+
40+
it('should initialize websocket for list resource', () => {
41+
(watchListResource as jest.Mock).mockReturnValue(mockWebSocket);
42+
43+
renderHook(() => useK8sQueryWatch(mockResourceInit, true, mockHashedKey, mockOptions));
44+
45+
expect(watchListResource).toHaveBeenCalledWith(mockResourceInit, mockOptions);
46+
expect(watchObjectResource).not.toHaveBeenCalled();
47+
});
48+
49+
it('should initialize websocket for single resource', () => {
50+
(watchObjectResource as jest.Mock).mockReturnValue(mockWebSocket);
51+
52+
renderHook(() => useK8sQueryWatch(mockResourceInit, false, mockHashedKey, mockOptions));
53+
54+
expect(watchObjectResource).toHaveBeenCalledWith(mockResourceInit, mockOptions);
55+
expect(watchListResource).not.toHaveBeenCalled();
56+
});
57+
58+
it('should not initialize websocket when resourceInit is null', () => {
59+
renderHook(() => useK8sQueryWatch(null, true, mockHashedKey, mockOptions));
60+
61+
expect(watchListResource).not.toHaveBeenCalled();
62+
expect(watchObjectResource).not.toHaveBeenCalled();
63+
});
64+
65+
it('should clean up websocket on unmount', () => {
66+
(watchListResource as jest.Mock).mockReturnValue(mockWebSocket);
67+
68+
const { unmount } = renderHook(() =>
69+
useK8sQueryWatch(mockResourceInit, true, mockHashedKey, mockOptions),
70+
);
71+
72+
unmount();
73+
74+
expect(mockWebSocket.destroy).toHaveBeenCalled();
75+
});
76+
77+
it('should handle websocket close with code 1006 and attempt reconnection', () => {
78+
jest.useFakeTimers();
79+
let closeHandler: (event: { code: number }) => void;
80+
81+
mockWebSocket.onClose.mockImplementation((handler) => {
82+
closeHandler = handler;
83+
});
84+
85+
(watchListResource as jest.Mock).mockReturnValue(mockWebSocket);
86+
87+
renderHook(() => useK8sQueryWatch(mockResourceInit, true, mockHashedKey, mockOptions));
88+
89+
// Simulate websocket close with code 1006
90+
act(() => {
91+
closeHandler({ code: 1006 });
92+
});
93+
94+
// First retry
95+
act(() => {
96+
jest.advanceTimersByTime(WEBSOCKET_RETRY_DELAY);
97+
});
98+
99+
expect(watchListResource).toHaveBeenCalledTimes(2);
100+
});
101+
102+
it('should set error state after max retry attempts', () => {
103+
jest.useFakeTimers();
104+
let closeHandler: (event: { code: number }) => void;
105+
106+
mockWebSocket.onClose.mockImplementation((handler) => {
107+
closeHandler = handler;
108+
});
109+
110+
(watchListResource as jest.Mock).mockReturnValue(mockWebSocket);
111+
112+
const { result } = renderHook(() =>
113+
useK8sQueryWatch(mockResourceInit, true, mockHashedKey, mockOptions),
114+
);
115+
116+
// Simulate multiple websocket closes
117+
for (let i = 0; i <= WEBSOCKET_RETRY_COUNT; i++) {
118+
act(() => {
119+
closeHandler({ code: 1006 });
120+
// Advance time by retry delay with exponential backoff
121+
jest.advanceTimersByTime(WEBSOCKET_RETRY_DELAY * Math.pow(2, i));
122+
});
123+
}
124+
125+
expect(result.current).toEqual({
126+
code: 1006,
127+
message: 'WebSocket connection failed after multiple attempts',
128+
});
129+
});
130+
131+
it('should handle websocket errors', () => {
132+
let errorHandler: (error: { code: number; message: string }) => void;
133+
134+
mockWebSocket.onError.mockImplementation((handler) => {
135+
errorHandler = handler;
136+
});
137+
138+
(watchListResource as jest.Mock).mockReturnValue(mockWebSocket);
139+
140+
const { result } = renderHook(() =>
141+
useK8sQueryWatch(mockResourceInit, true, mockHashedKey, mockOptions),
142+
);
143+
144+
const mockError = { code: 1011, message: 'Test error' };
145+
146+
act(() => {
147+
errorHandler(mockError);
148+
});
149+
150+
expect(result.current).toEqual(mockError);
151+
});
152+
153+
it('should clear error state and retry count on new resourceInit', () => {
154+
(watchListResource as jest.Mock).mockReturnValue(mockWebSocket);
155+
156+
const { rerender, result } = renderHook(
157+
({ resourceInit }) => useK8sQueryWatch(resourceInit, true, mockHashedKey, mockOptions),
158+
{ initialProps: { resourceInit: mockResourceInit } },
159+
);
160+
161+
// Set error state
162+
act(() => {
163+
mockWebSocket.onError.mock.calls[0][0]({ code: 1011, message: 'Test error' });
164+
});
165+
166+
expect(result.current).toBeTruthy();
167+
168+
// Rerender with new resourceInit
169+
rerender({
170+
resourceInit: {
171+
...mockResourceInit,
172+
model: { kind: 'TestNew', apiGroup: 'test.group', apiVersion: 'v1' } as K8sModelCommon,
173+
},
174+
});
175+
176+
expect(result.current).toBeNull();
177+
});
178+
});

0 commit comments

Comments
 (0)