@buenojs/bueno 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
2
|
+
import { Password, JWT, CSRF, createAuthMiddleware } from '../../src/security';
|
|
3
|
+
import { Context } from '../../src/context';
|
|
4
|
+
|
|
5
|
+
describe('Security', () => {
|
|
6
|
+
describe('Password', () => {
|
|
7
|
+
test('should hash password', async () => {
|
|
8
|
+
const password = 'mySecretPassword123';
|
|
9
|
+
const hash = await Password.hash(password);
|
|
10
|
+
|
|
11
|
+
expect(hash).toBeDefined();
|
|
12
|
+
expect(hash).not.toBe(password);
|
|
13
|
+
expect(hash.length).toBeGreaterThan(0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('should verify correct password', async () => {
|
|
17
|
+
const password = 'mySecretPassword123';
|
|
18
|
+
const hash = await Password.hash(password);
|
|
19
|
+
|
|
20
|
+
const isValid = await Password.verify(password, hash);
|
|
21
|
+
expect(isValid).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('should reject wrong password', async () => {
|
|
25
|
+
const password = 'mySecretPassword123';
|
|
26
|
+
const hash = await Password.hash(password);
|
|
27
|
+
|
|
28
|
+
const isValid = await Password.verify('wrongPassword', hash);
|
|
29
|
+
expect(isValid).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('should generate unique hashes for same password', async () => {
|
|
33
|
+
const password = 'mySecretPassword123';
|
|
34
|
+
const hash1 = await Password.hash(password);
|
|
35
|
+
const hash2 = await Password.hash(password);
|
|
36
|
+
|
|
37
|
+
expect(hash1).not.toBe(hash2);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('JWT', () => {
|
|
42
|
+
const secret = 'test-secret-key';
|
|
43
|
+
let jwt: JWT;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
jwt = new JWT(secret);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('should sign payload', async () => {
|
|
50
|
+
const payload = { userId: 123, email: 'test@example.com' };
|
|
51
|
+
const token = await jwt.sign(payload);
|
|
52
|
+
|
|
53
|
+
expect(token).toBeDefined();
|
|
54
|
+
expect(typeof token).toBe('string');
|
|
55
|
+
expect(token.split('.').length).toBe(3); // header.payload.signature
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('should verify and decode token', async () => {
|
|
59
|
+
const payload = { userId: 123, email: 'test@example.com' };
|
|
60
|
+
const token = await jwt.sign(payload);
|
|
61
|
+
|
|
62
|
+
const decoded = await jwt.verify(token);
|
|
63
|
+
|
|
64
|
+
expect(decoded).toBeDefined();
|
|
65
|
+
expect(decoded?.userId).toBe(123);
|
|
66
|
+
expect(decoded?.email).toBe('test@example.com');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('should reject invalid token', async () => {
|
|
70
|
+
const decoded = await jwt.verify('invalid.token.here');
|
|
71
|
+
expect(decoded).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('should reject token signed with different secret', async () => {
|
|
75
|
+
const payload = { userId: 123 };
|
|
76
|
+
const token = await jwt.sign(payload);
|
|
77
|
+
|
|
78
|
+
const otherJwt = new JWT('different-secret');
|
|
79
|
+
const decoded = await otherJwt.verify(token);
|
|
80
|
+
|
|
81
|
+
expect(decoded).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('should handle expired tokens', async () => {
|
|
85
|
+
const jwtWithExpiry = new JWT(secret, { expiresIn: -1 }); // Already expired
|
|
86
|
+
|
|
87
|
+
const payload = { userId: 123 };
|
|
88
|
+
const token = await jwtWithExpiry.sign(payload);
|
|
89
|
+
|
|
90
|
+
// Wait a moment for expiry
|
|
91
|
+
const decoded = await jwtWithExpiry.verify(token);
|
|
92
|
+
expect(decoded).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('CSRF', () => {
|
|
97
|
+
test('should generate CSRF token', () => {
|
|
98
|
+
const token = CSRF.generate();
|
|
99
|
+
|
|
100
|
+
expect(token).toBeDefined();
|
|
101
|
+
expect(typeof token).toBe('string');
|
|
102
|
+
expect(token.length).toBeGreaterThan(0);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('should generate unique tokens', () => {
|
|
106
|
+
const token1 = CSRF.generate();
|
|
107
|
+
const token2 = CSRF.generate();
|
|
108
|
+
|
|
109
|
+
expect(token1).not.toBe(token2);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('should validate token with same value', () => {
|
|
113
|
+
const token = CSRF.generate();
|
|
114
|
+
|
|
115
|
+
expect(CSRF.verify(token, token)).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('should reject invalid token', () => {
|
|
119
|
+
expect(CSRF.verify('invalid-token', 'different-token')).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('Auth Middleware', () => {
|
|
124
|
+
const secret = 'test-secret';
|
|
125
|
+
let jwt: JWT;
|
|
126
|
+
|
|
127
|
+
beforeEach(() => {
|
|
128
|
+
jwt = new JWT(secret);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('should allow valid JWT', async () => {
|
|
132
|
+
const token = await jwt.sign({ userId: 1 });
|
|
133
|
+
const request = new Request('http://localhost:3000/protected', {
|
|
134
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
135
|
+
});
|
|
136
|
+
const context = new Context(request, {});
|
|
137
|
+
|
|
138
|
+
const middleware = createAuthMiddleware({ jwt });
|
|
139
|
+
let passed = false;
|
|
140
|
+
|
|
141
|
+
const response = await middleware(context, async () => {
|
|
142
|
+
passed = true;
|
|
143
|
+
return new Response('OK');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(passed).toBe(true);
|
|
147
|
+
expect(context.get('user')).toBeDefined();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('should reject missing token', async () => {
|
|
151
|
+
const request = new Request('http://localhost:3000/protected');
|
|
152
|
+
const context = new Context(request, {});
|
|
153
|
+
|
|
154
|
+
const middleware = createAuthMiddleware({ jwt });
|
|
155
|
+
|
|
156
|
+
const response = await middleware(context, async () => new Response('OK'));
|
|
157
|
+
|
|
158
|
+
expect(response.status).toBe(401);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('should reject invalid token', async () => {
|
|
162
|
+
const request = new Request('http://localhost:3000/protected', {
|
|
163
|
+
headers: { Authorization: 'Bearer invalid-token' },
|
|
164
|
+
});
|
|
165
|
+
const context = new Context(request, {});
|
|
166
|
+
|
|
167
|
+
const middleware = createAuthMiddleware({ jwt });
|
|
168
|
+
|
|
169
|
+
const response = await middleware(context, async () => new Response('OK'));
|
|
170
|
+
|
|
171
|
+
expect(response.status).toBe(401);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
Span,
|
|
4
|
+
SpanEvent,
|
|
5
|
+
SpanKind,
|
|
6
|
+
StatusCode,
|
|
7
|
+
SpanStatus,
|
|
8
|
+
SpanOptions,
|
|
9
|
+
OTLPExporterOptions,
|
|
10
|
+
SamplerType,
|
|
11
|
+
TracerOptions,
|
|
12
|
+
TraceContext,
|
|
13
|
+
OTLPExporter,
|
|
14
|
+
Tracer,
|
|
15
|
+
generateTraceId,
|
|
16
|
+
generateSpanId,
|
|
17
|
+
nowNanoseconds,
|
|
18
|
+
createTracer,
|
|
19
|
+
traceMiddleware,
|
|
20
|
+
traceDatabase,
|
|
21
|
+
createTracedFetch,
|
|
22
|
+
SpanBuilder,
|
|
23
|
+
span,
|
|
24
|
+
} from '../../src/telemetry/index.ts';
|
|
25
|
+
|
|
26
|
+
describe('Telemetry Module', () => {
|
|
27
|
+
test('generateTraceId should return 32 hex characters', () => {
|
|
28
|
+
const traceId = generateTraceId();
|
|
29
|
+
expect(traceId).toHaveLength(32);
|
|
30
|
+
expect(/^[0-9a-f]{32}$/.test(traceId)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('generateSpanId should return 16 hex characters', () => {
|
|
34
|
+
const spanId = generateSpanId();
|
|
35
|
+
expect(spanId).toHaveLength(16);
|
|
36
|
+
expect(/^[0-9a-f]{16}$/.test(spanId)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('nowNanoseconds should return a number', () => {
|
|
40
|
+
const now = nowNanoseconds();
|
|
41
|
+
expect(typeof now).toBe('number');
|
|
42
|
+
expect(now).toBeGreaterThan(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('Tracer should create spans', () => {
|
|
46
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
47
|
+
const span = tracer.startSpan('test-operation', { kind: 'internal' });
|
|
48
|
+
|
|
49
|
+
expect(span.name).toBe('test-operation');
|
|
50
|
+
expect(span.kind).toBe('internal');
|
|
51
|
+
expect(span.traceId).toHaveLength(32);
|
|
52
|
+
expect(span.spanId).toHaveLength(16);
|
|
53
|
+
expect(span.ended).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('Tracer should end spans', () => {
|
|
57
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
58
|
+
const span = tracer.startSpan('test-operation');
|
|
59
|
+
|
|
60
|
+
tracer.endSpan(span);
|
|
61
|
+
|
|
62
|
+
expect(span.ended).toBe(true);
|
|
63
|
+
expect(span.endTime).toBeDefined();
|
|
64
|
+
expect(span.duration).toBeDefined();
|
|
65
|
+
expect(span.duration).toBeGreaterThanOrEqual(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('Tracer should add attributes', () => {
|
|
69
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
70
|
+
const span = tracer.startSpan('test-operation');
|
|
71
|
+
|
|
72
|
+
tracer.setAttribute(span, 'key1', 'value1');
|
|
73
|
+
tracer.setAttribute(span, 'key2', 123);
|
|
74
|
+
tracer.setAttribute(span, 'key3', true);
|
|
75
|
+
|
|
76
|
+
expect(span.attributes['key1']).toBe('value1');
|
|
77
|
+
expect(span.attributes['key2']).toBe(123);
|
|
78
|
+
expect(span.attributes['key3']).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('Tracer should add events', () => {
|
|
82
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
83
|
+
const span = tracer.startSpan('test-operation');
|
|
84
|
+
|
|
85
|
+
tracer.addEvent(span, 'event1', { detail: 'test' });
|
|
86
|
+
|
|
87
|
+
expect(span.events).toHaveLength(1);
|
|
88
|
+
expect(span.events[0].name).toBe('event1');
|
|
89
|
+
expect(span.events[0].attributes).toEqual({ detail: 'test' });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('Tracer should set status', () => {
|
|
93
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
94
|
+
const span = tracer.startSpan('test-operation');
|
|
95
|
+
|
|
96
|
+
tracer.setStatus(span, 'ok');
|
|
97
|
+
expect(span.status.code).toBe('ok');
|
|
98
|
+
|
|
99
|
+
tracer.setStatus(span, 'error', 'Something went wrong');
|
|
100
|
+
expect(span.status.code).toBe('error');
|
|
101
|
+
expect(span.status.message).toBe('Something went wrong');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('Tracer should record errors', () => {
|
|
105
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
106
|
+
const span = tracer.startSpan('test-operation');
|
|
107
|
+
|
|
108
|
+
const error = new Error('Test error');
|
|
109
|
+
tracer.setError(span, error);
|
|
110
|
+
|
|
111
|
+
expect(span.status.code).toBe('error');
|
|
112
|
+
expect(span.status.message).toBe('Test error');
|
|
113
|
+
expect(span.events).toHaveLength(1);
|
|
114
|
+
expect(span.events[0].name).toBe('exception');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('Tracer.withSpan should manage span lifecycle', async () => {
|
|
118
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
119
|
+
|
|
120
|
+
const result = await tracer.withSpan('test-operation', async (span) => {
|
|
121
|
+
tracer.setAttribute(span, 'test', 'value');
|
|
122
|
+
return 'success';
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(result).toBe('success');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('Tracer.withSpan should record errors', async () => {
|
|
129
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
await tracer.withSpan('test-operation', async () => {
|
|
133
|
+
throw new Error('Test error');
|
|
134
|
+
});
|
|
135
|
+
expect(true).toBe(false); // Should not reach here
|
|
136
|
+
} catch (e) {
|
|
137
|
+
expect((e as Error).message).toBe('Test error');
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('Tracer should inject context', () => {
|
|
142
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
143
|
+
const span = tracer.startSpan('test-operation');
|
|
144
|
+
|
|
145
|
+
const carrier: Record<string, string> = {};
|
|
146
|
+
tracer.injectContext(carrier, span);
|
|
147
|
+
|
|
148
|
+
expect(carrier['traceparent']).toBeDefined();
|
|
149
|
+
expect(carrier['traceparent']).toMatch(/^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('Tracer should extract context', () => {
|
|
153
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
154
|
+
|
|
155
|
+
const carrier = {
|
|
156
|
+
traceparent: '00-0123456789abcdef0123456789abcdef-0123456789abcdef-01',
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const context = tracer.extractContext(carrier);
|
|
160
|
+
|
|
161
|
+
expect(context).not.toBeNull();
|
|
162
|
+
expect(context!.traceId).toBe('0123456789abcdef0123456789abcdef');
|
|
163
|
+
expect(context!.spanId).toBe('0123456789abcdef');
|
|
164
|
+
expect(context!.traceFlags).toBe(1);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('Tracer should return null for invalid traceparent', () => {
|
|
168
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
169
|
+
|
|
170
|
+
expect(tracer.extractContext({})).toBeNull();
|
|
171
|
+
expect(tracer.extractContext({ traceparent: 'invalid' })).toBeNull();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('createTracer should create a configured tracer', () => {
|
|
175
|
+
const tracer = createTracer('my-service', {
|
|
176
|
+
sampler: 'always',
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(tracer).toBeInstanceOf(Tracer);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('Tracer should support never sampler', () => {
|
|
183
|
+
const tracer = new Tracer({ serviceName: 'test-service', sampler: 'never' });
|
|
184
|
+
const span = tracer.startSpan('test-operation');
|
|
185
|
+
|
|
186
|
+
// Span should still be created but not exported
|
|
187
|
+
expect(span.name).toBe('test-operation');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('Tracer should support probabilistic sampler', () => {
|
|
191
|
+
const tracer = new Tracer({
|
|
192
|
+
serviceName: 'test-service',
|
|
193
|
+
sampler: 'probabilistic',
|
|
194
|
+
probability: 0.5,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Create multiple spans and verify some are sampled
|
|
198
|
+
const spans = [];
|
|
199
|
+
for (let i = 0; i < 10; i++) {
|
|
200
|
+
spans.push(tracer.startSpan('test-operation'));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// All spans should be created
|
|
204
|
+
expect(spans).toHaveLength(10);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('Tracer should create child spans', () => {
|
|
208
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
209
|
+
const parentSpan = tracer.startSpan('parent-operation');
|
|
210
|
+
|
|
211
|
+
const childSpan = tracer.startSpan('child-operation', { parent: parentSpan });
|
|
212
|
+
|
|
213
|
+
expect(childSpan.traceId).toBe(parentSpan.traceId);
|
|
214
|
+
expect(childSpan.parentSpanId).toBe(parentSpan.spanId);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('Tracer should set multiple attributes at once', () => {
|
|
218
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
219
|
+
const span = tracer.startSpan('test-operation');
|
|
220
|
+
|
|
221
|
+
tracer.setAttributes(span, {
|
|
222
|
+
key1: 'value1',
|
|
223
|
+
key2: 123,
|
|
224
|
+
key3: true,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(span.attributes['key1']).toBe('value1');
|
|
228
|
+
expect(span.attributes['key2']).toBe(123);
|
|
229
|
+
expect(span.attributes['key3']).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('Tracer should update span name', () => {
|
|
233
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
234
|
+
const span = tracer.startSpan('test-operation');
|
|
235
|
+
|
|
236
|
+
tracer.updateName(span, 'updated-operation');
|
|
237
|
+
|
|
238
|
+
expect(span.name).toBe('updated-operation');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('Tracer should not modify ended spans', () => {
|
|
242
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
243
|
+
const span = tracer.startSpan('test-operation');
|
|
244
|
+
|
|
245
|
+
tracer.endSpan(span);
|
|
246
|
+
|
|
247
|
+
// These should be no-ops
|
|
248
|
+
tracer.setAttribute(span, 'key', 'value');
|
|
249
|
+
tracer.addEvent(span, 'event');
|
|
250
|
+
tracer.setStatus(span, 'error');
|
|
251
|
+
|
|
252
|
+
expect(span.attributes).toEqual({});
|
|
253
|
+
expect(span.events).toHaveLength(0);
|
|
254
|
+
expect(span.status.code).toBe('unset');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('Tracer should get current span', async () => {
|
|
258
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
259
|
+
|
|
260
|
+
expect(tracer.getCurrentSpan()).toBeNull();
|
|
261
|
+
|
|
262
|
+
await tracer.withSpan('test-operation', async (span) => {
|
|
263
|
+
expect(tracer.getCurrentSpan()).toBe(span);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(tracer.getCurrentSpan()).toBeNull();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('SpanBuilder should provide fluent API', () => {
|
|
270
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
271
|
+
|
|
272
|
+
const spanObj = span(tracer, 'test-operation', { kind: 'server' })
|
|
273
|
+
.setAttribute('key1', 'value1')
|
|
274
|
+
.setAttributes({ key2: 123, key3: true })
|
|
275
|
+
.addEvent('event1', { detail: 'test' })
|
|
276
|
+
.setStatus('ok')
|
|
277
|
+
.end();
|
|
278
|
+
|
|
279
|
+
expect(spanObj.name).toBe('test-operation');
|
|
280
|
+
expect(spanObj.kind).toBe('server');
|
|
281
|
+
expect(spanObj.attributes['key1']).toBe('value1');
|
|
282
|
+
expect(spanObj.attributes['key2']).toBe(123);
|
|
283
|
+
expect(spanObj.events).toHaveLength(1);
|
|
284
|
+
expect(spanObj.status.code).toBe('ok');
|
|
285
|
+
expect(spanObj.ended).toBe(true);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('OTLPExporter should be created with options', () => {
|
|
289
|
+
const exporter = new OTLPExporter({
|
|
290
|
+
endpoint: 'http://localhost:4318/v1/traces',
|
|
291
|
+
headers: { 'Authorization': 'Bearer token' },
|
|
292
|
+
exportInterval: 5000,
|
|
293
|
+
maxBatchSize: 100,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
expect(exporter).toBeInstanceOf(OTLPExporter);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('OTLPExporter should set service name', () => {
|
|
300
|
+
const exporter = new OTLPExporter({
|
|
301
|
+
endpoint: 'http://localhost:4318/v1/traces',
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
exporter.setServiceName('test-service');
|
|
305
|
+
exporter.setResourceAttributes({ version: '1.0.0' });
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('OTLPExporter should handle empty flush', async () => {
|
|
309
|
+
const exporter = new OTLPExporter({
|
|
310
|
+
endpoint: 'http://localhost:4318/v1/traces',
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Should not throw
|
|
314
|
+
await exporter.flush();
|
|
315
|
+
await exporter.close();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('traceDatabase should wrap database methods', () => {
|
|
319
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
320
|
+
|
|
321
|
+
const db = {
|
|
322
|
+
query: async (sql: string) => [{ id: 1 }],
|
|
323
|
+
execute: async (sql: string) => { },
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const tracedDb = traceDatabase(tracer, db, 'postgresql');
|
|
327
|
+
|
|
328
|
+
expect(typeof tracedDb.query).toBe('function');
|
|
329
|
+
expect(typeof tracedDb.execute).toBe('function');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test('createTracedFetch should create traced fetch function', () => {
|
|
333
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
334
|
+
const tracedFetch = createTracedFetch(tracer);
|
|
335
|
+
|
|
336
|
+
expect(typeof tracedFetch).toBe('function');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('Tracer should start span from extracted context', () => {
|
|
340
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
341
|
+
|
|
342
|
+
const context: TraceContext = {
|
|
343
|
+
traceId: '0123456789abcdef0123456789abcdef',
|
|
344
|
+
spanId: '0123456789abcdef',
|
|
345
|
+
traceFlags: 1,
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const span = tracer.startSpanFromContext('child-operation', context);
|
|
349
|
+
|
|
350
|
+
expect(span.traceId).toBe(context.traceId);
|
|
351
|
+
expect(span.parentSpanId).toBe(context.spanId);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('Tracer should handle nested withSpan calls', async () => {
|
|
355
|
+
const tracer = new Tracer({ serviceName: 'test-service' });
|
|
356
|
+
|
|
357
|
+
await tracer.withSpan('parent', async (parentSpan) => {
|
|
358
|
+
expect(tracer.getCurrentSpan()).toBe(parentSpan);
|
|
359
|
+
|
|
360
|
+
await tracer.withSpan('child', async (childSpan) => {
|
|
361
|
+
expect(tracer.getCurrentSpan()).toBe(childSpan);
|
|
362
|
+
expect(childSpan.parentSpanId).toBe(parentSpan.spanId);
|
|
363
|
+
expect(childSpan.traceId).toBe(parentSpan.traceId);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
expect(tracer.getCurrentSpan()).toBe(parentSpan);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
expect(tracer.getCurrentSpan()).toBeNull();
|
|
370
|
+
});
|
|
371
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
createTestCache,
|
|
4
|
+
TestCache,
|
|
5
|
+
assertCacheHas,
|
|
6
|
+
assertCacheNotHas,
|
|
7
|
+
assertCacheValue,
|
|
8
|
+
assertCacheStats,
|
|
9
|
+
} from "../../src/testing/index";
|
|
10
|
+
|
|
11
|
+
describe("TestCache", () => {
|
|
12
|
+
test("should create cache with initial data", async () => {
|
|
13
|
+
const cache = await createTestCache({ user: { id: 1, name: "Test" } });
|
|
14
|
+
const user = await cache.get("user");
|
|
15
|
+
expect(user).toEqual({ id: 1, name: "Test" });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("should track operations", async () => {
|
|
19
|
+
const cache = new TestCache();
|
|
20
|
+
await cache.set("key1", "value1");
|
|
21
|
+
await cache.get("key1");
|
|
22
|
+
await cache.get("missing");
|
|
23
|
+
|
|
24
|
+
expect(cache.operations).toHaveLength(3);
|
|
25
|
+
expect(cache.operations[0].type).toBe("set");
|
|
26
|
+
expect(cache.operations[1].type).toBe("get");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("should track statistics", async () => {
|
|
30
|
+
const cache = new TestCache();
|
|
31
|
+
await cache.set("key1", "value1");
|
|
32
|
+
await cache.get("key1"); // hit
|
|
33
|
+
await cache.get("missing"); // miss
|
|
34
|
+
|
|
35
|
+
const stats = cache.getStats();
|
|
36
|
+
expect(stats.hits).toBe(1);
|
|
37
|
+
expect(stats.misses).toBe(1);
|
|
38
|
+
expect(stats.sets).toBe(1);
|
|
39
|
+
expect(stats.keyCount).toBe(1);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("peek should not affect stats", async () => {
|
|
43
|
+
const cache = new TestCache();
|
|
44
|
+
await cache.set("key1", "value1");
|
|
45
|
+
cache.peek("key1");
|
|
46
|
+
cache.peek("missing");
|
|
47
|
+
|
|
48
|
+
const stats = cache.getStats();
|
|
49
|
+
expect(stats.hits).toBe(0);
|
|
50
|
+
expect(stats.misses).toBe(0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("setMany should set multiple entries", async () => {
|
|
54
|
+
const cache = new TestCache();
|
|
55
|
+
await cache.setMany({ a: 1, b: 2, c: 3 });
|
|
56
|
+
|
|
57
|
+
expect(cache.getKeys()).toContain("a");
|
|
58
|
+
expect(cache.getKeys()).toContain("b");
|
|
59
|
+
expect(cache.getKeys()).toContain("c");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("reset should clear everything", async () => {
|
|
63
|
+
const cache = new TestCache();
|
|
64
|
+
await cache.set("key1", "value1");
|
|
65
|
+
await cache.get("key1");
|
|
66
|
+
cache.reset();
|
|
67
|
+
|
|
68
|
+
expect(cache.getKeys()).toHaveLength(0);
|
|
69
|
+
expect(cache.operations).toHaveLength(0);
|
|
70
|
+
expect(cache.getStats().sets).toBe(0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("assertions should work", async () => {
|
|
74
|
+
const cache = await createTestCache({ existing: "value" });
|
|
75
|
+
|
|
76
|
+
assertCacheHas(cache, "existing");
|
|
77
|
+
assertCacheNotHas(cache, "missing");
|
|
78
|
+
assertCacheValue(cache, "existing", "value");
|
|
79
|
+
assertCacheStats(cache, { keyCount: 1 });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("getEntries should return all key-value pairs", async () => {
|
|
83
|
+
const cache = new TestCache();
|
|
84
|
+
await cache.set("a", 1);
|
|
85
|
+
await cache.set("b", 2);
|
|
86
|
+
|
|
87
|
+
const entries = cache.getEntries();
|
|
88
|
+
expect(entries).toHaveLength(2);
|
|
89
|
+
expect(Object.fromEntries(entries)).toEqual({ a: 1, b: 2 });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("clearAll should clear all keys", async () => {
|
|
93
|
+
const cache = new TestCache();
|
|
94
|
+
await cache.set("a", 1);
|
|
95
|
+
await cache.set("b", 2);
|
|
96
|
+
await cache.clearAll();
|
|
97
|
+
|
|
98
|
+
expect(cache.getKeys()).toHaveLength(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("delete should track deletes in stats", async () => {
|
|
102
|
+
const cache = new TestCache();
|
|
103
|
+
await cache.set("key1", "value1");
|
|
104
|
+
await cache.delete("key1");
|
|
105
|
+
|
|
106
|
+
const stats = cache.getStats();
|
|
107
|
+
expect(stats.deletes).toBe(1);
|
|
108
|
+
expect(stats.keyCount).toBe(0);
|
|
109
|
+
});
|
|
110
|
+
});
|