@tumbaland/backend-core 1.16.0 → 1.16.1
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/package.json +1 -1
- package/src/app/createBaseApp.test.ts +9 -0
- package/src/health/healthController.test.ts +23 -1
- package/src/logging/logger.test.ts +15 -0
- package/src/metrics/index.test.ts +25 -0
- package/src/middleware/validate.test.ts +12 -0
- package/src/tracing/index.test.ts +18 -0
- package/tsconfig.build.json +3 -0
- package/tsconfig.json +4 -1
package/package.json
CHANGED
|
@@ -54,6 +54,15 @@ describe('createBaseApp', () => {
|
|
|
54
54
|
expect(second.status).toBe(429);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
it('uses the plain request logger (no metrics histogram) when metrics is false', async () => {
|
|
58
|
+
const app = createBaseApp({ metrics: false });
|
|
59
|
+
app.get('/ping', (_req, res) => res.json({ ok: true }));
|
|
60
|
+
|
|
61
|
+
const res = await request(app).get('/ping');
|
|
62
|
+
|
|
63
|
+
expect(res.status).toBe(200);
|
|
64
|
+
});
|
|
65
|
+
|
|
57
66
|
it('mounts /health/live ahead of the rate limiter, so it never gets 429s a busy service would', async () => {
|
|
58
67
|
const app = createBaseApp({ rateLimiter: (await import('../middleware/security')).createRateLimiter({ windowMs: 60_000, max: 1 }) });
|
|
59
68
|
app.get('/ping', (_req, res) => res.json({ ok: true }));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Request, Response } from 'express';
|
|
2
2
|
import mongoose from 'mongoose';
|
|
3
|
-
import { healthCheck, healthLive } from './healthController';
|
|
3
|
+
import { healthCheck, healthLive, metricsHandler } from './healthController';
|
|
4
4
|
|
|
5
5
|
function mockRes(): Response {
|
|
6
6
|
const res: Partial<Response> = {};
|
|
@@ -88,4 +88,26 @@ describe('healthCheck', () => {
|
|
|
88
88
|
expect(res.status).toHaveBeenCalledWith(503);
|
|
89
89
|
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ status: 'error', error: 'Health check failed' }));
|
|
90
90
|
});
|
|
91
|
+
|
|
92
|
+
it('falls back to default service/version/description in the error body when env vars are unset', async () => {
|
|
93
|
+
delete process.env.SERVICE_NAME;
|
|
94
|
+
delete process.env.npm_package_version;
|
|
95
|
+
delete process.env.SERVICE_DESCRIPTION;
|
|
96
|
+
jest.spyOn(mongoose, 'connection', 'get').mockImplementation(() => {
|
|
97
|
+
throw new Error('boom');
|
|
98
|
+
});
|
|
99
|
+
const res = mockRes();
|
|
100
|
+
|
|
101
|
+
await healthCheck({} as Request, res);
|
|
102
|
+
|
|
103
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
104
|
+
expect.objectContaining({ service: 'unknown-service', version: '1.0.0', description: 'Backend service' })
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('metricsHandler re-export', () => {
|
|
110
|
+
it('re-exports the same metricsHandler function from ../metrics', () => {
|
|
111
|
+
expect(typeof metricsHandler).toBe('function');
|
|
112
|
+
});
|
|
91
113
|
});
|
|
@@ -35,6 +35,21 @@ describe('logger', () => {
|
|
|
35
35
|
expect(logger.level).toBe('info');
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
it('defaults to development (debug level) when NODE_ENV is unset', async () => {
|
|
39
|
+
const ORIGINAL_ENV = process.env;
|
|
40
|
+
const merged: Record<string, string | undefined> = { ...ORIGINAL_ENV };
|
|
41
|
+
delete merged.NODE_ENV;
|
|
42
|
+
process.env = merged as NodeJS.ProcessEnv;
|
|
43
|
+
|
|
44
|
+
let loggerModule!: typeof import('./logger').default;
|
|
45
|
+
await jest.isolateModulesAsync(async () => {
|
|
46
|
+
loggerModule = (await import('./logger')).default;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
process.env = ORIGINAL_ENV;
|
|
50
|
+
expect(loggerModule.level).toBe('debug');
|
|
51
|
+
});
|
|
52
|
+
|
|
38
53
|
it('does not create a file transport when LOG_TO_FILE is unset', async () => {
|
|
39
54
|
const logger = await loadLogger({ NODE_ENV: 'development', LOG_TO_FILE: undefined });
|
|
40
55
|
expect(namedTransports(logger, 'file')).toHaveLength(0);
|
|
@@ -81,6 +81,31 @@ describe('metricsMiddleware', () => {
|
|
|
81
81
|
});
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
+
describe('default labels', () => {
|
|
85
|
+
it('falls back to app "unknown-service" and version "1.0.0" when their env vars are unset', async () => {
|
|
86
|
+
const ORIGINAL_ENV = process.env;
|
|
87
|
+
const merged: Record<string, string | undefined> = { ...ORIGINAL_ENV };
|
|
88
|
+
delete merged.SERVICE_NAME;
|
|
89
|
+
delete merged.npm_package_version;
|
|
90
|
+
process.env = merged as NodeJS.ProcessEnv;
|
|
91
|
+
|
|
92
|
+
// isolateModulesAsync sandboxes the whole module registry, including
|
|
93
|
+
// prom-client itself — the spy must target the Registry class required
|
|
94
|
+
// *inside* the sandbox, not the one imported at the top of this file.
|
|
95
|
+
await jest.isolateModulesAsync(async () => {
|
|
96
|
+
const innerPromClient = (await import('prom-client')).default;
|
|
97
|
+
const setDefaultLabelsSpy = jest.spyOn(innerPromClient.Registry.prototype, 'setDefaultLabels');
|
|
98
|
+
|
|
99
|
+
await import('./index');
|
|
100
|
+
|
|
101
|
+
expect(setDefaultLabelsSpy).toHaveBeenCalledWith({ app: 'unknown-service', version: '1.0.0' });
|
|
102
|
+
setDefaultLabelsSpy.mockRestore();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
process.env = ORIGINAL_ENV;
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
84
109
|
describe('businessMetrics', () => {
|
|
85
110
|
it('exposes counters that can be incremented without throwing', () => {
|
|
86
111
|
expect(() => businessMetrics.albumsCreated.inc()).not.toThrow();
|
|
@@ -57,4 +57,16 @@ describe('validate', () => {
|
|
|
57
57
|
expect(res.status).toBe(200);
|
|
58
58
|
expect(res.body.data).toEqual({ name: 'Carol' });
|
|
59
59
|
});
|
|
60
|
+
|
|
61
|
+
it('omits the field prefix when the zod issue has no path (whole-body validation)', async () => {
|
|
62
|
+
const arraySchema = z.array(z.string());
|
|
63
|
+
const app = buildApp(arraySchema);
|
|
64
|
+
const rawResult = arraySchema.safeParse({ not: 'an array' });
|
|
65
|
+
|
|
66
|
+
const res = await request(app).post('/things').send({ not: 'an array' });
|
|
67
|
+
|
|
68
|
+
expect(res.status).toBe(400);
|
|
69
|
+
expect(rawResult.success).toBe(false);
|
|
70
|
+
expect(res.body.message).toBe(!rawResult.success ? rawResult.error.issues[0].message : undefined);
|
|
71
|
+
});
|
|
60
72
|
});
|
|
@@ -39,6 +39,7 @@ jest.mock('@opentelemetry/resources', () => ({
|
|
|
39
39
|
}));
|
|
40
40
|
|
|
41
41
|
import { context } from '@opentelemetry/api';
|
|
42
|
+
import { ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
|
|
42
43
|
|
|
43
44
|
async function loadTracing() {
|
|
44
45
|
let tracing!: typeof import('./index');
|
|
@@ -92,6 +93,23 @@ describe('initTracer', () => {
|
|
|
92
93
|
expect(exporterCtorMock).toHaveBeenCalledWith({ url: 'http://jaeger-endpoint/v1/traces' });
|
|
93
94
|
});
|
|
94
95
|
|
|
96
|
+
it('falls back to version 1.0.0 and environment "development" when their env vars are unset', async () => {
|
|
97
|
+
const merged: Record<string, string | undefined> = { ...process.env };
|
|
98
|
+
delete merged.npm_package_version;
|
|
99
|
+
delete merged.NODE_ENV;
|
|
100
|
+
process.env = merged as NodeJS.ProcessEnv;
|
|
101
|
+
|
|
102
|
+
const { initTracer } = await loadTracing();
|
|
103
|
+
initTracer('album-service');
|
|
104
|
+
|
|
105
|
+
expect(resourceFromAttributesMock).toHaveBeenCalledWith(
|
|
106
|
+
expect.objectContaining({
|
|
107
|
+
[ATTR_SERVICE_VERSION]: '1.0.0',
|
|
108
|
+
'deployment.environment': 'development'
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
95
113
|
it('registers the provider and returns a tracer scoped to the given service name', async () => {
|
|
96
114
|
const { initTracer } = await loadTracing();
|
|
97
115
|
|
package/tsconfig.build.json
CHANGED
package/tsconfig.json
CHANGED
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "es2023",
|
|
4
4
|
"module": "commonjs",
|
|
5
|
-
"lib": ["ES2020"],
|
|
6
5
|
"outDir": "./dist",
|
|
7
6
|
"rootDir": "./src",
|
|
8
7
|
"strict": true,
|
|
9
8
|
"esModuleInterop": true,
|
|
10
9
|
"skipLibCheck": true,
|
|
11
10
|
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"moduleDetection": "force",
|
|
12
|
+
"noImplicitReturns": true,
|
|
13
|
+
"noFallthroughCasesInSwitch": true,
|
|
14
|
+
"noImplicitOverride": true,
|
|
12
15
|
"declaration": true,
|
|
13
16
|
"declarationMap": true,
|
|
14
17
|
"sourceMap": true,
|