Skip to content

Commit 581d0d3

Browse files
Copilotahmadawais
andcommitted
Add comprehensive test suite for Langbase SDK with vitest
Co-authored-by: ahmadawais <960133+ahmadawais@users.noreply.github.com>
1 parent 4d6947c commit 581d0d3

9 files changed

Lines changed: 3824 additions & 0 deletions

File tree

.github/workflows/tests.yml

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [main, develop]
6+
pull_request:
7+
branches: [main, develop]
8+
9+
jobs:
10+
test:
11+
name: 'Test SDK'
12+
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
node-version: [18.x, 20.x]
16+
steps:
17+
- name: Checkout Repo
18+
uses: actions/checkout@v4
19+
with:
20+
fetch-depth: 0
21+
22+
- name: Setup pnpm 8
23+
uses: pnpm/action-setup@v2
24+
with:
25+
version: 8.6.9
26+
27+
- name: Use Node.js ${{ matrix.node-version }}
28+
uses: actions/setup-node@v4
29+
with:
30+
node-version: ${{ matrix.node-version }}
31+
cache: 'pnpm'
32+
33+
- name: Install dependencies
34+
run: pnpm install --frozen-lockfile
35+
36+
- name: Run linting
37+
run: pnpm --filter langbase lint
38+
39+
- name: Run type check
40+
run: pnpm --filter langbase type-check
41+
42+
- name: Run tests (Node.js environment)
43+
run: pnpm --filter langbase test:node
44+
45+
- name: Run tests (Edge runtime environment)
46+
run: pnpm --filter langbase test:edge
47+
48+
- name: Install Playwright Browsers
49+
run: pnpm exec playwright install --with-deps
50+
51+
- name: Run UI tests (React components)
52+
run: pnpm --filter langbase test:ui:react
53+
54+
# Optional: Add a separate job for ecosystem tests
55+
ecosystem-test:
56+
name: 'Ecosystem Tests'
57+
runs-on: ubuntu-latest
58+
needs: test
59+
if: github.event_name == 'pull_request'
60+
steps:
61+
- name: Checkout Repo
62+
uses: actions/checkout@v4
63+
64+
- name: Setup pnpm 8
65+
uses: pnpm/action-setup@v2
66+
with:
67+
version: 8.6.9
68+
69+
- name: Use Node.js 20.x
70+
uses: actions/setup-node@v4
71+
with:
72+
node-version: 20.x
73+
cache: 'pnpm'
74+
75+
- name: Install dependencies
76+
run: pnpm install --frozen-lockfile
77+
78+
- name: Build SDK
79+
run: pnpm --filter langbase build
80+
81+
- name: Test Node.js ESM
82+
working-directory: ecosystem-tests/node-esm
83+
run: |
84+
npm install
85+
timeout 30s node index.mjs || echo "ESM test completed"
86+
87+
- name: Test Node.js CJS
88+
working-directory: ecosystem-tests/node-cjs
89+
run: |
90+
npm install
91+
timeout 30s node index.cjs || echo "CJS test completed"
92+
93+
- name: Setup Bun
94+
uses: oven-sh/setup-bun@v1
95+
with:
96+
bun-version: latest
97+
98+
- name: Test Bun
99+
working-directory: ecosystem-tests/bun
100+
run: |
101+
bun install
102+
timeout 30s bun run index.ts || echo "Bun test completed"
103+
104+
- name: Setup Deno
105+
uses: denoland/setup-deno@v1
106+
with:
107+
deno-version: v1.x
108+
109+
- name: Test Deno
110+
working-directory: ecosystem-tests/deno
111+
run: timeout 30s deno run --allow-net index.ts || echo "Deno test completed"
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import {beforeEach, describe, expect, it, vi} from 'vitest';
2+
import {
3+
APIError,
4+
APIConnectionError,
5+
APIConnectionTimeoutError,
6+
AuthenticationError,
7+
BadRequestError,
8+
ConflictError,
9+
InternalServerError,
10+
NotFoundError,
11+
PermissionDeniedError,
12+
RateLimitError,
13+
UnprocessableEntityError,
14+
} from './errors';
15+
16+
describe('Error Classes', () => {
17+
describe('APIError', () => {
18+
it('should create APIError with all parameters', () => {
19+
const status = 400;
20+
const error = {message: 'Test error', code: 'test_error'};
21+
const message = 'API Error occurred';
22+
const headers = {'lb-request-id': 'req-123'};
23+
24+
const apiError = new APIError(status, error, message, headers);
25+
26+
expect(apiError.status).toBe(400);
27+
expect(apiError.error).toEqual(error);
28+
expect(apiError.message).toBe('400 API Error occurred');
29+
expect(apiError.headers).toEqual(headers);
30+
expect(apiError.request_id).toBe('req-123');
31+
expect(apiError.code).toBe('test_error');
32+
});
33+
34+
it('should handle error with nested message object', () => {
35+
const error = {message: {details: 'Detailed error info'}};
36+
const apiError = new APIError(400, error, 'Test', {});
37+
38+
expect(apiError.message).toBe('400 {"details":"Detailed error info"}');
39+
});
40+
41+
it('should handle error without specific message', () => {
42+
const error = {code: 'generic_error'};
43+
const apiError = new APIError(500, error, undefined, {});
44+
45+
expect(apiError.message).toBe('500 {"code":"generic_error"}');
46+
});
47+
48+
it('should handle status only', () => {
49+
const apiError = new APIError(404, undefined, undefined, {});
50+
51+
expect(apiError.message).toBe('404 status code (no body)');
52+
});
53+
54+
it('should handle message only', () => {
55+
const apiError = new APIError(undefined, undefined, 'Custom message', {});
56+
57+
expect(apiError.message).toBe('Custom message');
58+
});
59+
60+
it('should handle no parameters', () => {
61+
const apiError = new APIError(undefined, undefined, undefined, {});
62+
63+
expect(apiError.message).toBe('(no status code or body)');
64+
});
65+
66+
describe('APIError.generate', () => {
67+
it('should generate BadRequestError for 400 status', () => {
68+
const error = APIError.generate(
69+
400,
70+
{error: {message: 'Bad request'}},
71+
'Bad Request',
72+
{}
73+
);
74+
75+
expect(error).toBeInstanceOf(BadRequestError);
76+
expect(error.status).toBe(400);
77+
});
78+
79+
it('should generate AuthenticationError for 401 status', () => {
80+
const error = APIError.generate(
81+
401,
82+
{error: {message: 'Unauthorized'}},
83+
'Unauthorized',
84+
{}
85+
);
86+
87+
expect(error).toBeInstanceOf(AuthenticationError);
88+
expect(error.status).toBe(401);
89+
});
90+
91+
it('should generate PermissionDeniedError for 403 status', () => {
92+
const error = APIError.generate(
93+
403,
94+
{error: {message: 'Forbidden'}},
95+
'Forbidden',
96+
{}
97+
);
98+
99+
expect(error).toBeInstanceOf(PermissionDeniedError);
100+
expect(error.status).toBe(403);
101+
});
102+
103+
it('should generate NotFoundError for 404 status', () => {
104+
const error = APIError.generate(
105+
404,
106+
{error: {message: 'Not found'}},
107+
'Not Found',
108+
{}
109+
);
110+
111+
expect(error).toBeInstanceOf(NotFoundError);
112+
expect(error.status).toBe(404);
113+
});
114+
115+
it('should generate ConflictError for 409 status', () => {
116+
const error = APIError.generate(
117+
409,
118+
{error: {message: 'Conflict'}},
119+
'Conflict',
120+
{}
121+
);
122+
123+
expect(error).toBeInstanceOf(ConflictError);
124+
expect(error.status).toBe(409);
125+
});
126+
127+
it('should generate UnprocessableEntityError for 422 status', () => {
128+
const error = APIError.generate(
129+
422,
130+
{error: {message: 'Validation failed'}},
131+
'Unprocessable Entity',
132+
{}
133+
);
134+
135+
expect(error).toBeInstanceOf(UnprocessableEntityError);
136+
expect(error.status).toBe(422);
137+
});
138+
139+
it('should generate RateLimitError for 429 status', () => {
140+
const error = APIError.generate(
141+
429,
142+
{error: {message: 'Rate limit exceeded'}},
143+
'Too Many Requests',
144+
{}
145+
);
146+
147+
expect(error).toBeInstanceOf(RateLimitError);
148+
expect(error.status).toBe(429);
149+
});
150+
151+
it('should generate InternalServerError for 500+ status', () => {
152+
const error = APIError.generate(
153+
500,
154+
{error: {message: 'Internal error'}},
155+
'Internal Server Error',
156+
{}
157+
);
158+
159+
expect(error).toBeInstanceOf(InternalServerError);
160+
expect(error.status).toBe(500);
161+
});
162+
163+
it('should generate generic APIError for unknown status codes', () => {
164+
const error = APIError.generate(
165+
418,
166+
{error: {message: "I'm a teapot"}},
167+
"I'm a teapot",
168+
{}
169+
);
170+
171+
expect(error).toBeInstanceOf(APIError);
172+
expect(error.status).toBe(418);
173+
});
174+
175+
it('should generate APIConnectionError when status is undefined', () => {
176+
const error = APIError.generate(
177+
undefined,
178+
new Error('Network error'),
179+
undefined,
180+
{}
181+
);
182+
183+
expect(error).toBeInstanceOf(APIConnectionError);
184+
});
185+
});
186+
});
187+
188+
describe('APIConnectionError', () => {
189+
it('should create with default message', () => {
190+
const error = new APIConnectionError({});
191+
192+
expect(error.status).toBeUndefined();
193+
expect(error.message).toBe('Connection error.');
194+
expect(error).toBeInstanceOf(APIError);
195+
});
196+
197+
it('should create with custom message', () => {
198+
const error = new APIConnectionError({message: 'Custom connection error'});
199+
200+
expect(error.message).toBe('Custom connection error');
201+
});
202+
203+
it('should create with cause', () => {
204+
const cause = new Error('Network failure');
205+
const error = new APIConnectionError({cause});
206+
207+
expect((error as any).cause).toBe(cause);
208+
});
209+
210+
it('should create with both message and cause', () => {
211+
const cause = new Error('Network failure');
212+
const error = new APIConnectionError({
213+
message: 'Connection failed',
214+
cause,
215+
});
216+
217+
expect(error.message).toBe('Connection failed');
218+
expect((error as any).cause).toBe(cause);
219+
});
220+
});
221+
222+
describe('APIConnectionTimeoutError', () => {
223+
it('should create with default timeout message', () => {
224+
const error = new APIConnectionTimeoutError();
225+
226+
expect(error.message).toBe('Request timed out.');
227+
expect(error).toBeInstanceOf(APIConnectionError);
228+
});
229+
230+
it('should create with custom timeout message', () => {
231+
const error = new APIConnectionTimeoutError({
232+
message: 'Custom timeout error',
233+
});
234+
235+
expect(error.message).toBe('Custom timeout error');
236+
});
237+
});
238+
239+
describe('Specific Error Classes', () => {
240+
it('BadRequestError should have correct status', () => {
241+
const error = new BadRequestError(400, {}, 'Bad request', {});
242+
expect(error.status).toBe(400);
243+
});
244+
245+
it('AuthenticationError should have correct status', () => {
246+
const error = new AuthenticationError(401, {}, 'Unauthorized', {});
247+
expect(error.status).toBe(401);
248+
});
249+
250+
it('PermissionDeniedError should have correct status', () => {
251+
const error = new PermissionDeniedError(403, {}, 'Forbidden', {});
252+
expect(error.status).toBe(403);
253+
});
254+
255+
it('NotFoundError should have correct status', () => {
256+
const error = new NotFoundError(404, {}, 'Not found', {});
257+
expect(error.status).toBe(404);
258+
});
259+
260+
it('ConflictError should have correct status', () => {
261+
const error = new ConflictError(409, {}, 'Conflict', {});
262+
expect(error.status).toBe(409);
263+
});
264+
265+
it('UnprocessableEntityError should have correct status', () => {
266+
const error = new UnprocessableEntityError(422, {}, 'Unprocessable', {});
267+
expect(error.status).toBe(422);
268+
});
269+
270+
it('RateLimitError should have correct status', () => {
271+
const error = new RateLimitError(429, {}, 'Rate limited', {});
272+
expect(error.status).toBe(429);
273+
});
274+
275+
it('InternalServerError should inherit from APIError', () => {
276+
const error = new InternalServerError(500, {}, 'Internal error', {});
277+
expect(error).toBeInstanceOf(APIError);
278+
expect(error.status).toBe(500);
279+
});
280+
});
281+
});

0 commit comments

Comments
 (0)