Skip to content

Commit 06172c0

Browse files
committed
fix: added timeout if search engines are not available
1 parent edbf590 commit 06172c0

File tree

9 files changed

+236
-159
lines changed

9 files changed

+236
-159
lines changed

composer.lock

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

phpmyfaq/admin/assets/src/api/elasticsearch.test.ts

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import { fetchElasticsearchAction, fetchElasticsearchStatistics, fetchElasticsea
33

44
describe('Elasticsearch API', () => {
55
afterEach(() => {
6+
vi.useRealTimers();
67
vi.restoreAllMocks();
78
});
89

910
describe('fetchElasticsearchAction', () => {
1011
it('should fetch Elasticsearch action and return JSON response if successful', async () => {
1112
const mockResponse = { success: true, message: 'Action executed' };
12-
global.fetch = vi.fn(() =>
13+
globalThis.fetch = vi.fn(() =>
1314
Promise.resolve({
1415
ok: true,
1516
json: () => Promise.resolve(mockResponse),
@@ -20,7 +21,7 @@ describe('Elasticsearch API', () => {
2021
const result = await fetchElasticsearchAction(action);
2122

2223
expect(result).toEqual(mockResponse);
23-
expect(global.fetch).toHaveBeenCalledWith('./api/elasticsearch/some-action', {
24+
expect(globalThis.fetch).toHaveBeenCalledWith('./api/elasticsearch/some-action', {
2425
method: 'GET',
2526
cache: 'no-cache',
2627
headers: {
@@ -33,7 +34,7 @@ describe('Elasticsearch API', () => {
3334

3435
it('should throw an error if fetch fails', async () => {
3536
const mockError = new Error('Fetch failed');
36-
global.fetch = vi.fn(() => Promise.reject(mockError));
37+
globalThis.fetch = vi.fn(() => Promise.reject(mockError));
3738

3839
const action = 'some-action';
3940

@@ -60,7 +61,7 @@ describe('Elasticsearch API', () => {
6061
},
6162
},
6263
};
63-
global.fetch = vi.fn(() =>
64+
globalThis.fetch = vi.fn(() =>
6465
Promise.resolve({
6566
ok: true,
6667
json: () => Promise.resolve(mockResponse),
@@ -70,7 +71,7 @@ describe('Elasticsearch API', () => {
7071
const result = await fetchElasticsearchStatistics();
7172

7273
expect(result).toEqual(mockResponse);
73-
expect(global.fetch).toHaveBeenCalledWith('./api/elasticsearch/statistics', {
74+
expect(globalThis.fetch).toHaveBeenCalledWith('./api/elasticsearch/statistics', {
7475
method: 'GET',
7576
cache: 'no-cache',
7677
headers: {
@@ -83,7 +84,7 @@ describe('Elasticsearch API', () => {
8384

8485
it('should throw an error if fetch fails', async () => {
8586
const mockError = new Error('Fetch failed');
86-
global.fetch = vi.fn(() => Promise.reject(mockError));
87+
globalThis.fetch = vi.fn(() => Promise.reject(mockError));
8788

8889
await expect(fetchElasticsearchStatistics()).rejects.toThrow(mockError);
8990
});
@@ -92,7 +93,7 @@ describe('Elasticsearch API', () => {
9293
describe('fetchElasticsearchHealthcheck', () => {
9394
it('should fetch Elasticsearch healthcheck and return JSON response when available', async () => {
9495
const mockResponse = { available: true, status: 'healthy' };
95-
global.fetch = vi.fn(() =>
96+
globalThis.fetch = vi.fn(() =>
9697
Promise.resolve({
9798
ok: true,
9899
json: () => Promise.resolve(mockResponse),
@@ -102,20 +103,24 @@ describe('Elasticsearch API', () => {
102103
const result = await fetchElasticsearchHealthcheck();
103104

104105
expect(result).toEqual(mockResponse);
105-
expect(global.fetch).toHaveBeenCalledWith('./api/elasticsearch/healthcheck', {
106-
method: 'GET',
107-
cache: 'no-cache',
108-
headers: {
109-
'Content-Type': 'application/json',
110-
},
111-
redirect: 'follow',
112-
referrerPolicy: 'no-referrer',
113-
});
106+
expect(globalThis.fetch).toHaveBeenCalledWith(
107+
'./api/elasticsearch/healthcheck',
108+
expect.objectContaining({
109+
method: 'GET',
110+
cache: 'no-cache',
111+
headers: {
112+
'Content-Type': 'application/json',
113+
},
114+
redirect: 'follow',
115+
referrerPolicy: 'no-referrer',
116+
signal: expect.any(AbortSignal),
117+
})
118+
);
114119
});
115120

116121
it('should throw an error when Elasticsearch returns 503 Service Unavailable', async () => {
117122
const errorResponse = { available: false, status: 'unavailable' };
118-
global.fetch = vi.fn(() =>
123+
globalThis.fetch = vi.fn(() =>
119124
Promise.resolve({
120125
ok: false,
121126
status: 503,
@@ -128,7 +133,7 @@ describe('Elasticsearch API', () => {
128133

129134
it('should throw an error with custom message when error data is provided', async () => {
130135
const errorResponse = { error: 'Connection refused' };
131-
global.fetch = vi.fn(() =>
136+
globalThis.fetch = vi.fn(() =>
132137
Promise.resolve({
133138
ok: false,
134139
status: 503,
@@ -141,7 +146,7 @@ describe('Elasticsearch API', () => {
141146

142147
it('should throw an error if fetch fails', async () => {
143148
const mockError = new Error('Network error');
144-
global.fetch = vi.fn(() => Promise.reject(mockError));
149+
globalThis.fetch = vi.fn(() => Promise.reject(mockError));
145150

146151
await expect(fetchElasticsearchHealthcheck()).rejects.toThrow(mockError);
147152
});

phpmyfaq/admin/assets/src/api/elasticsearch.ts

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,35 @@ export const fetchElasticsearchStatistics = async (): Promise<ElasticsearchRespo
4343
return await response.json();
4444
};
4545

46-
export const fetchElasticsearchHealthcheck = async (): Promise<Response> => {
47-
const response = await fetch('./api/elasticsearch/healthcheck', {
48-
method: 'GET',
49-
cache: 'no-cache',
50-
headers: {
51-
'Content-Type': 'application/json',
52-
},
53-
redirect: 'follow',
54-
referrerPolicy: 'no-referrer',
55-
});
46+
export const fetchElasticsearchHealthcheck = async (timeoutMs: number = 5000): Promise<Response> => {
47+
const controller = new AbortController();
48+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
5649

57-
if (!response.ok) {
58-
const errorData = await response.json();
59-
throw new Error(errorData.error || 'Elasticsearch is unavailable');
60-
}
50+
try {
51+
const response = await fetch('./api/elasticsearch/healthcheck', {
52+
method: 'GET',
53+
cache: 'no-cache',
54+
headers: {
55+
'Content-Type': 'application/json',
56+
},
57+
redirect: 'follow',
58+
referrerPolicy: 'no-referrer',
59+
signal: controller.signal,
60+
});
6161

62-
return await response.json();
62+
clearTimeout(timeoutId);
63+
64+
if (!response.ok) {
65+
const errorData = await response.json();
66+
throw new Error(errorData.error || 'Elasticsearch is unavailable');
67+
}
68+
69+
return await response.json();
70+
} catch (error) {
71+
clearTimeout(timeoutId);
72+
if (error instanceof Error && error.name === 'AbortError') {
73+
throw new Error('Elasticsearch health check timed out. Service may be down.');
74+
}
75+
throw error;
76+
}
6377
};

phpmyfaq/admin/assets/src/api/opensearch.test.ts

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import { fetchOpenSearchAction, fetchOpenSearchStatistics, fetchOpenSearchHealth
33

44
describe('OpenSearch API', () => {
55
afterEach(() => {
6+
vi.useRealTimers();
67
vi.restoreAllMocks();
78
});
89

910
describe('fetchOpenSearchAction', () => {
1011
it('should fetch OpenSearch action and return JSON response if successful', async () => {
1112
const mockResponse = { success: true, message: 'Action executed' };
12-
global.fetch = vi.fn(() =>
13+
globalThis.fetch = vi.fn(() =>
1314
Promise.resolve({
1415
ok: true,
1516
json: () => Promise.resolve(mockResponse),
@@ -20,7 +21,7 @@ describe('OpenSearch API', () => {
2021
const result = await fetchOpenSearchAction(action);
2122

2223
expect(result).toEqual(mockResponse);
23-
expect(global.fetch).toHaveBeenCalledWith('./api/opensearch/index', {
24+
expect(globalThis.fetch).toHaveBeenCalledWith('./api/opensearch/index', {
2425
method: 'GET',
2526
cache: 'no-cache',
2627
headers: {
@@ -33,7 +34,7 @@ describe('OpenSearch API', () => {
3334

3435
it('should throw an error if fetch fails', async () => {
3536
const mockError = new Error('Fetch failed');
36-
global.fetch = vi.fn(() => Promise.reject(mockError));
37+
globalThis.fetch = vi.fn(() => Promise.reject(mockError));
3738

3839
await expect(fetchOpenSearchAction('index')).rejects.toThrow(mockError);
3940
});
@@ -48,7 +49,7 @@ describe('OpenSearch API', () => {
4849
size: '10GB',
4950
};
5051

51-
global.fetch = vi.fn(() =>
52+
globalThis.fetch = vi.fn(() =>
5253
Promise.resolve({
5354
ok: true,
5455
json: () => Promise.resolve(mockResponse),
@@ -58,7 +59,7 @@ describe('OpenSearch API', () => {
5859
const result = await fetchOpenSearchStatistics();
5960

6061
expect(result).toEqual(mockResponse);
61-
expect(global.fetch).toHaveBeenCalledWith('./api/opensearch/statistics', {
62+
expect(globalThis.fetch).toHaveBeenCalledWith('./api/opensearch/statistics', {
6263
method: 'GET',
6364
cache: 'no-cache',
6465
headers: {
@@ -71,7 +72,7 @@ describe('OpenSearch API', () => {
7172

7273
it('should throw an error if fetch fails', async () => {
7374
const mockError = new Error('Fetch failed');
74-
global.fetch = vi.fn(() => Promise.reject(mockError));
75+
globalThis.fetch = vi.fn(() => Promise.reject(mockError));
7576

7677
await expect(fetchOpenSearchStatistics()).rejects.toThrow(mockError);
7778
});
@@ -80,7 +81,7 @@ describe('OpenSearch API', () => {
8081
describe('fetchOpenSearchHealthcheck', () => {
8182
it('should fetch OpenSearch healthcheck and return JSON response when available', async () => {
8283
const mockResponse = { available: true, status: 'healthy' };
83-
global.fetch = vi.fn(() =>
84+
globalThis.fetch = vi.fn(() =>
8485
Promise.resolve({
8586
ok: true,
8687
json: () => Promise.resolve(mockResponse),
@@ -90,20 +91,24 @@ describe('OpenSearch API', () => {
9091
const result = await fetchOpenSearchHealthcheck();
9192

9293
expect(result).toEqual(mockResponse);
93-
expect(global.fetch).toHaveBeenCalledWith('./api/opensearch/healthcheck', {
94-
method: 'GET',
95-
cache: 'no-cache',
96-
headers: {
97-
'Content-Type': 'application/json',
98-
},
99-
redirect: 'follow',
100-
referrerPolicy: 'no-referrer',
101-
});
94+
expect(globalThis.fetch).toHaveBeenCalledWith(
95+
'./api/opensearch/healthcheck',
96+
expect.objectContaining({
97+
method: 'GET',
98+
cache: 'no-cache',
99+
headers: {
100+
'Content-Type': 'application/json',
101+
},
102+
redirect: 'follow',
103+
referrerPolicy: 'no-referrer',
104+
signal: expect.any(AbortSignal),
105+
})
106+
);
102107
});
103108

104109
it('should throw an error when OpenSearch returns 503 Service Unavailable', async () => {
105110
const errorResponse = { available: false, status: 'unavailable' };
106-
global.fetch = vi.fn(() =>
111+
globalThis.fetch = vi.fn(() =>
107112
Promise.resolve({
108113
ok: false,
109114
status: 503,
@@ -116,7 +121,7 @@ describe('OpenSearch API', () => {
116121

117122
it('should throw an error with custom message when error data is provided', async () => {
118123
const errorResponse = { error: 'Connection refused' };
119-
global.fetch = vi.fn(() =>
124+
globalThis.fetch = vi.fn(() =>
120125
Promise.resolve({
121126
ok: false,
122127
status: 503,
@@ -129,7 +134,7 @@ describe('OpenSearch API', () => {
129134

130135
it('should throw an error if fetch fails', async () => {
131136
const mockError = new Error('Network error');
132-
global.fetch = vi.fn(() => Promise.reject(mockError));
137+
globalThis.fetch = vi.fn(() => Promise.reject(mockError));
133138

134139
await expect(fetchOpenSearchHealthcheck()).rejects.toThrow(mockError);
135140
});

0 commit comments

Comments
 (0)