Skip to content

Commit 1982e6d

Browse files
authored
Add backend API end-to-end tests (#109)
1 parent 60663c5 commit 1982e6d

9 files changed

Lines changed: 1038 additions & 0 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { describe, test, expect, afterEach } from 'bun:test';
2+
import app from '../hono-app';
3+
import { aiConfiguratorService } from '../services/aiconfigurator';
4+
import { mockServiceMethod } from '../test/helpers';
5+
import {
6+
aiConfiguratorStatusAvailable,
7+
aiConfiguratorStatusUnavailable,
8+
aiConfiguratorSuccessResult,
9+
} from '../test/fixtures';
10+
11+
describe('AI Configurator Routes', () => {
12+
const restores: Array<() => void> = [];
13+
14+
afterEach(() => {
15+
restores.forEach((r) => r());
16+
restores.length = 0;
17+
});
18+
19+
describe('GET /api/aiconfigurator/status', () => {
20+
test('returns available status', async () => {
21+
restores.push(
22+
mockServiceMethod(aiConfiguratorService, 'checkStatus', async () => aiConfiguratorStatusAvailable),
23+
);
24+
25+
const res = await app.request('/api/aiconfigurator/status');
26+
expect(res.status).toBe(200);
27+
28+
const data = await res.json();
29+
expect(data.available).toBe(true);
30+
expect(data.version).toBeDefined();
31+
});
32+
33+
test('returns unavailable status', async () => {
34+
restores.push(
35+
mockServiceMethod(aiConfiguratorService, 'checkStatus', async () => aiConfiguratorStatusUnavailable),
36+
);
37+
38+
const res = await app.request('/api/aiconfigurator/status');
39+
expect(res.status).toBe(200);
40+
41+
const data = await res.json();
42+
expect(data.available).toBe(false);
43+
expect(data.error).toBeDefined();
44+
});
45+
});
46+
47+
describe('POST /api/aiconfigurator/analyze', () => {
48+
test('returns 400 for empty body', async () => {
49+
const res = await app.request('/api/aiconfigurator/analyze', {
50+
method: 'POST',
51+
headers: { 'Content-Type': 'application/json' },
52+
body: JSON.stringify({}),
53+
});
54+
expect(res.status).toBe(400);
55+
56+
const data = await res.json();
57+
expect(data.error).toBeDefined();
58+
expect(data.error.message).toBe('Invalid request');
59+
});
60+
61+
test('returns 400 when gpuType is missing', async () => {
62+
const res = await app.request('/api/aiconfigurator/analyze', {
63+
method: 'POST',
64+
headers: { 'Content-Type': 'application/json' },
65+
body: JSON.stringify({ modelId: 'meta-llama/Llama-3.1-8B-Instruct', gpuCount: 1 }),
66+
});
67+
expect(res.status).toBe(400);
68+
69+
const data = await res.json();
70+
expect(data.error).toBeDefined();
71+
});
72+
73+
test('returns 400 when gpuType is empty string', async () => {
74+
const res = await app.request('/api/aiconfigurator/analyze', {
75+
method: 'POST',
76+
headers: { 'Content-Type': 'application/json' },
77+
body: JSON.stringify({
78+
modelId: 'meta-llama/Llama-3.1-8B-Instruct',
79+
gpuType: '',
80+
gpuCount: 1,
81+
}),
82+
});
83+
expect(res.status).toBe(400);
84+
});
85+
86+
test('returns 400 when gpuCount is negative', async () => {
87+
const res = await app.request('/api/aiconfigurator/analyze', {
88+
method: 'POST',
89+
headers: { 'Content-Type': 'application/json' },
90+
body: JSON.stringify({
91+
modelId: 'meta-llama/Llama-3.1-8B-Instruct',
92+
gpuType: 'H100-80GB',
93+
gpuCount: -1,
94+
}),
95+
});
96+
expect(res.status).toBe(400);
97+
});
98+
99+
test('returns 400 when gpuCount is 0', async () => {
100+
const res = await app.request('/api/aiconfigurator/analyze', {
101+
method: 'POST',
102+
headers: { 'Content-Type': 'application/json' },
103+
body: JSON.stringify({
104+
modelId: 'meta-llama/Llama-3.1-8B-Instruct',
105+
gpuType: 'H100-80GB',
106+
gpuCount: 0,
107+
}),
108+
});
109+
expect(res.status).toBe(400);
110+
111+
const data = await res.json();
112+
expect(data.error).toBeDefined();
113+
});
114+
115+
test('returns 200 with success result for valid body', async () => {
116+
restores.push(
117+
mockServiceMethod(aiConfiguratorService, 'analyze', async () => aiConfiguratorSuccessResult),
118+
);
119+
120+
const res = await app.request('/api/aiconfigurator/analyze', {
121+
method: 'POST',
122+
headers: { 'Content-Type': 'application/json' },
123+
body: JSON.stringify({
124+
modelId: 'meta-llama/Llama-3.1-8B-Instruct',
125+
gpuType: 'H100-80GB',
126+
gpuCount: 2,
127+
}),
128+
});
129+
expect(res.status).toBe(200);
130+
131+
const data = await res.json();
132+
expect(data.success).toBe(true);
133+
expect(data.config).toBeDefined();
134+
expect(data.mode).toBeDefined();
135+
expect(data.replicas).toBeDefined();
136+
});
137+
});
138+
139+
describe('POST /api/aiconfigurator/normalize-gpu', () => {
140+
test('returns 400 for empty body', async () => {
141+
const res = await app.request('/api/aiconfigurator/normalize-gpu', {
142+
method: 'POST',
143+
headers: { 'Content-Type': 'application/json' },
144+
body: JSON.stringify({}),
145+
});
146+
expect(res.status).toBe(400);
147+
148+
const data = await res.json();
149+
expect(data.error).toBeDefined();
150+
expect(data.error.message).toContain('gpuProduct');
151+
});
152+
153+
test('returns 200 with normalized GPU type', async () => {
154+
const res = await app.request('/api/aiconfigurator/normalize-gpu', {
155+
method: 'POST',
156+
headers: { 'Content-Type': 'application/json' },
157+
body: JSON.stringify({ gpuProduct: 'NVIDIA-A100-SXM4-80GB' }),
158+
});
159+
expect(res.status).toBe(200);
160+
161+
const data = await res.json();
162+
expect(data.gpuProduct).toBe('NVIDIA-A100-SXM4-80GB');
163+
expect(data.normalized).toBeDefined();
164+
});
165+
});
166+
});
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { describe, test, expect, afterEach } from 'bun:test';
2+
import app from '../hono-app';
3+
import { autoscalerService } from '../services/autoscaler';
4+
import { mockServiceMethod } from '../test/helpers';
5+
import {
6+
autoscalerDetectionAKS,
7+
autoscalerDetectionCA,
8+
autoscalerDetectionNone,
9+
autoscalerStatus,
10+
} from '../test/fixtures';
11+
12+
describe('Autoscaler Routes', () => {
13+
let restore: (() => void) | undefined;
14+
15+
afterEach(() => {
16+
restore?.();
17+
restore = undefined;
18+
});
19+
20+
describe('GET /api/autoscaler/detection', () => {
21+
test('returns AKS managed autoscaler detection', async () => {
22+
restore = mockServiceMethod(autoscalerService, 'detectAutoscaler', (() =>
23+
Promise.resolve(autoscalerDetectionAKS)) as typeof autoscalerService.detectAutoscaler);
24+
25+
const res = await app.request('/api/autoscaler/detection');
26+
expect(res.status).toBe(200);
27+
const data = await res.json();
28+
expect(data.type).toBe('aks-managed');
29+
expect(data.detected).toBe(true);
30+
expect(data.healthy).toBe(true);
31+
expect(data.nodeGroupCount).toBe(2);
32+
});
33+
34+
test('returns cluster-autoscaler detection', async () => {
35+
restore = mockServiceMethod(autoscalerService, 'detectAutoscaler', (() =>
36+
Promise.resolve(autoscalerDetectionCA)) as typeof autoscalerService.detectAutoscaler);
37+
38+
const res = await app.request('/api/autoscaler/detection');
39+
expect(res.status).toBe(200);
40+
const data = await res.json();
41+
expect(data.type).toBe('cluster-autoscaler');
42+
expect(data.detected).toBe(true);
43+
expect(data.healthy).toBe(true);
44+
expect(data.nodeGroupCount).toBe(3);
45+
});
46+
47+
test('returns no autoscaler detection', async () => {
48+
restore = mockServiceMethod(autoscalerService, 'detectAutoscaler', (() =>
49+
Promise.resolve(autoscalerDetectionNone)) as typeof autoscalerService.detectAutoscaler);
50+
51+
const res = await app.request('/api/autoscaler/detection');
52+
expect(res.status).toBe(200);
53+
const data = await res.json();
54+
expect(data.type).toBe('none');
55+
expect(data.detected).toBe(false);
56+
expect(data.healthy).toBe(false);
57+
});
58+
59+
test('returns 500 on service error', async () => {
60+
restore = mockServiceMethod(autoscalerService, 'detectAutoscaler', (() =>
61+
Promise.reject(new Error('detection failed'))) as typeof autoscalerService.detectAutoscaler);
62+
63+
const res = await app.request('/api/autoscaler/detection');
64+
expect(res.status).toBe(500);
65+
const data = await res.json();
66+
expect(data.error).toBeDefined();
67+
expect(data.error.message).toBe('detection failed');
68+
expect(data.error.statusCode).toBe(500);
69+
});
70+
});
71+
72+
describe('GET /api/autoscaler/status', () => {
73+
test('returns autoscaler status', async () => {
74+
restore = mockServiceMethod(autoscalerService, 'getAutoscalerStatus', (() =>
75+
Promise.resolve(autoscalerStatus)) as typeof autoscalerService.getAutoscalerStatus);
76+
77+
const res = await app.request('/api/autoscaler/status');
78+
expect(res.status).toBe(200);
79+
const data = await res.json();
80+
expect(data.health).toBe('Healthy');
81+
expect(data.nodeGroups).toBeDefined();
82+
expect(data.nodeGroups.length).toBe(2);
83+
});
84+
85+
test('returns 404 when status is null', async () => {
86+
restore = mockServiceMethod(autoscalerService, 'getAutoscalerStatus', (() =>
87+
Promise.resolve(null)) as typeof autoscalerService.getAutoscalerStatus);
88+
89+
const res = await app.request('/api/autoscaler/status');
90+
expect(res.status).toBe(404);
91+
const data = await res.json();
92+
expect(data.error).toBeDefined();
93+
expect(data.error.message).toBe('Autoscaler status not available');
94+
expect(data.error.statusCode).toBe(404);
95+
});
96+
97+
test('returns 500 on service error', async () => {
98+
restore = mockServiceMethod(autoscalerService, 'getAutoscalerStatus', (() =>
99+
Promise.reject(new Error('status failed'))) as typeof autoscalerService.getAutoscalerStatus);
100+
101+
const res = await app.request('/api/autoscaler/status');
102+
expect(res.status).toBe(500);
103+
const data = await res.json();
104+
expect(data.error).toBeDefined();
105+
expect(data.error.message).toBe('status failed');
106+
expect(data.error.statusCode).toBe(500);
107+
});
108+
});
109+
});

0 commit comments

Comments
 (0)