Skip to content

Commit 51b968b

Browse files
committed
clean: add tests
1 parent 10a8c3b commit 51b968b

10 files changed

Lines changed: 3695 additions & 74 deletions

jest.config.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/** @type {import('jest').Config} */
2+
module.exports = {
3+
preset: 'ts-jest',
4+
testEnvironment: 'node',
5+
roots: ['<rootDir>/src'],
6+
testMatch: ['**/__tests__/**/*.test.ts'],
7+
transform: {
8+
'^.+\\.ts$': 'ts-jest',
9+
},
10+
collectCoverageFrom: [
11+
'src/**/*.ts',
12+
'!src/**/*.d.ts',
13+
],
14+
coverageDirectory: 'coverage',
15+
coverageReporters: ['json-summary', 'text', 'lcov'],
16+
clearMocks: true,
17+
restoreMocks: true,
18+
};

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@
5555
}
5656
},
5757
"devDependencies": {
58+
"@jest/globals": "^30.2.0",
59+
"@types/jest": "^30.0.0",
60+
"jest": "^30.2.0",
61+
"jest-environment-jsdom": "^30.2.0",
62+
"server-only": "^0.0.1",
63+
"ts-jest": "^29.4.6",
5864
"turbo": "^2.6.3",
5965
"typescript": "~5.9.2"
6066
},

pnpm-lock.yaml

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

src/__tests__/core.test.ts

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { describe, it, expect, jest } from '@jest/globals';
2+
import z from 'zod';
3+
import { endpoint, router, CtxRouter, type Endpoint } from '../core';
4+
5+
describe('core', () => {
6+
describe('endpoint', () => {
7+
describe('input method', () => {
8+
it('creates endpoint with input schema and action', () => {
9+
const schema = z.object({ name: z.string() });
10+
const actionFn = jest.fn().mockReturnValue({ success: true });
11+
12+
const result = endpoint.input(schema).action(actionFn);
13+
14+
expect(result.input).toBe(schema);
15+
expect(result.action).toBe(actionFn);
16+
});
17+
18+
it('handles typed input and context', () => {
19+
const schema = z.object({ id: z.number() });
20+
type Context = { userId: string };
21+
22+
const actionFn = jest.fn(() => ({ data: 'test' }));
23+
const result = endpoint.input(schema).action<{ data: string }, Context>(actionFn);
24+
25+
expect(result.input).toBe(schema);
26+
expect(result.action).toBe(actionFn);
27+
});
28+
});
29+
30+
describe('action method', () => {
31+
it('creates endpoint without input schema', () => {
32+
const actionFn = jest.fn().mockReturnValue({ message: 'hello' });
33+
34+
const result = endpoint.action(actionFn);
35+
36+
expect(result.input).toBeUndefined();
37+
expect(result.action).toBe(actionFn);
38+
});
39+
40+
it('handles typed output and context', () => {
41+
type Context = { role: string };
42+
const actionFn = jest.fn(() => ({ status: 'ok' }));
43+
44+
const result = endpoint.action<{ status: string }, Context>(actionFn);
45+
46+
expect(result.input).toBeUndefined();
47+
expect(result.action).toBe(actionFn);
48+
});
49+
});
50+
});
51+
52+
describe('router', () => {
53+
it('returns the router object as-is', () => {
54+
const testRouter = {
55+
getUser: endpoint.input(z.object({ id: z.string() })).action(() => ({ name: 'test' })),
56+
getAllUsers: endpoint.action(() => [{ name: 'user1' }, { name: 'user2' }])
57+
};
58+
59+
const result = router(testRouter);
60+
61+
expect(result).toBe(testRouter);
62+
expect(result.getUser).toBeDefined();
63+
expect(result.getAllUsers).toBeDefined();
64+
});
65+
66+
it('handles empty router', () => {
67+
const emptyRouter = {};
68+
const result = router(emptyRouter);
69+
70+
expect(result).toBe(emptyRouter);
71+
expect(Object.keys(result)).toHaveLength(0);
72+
});
73+
74+
it('preserves router structure with context type', () => {
75+
type Context = { user: { id: string } };
76+
const contextRouter = {
77+
protectedEndpoint: endpoint.input(z.object({ data: z.string() })).action<string, Context>(() => 'protected')
78+
};
79+
80+
const result = router(contextRouter as any);
81+
82+
expect(result).toBe(contextRouter);
83+
expect(result.protectedEndpoint).toBeDefined();
84+
});
85+
});
86+
87+
describe('CtxRouter', () => {
88+
type TestContext = { userId: string; role: string };
89+
90+
it('creates instance with context type', () => {
91+
const ctxRouter = new CtxRouter<TestContext>();
92+
93+
expect(ctxRouter).toBeInstanceOf(CtxRouter);
94+
expect(ctxRouter.endpoint).toBeDefined();
95+
expect(ctxRouter.router).toBeDefined();
96+
});
97+
98+
describe('endpoint.input', () => {
99+
it('creates endpoint with input schema and typed action', () => {
100+
const ctxRouter = new CtxRouter<TestContext>();
101+
const schema = z.object({ name: z.string() });
102+
const actionFn = jest.fn().mockReturnValue({ created: true });
103+
104+
const result = ctxRouter.endpoint.input(schema).action(actionFn);
105+
106+
expect(result.input).toBe(schema);
107+
expect(result.action).toBe(actionFn);
108+
});
109+
110+
it('maintains context type in action function', () => {
111+
const ctxRouter = new CtxRouter<TestContext>();
112+
const schema = z.object({ data: z.string() });
113+
114+
const actionFn = jest.fn((input: z.infer<typeof schema>, context: { userId: string; role: string; request: any }) => {
115+
return { result: `${input.data}-${context.userId}` };
116+
});
117+
118+
const result = ctxRouter.endpoint.input(schema).action<{ result: string }>(actionFn);
119+
120+
expect(result.input).toBe(schema);
121+
expect(result.action).toBe(actionFn);
122+
});
123+
});
124+
125+
describe('endpoint.action', () => {
126+
it('creates endpoint without input schema', () => {
127+
const ctxRouter = new CtxRouter<TestContext>();
128+
const actionFn = jest.fn().mockReturnValue({ status: 'ready' });
129+
130+
const result = ctxRouter.endpoint.action(actionFn);
131+
132+
expect(result.input).toBeUndefined();
133+
expect(result.action).toBe(actionFn);
134+
});
135+
136+
it('maintains context type in action function', () => {
137+
const ctxRouter = new CtxRouter<TestContext>();
138+
139+
const actionFn = jest.fn((context: { userId: string; role: string; request: any }) => {
140+
return { userInfo: context.userId };
141+
});
142+
143+
const result = ctxRouter.endpoint.action<{ userInfo: string }>(actionFn);
144+
145+
expect(result.input).toBeUndefined();
146+
expect(result.action).toBe(actionFn);
147+
});
148+
});
149+
150+
describe('router method', () => {
151+
it('returns router object as-is', () => {
152+
const ctxRouter = new CtxRouter<TestContext>();
153+
const testRouterObj = {
154+
endpoint1: ctxRouter.endpoint.action(() => ({ data: 'test1' })),
155+
endpoint2: ctxRouter.endpoint.input(z.object({ id: z.number() })).action(() => ({ data: 'test2' }))
156+
};
157+
158+
const result = ctxRouter.router(testRouterObj);
159+
160+
expect(result).toBe(testRouterObj);
161+
expect(result.endpoint1).toBeDefined();
162+
expect(result.endpoint2).toBeDefined();
163+
});
164+
165+
it('handles empty router object', () => {
166+
const ctxRouter = new CtxRouter<TestContext>();
167+
const emptyRouterObj = {};
168+
169+
const result = ctxRouter.router(emptyRouterObj);
170+
171+
expect(result).toBe(emptyRouterObj);
172+
expect(Object.keys(result)).toHaveLength(0);
173+
});
174+
});
175+
});
176+
177+
describe('type definitions', () => {
178+
it('Endpoint type works with different configurations', () => {
179+
const schema = z.object({ test: z.string() });
180+
181+
// Test endpoint without input
182+
const noInputEndpoint: Endpoint<string> = {
183+
action: () => 'result'
184+
};
185+
expect(noInputEndpoint.input).toBeUndefined();
186+
187+
// Test endpoint with input
188+
const withInputEndpoint: Endpoint<number, typeof schema> = {
189+
input: schema,
190+
action: (input) => input.test.length
191+
};
192+
expect(withInputEndpoint.input).toBe(schema);
193+
194+
// Test endpoint with context
195+
type Context = { admin: boolean };
196+
const withContextEndpoint: Endpoint<boolean, undefined, Context> = {
197+
action: (context) => context.admin
198+
};
199+
expect(withContextEndpoint.input).toBeUndefined();
200+
});
201+
});
202+
});

0 commit comments

Comments
 (0)