@terreno/api 0.20.2 → 0.22.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/.ai/guidelines/core.md +71 -0
- package/.ai/skills/mongoose-schema-safety/SKILL.md +143 -0
- package/README.md +54 -1
- package/bunfig.toml +1 -1
- package/dist/__tests__/versionCheckPlugin.test.js +29 -7
- package/dist/actions.openApi.test.js +13 -11
- package/dist/api.js +98 -11
- package/dist/api.query.test.js +31 -1
- package/dist/api.test.js +211 -0
- package/dist/auth.test.js +418 -43
- package/dist/betterAuth.d.ts +1 -1
- package/dist/consentApp.test.js +1 -0
- package/dist/example.js +4 -4
- package/dist/expressServer.d.ts +0 -22
- package/dist/expressServer.js +1 -125
- package/dist/expressServer.test.js +90 -91
- package/dist/githubAuth.test.js +22 -22
- package/dist/logger.d.ts +154 -0
- package/dist/logger.js +445 -26
- package/dist/logger.test.js +435 -0
- package/dist/middleware.d.ts +7 -0
- package/dist/middleware.js +58 -1
- package/dist/middleware.test.js +159 -0
- package/dist/models/consentForm.js +2 -1
- package/dist/models/consentResponse.js +2 -1
- package/dist/models/versionConfig.js +2 -1
- package/dist/openApi.test.js +10 -17
- package/dist/openApiBuilder.d.ts +18 -0
- package/dist/openApiBuilder.js +21 -0
- package/dist/openApiBuilder.test.js +34 -10
- package/dist/permissions.test.js +10 -43
- package/dist/populate.test.js +10 -42
- package/dist/realtime/changeStreamWatcher.d.ts +4 -4
- package/dist/realtime/changeStreamWatcher.js +2 -4
- package/dist/realtime/queryMatcher.d.ts +1 -1
- package/dist/realtime/queryMatcher.js +39 -14
- package/dist/realtime/types.d.ts +3 -3
- package/dist/requestContext.d.ts +61 -0
- package/dist/requestContext.js +74 -0
- package/dist/secretProviders.test.js +335 -0
- package/dist/syncConsents.test.js +2 -2
- package/dist/terrenoApp.d.ts +27 -15
- package/dist/terrenoApp.js +24 -14
- package/dist/terrenoApp.test.js +52 -0
- package/dist/tests/bunSetup.js +66 -262
- package/dist/tests/createTestData.d.ts +9 -0
- package/dist/tests/createTestData.js +272 -0
- package/dist/tests/models.d.ts +71 -0
- package/dist/tests/models.js +134 -0
- package/dist/tests/mongoTestSetup.d.ts +7 -0
- package/dist/tests/mongoTestSetup.js +150 -0
- package/dist/tests/testEnv.d.ts +0 -0
- package/dist/tests/testEnv.js +6 -0
- package/dist/tests/testHelper.d.ts +22 -0
- package/dist/tests/testHelper.js +115 -0
- package/dist/tests/types.d.ts +29 -0
- package/dist/tests/types.js +2 -0
- package/dist/tests.d.ts +10 -78
- package/dist/tests.js +24 -241
- package/dist/transformers.test.js +14 -50
- package/package.json +18 -4
- package/src/__snapshots__/openApiBuilder.test.ts.snap +1 -0
- package/src/__tests__/versionCheckPlugin.test.ts +43 -15
- package/src/actions.openApi.test.ts +12 -10
- package/src/api.query.test.ts +24 -1
- package/src/api.test.ts +169 -0
- package/src/api.ts +71 -0
- package/src/auth.test.ts +287 -39
- package/src/betterAuth.ts +1 -1
- package/src/consentApp.test.ts +1 -0
- package/src/example.ts +4 -4
- package/src/expressServer.test.ts +82 -85
- package/src/expressServer.ts +1 -213
- package/src/githubAuth.test.ts +22 -22
- package/src/logger.test.ts +466 -1
- package/src/logger.ts +477 -14
- package/src/middleware.test.ts +74 -2
- package/src/middleware.ts +57 -0
- package/src/models/consentForm.ts +3 -4
- package/src/models/consentResponse.ts +6 -4
- package/src/models/versionConfig.ts +3 -4
- package/src/openApi.test.ts +10 -17
- package/src/openApiBuilder.test.ts +27 -10
- package/src/openApiBuilder.ts +24 -0
- package/src/permissions.test.ts +8 -23
- package/src/populate.test.ts +7 -22
- package/src/realtime/changeStreamWatcher.ts +15 -10
- package/src/realtime/queryMatcher.ts +54 -27
- package/src/realtime/types.ts +4 -4
- package/src/requestContext.ts +86 -0
- package/src/secretProviders.test.ts +219 -1
- package/src/syncConsents.test.ts +1 -1
- package/src/terrenoApp.test.ts +38 -0
- package/src/terrenoApp.ts +37 -15
- package/src/tests/bunSetup.ts +22 -236
- package/src/tests/createTestData.ts +176 -0
- package/src/tests/models.ts +164 -0
- package/src/tests/mongoTestSetup.ts +69 -0
- package/src/tests/testEnv.ts +4 -0
- package/src/tests/testHelper.ts +57 -0
- package/src/tests/types.ts +35 -0
- package/src/tests.ts +40 -231
- package/src/transformers.test.ts +11 -30
- package/tsconfig.typedoc.json +4 -0
- package/dist/tests/index.d.ts +0 -1
- package/dist/tests/index.js +0 -17
- package/src/tests/index.ts +0 -1
package/src/githubAuth.test.ts
CHANGED
|
@@ -8,10 +8,10 @@ import supertest from "supertest";
|
|
|
8
8
|
import type TestAgent from "supertest/lib/agent";
|
|
9
9
|
|
|
10
10
|
import {generateTokens, type UserModel} from "./auth";
|
|
11
|
-
import {setupServer} from "./expressServer";
|
|
12
11
|
import {type GitHubUserFields, githubUserPlugin, setupGitHubAuth} from "./githubAuth";
|
|
13
12
|
import {logger} from "./logger";
|
|
14
13
|
import {createdUpdatedPlugin, isDisabledPlugin} from "./plugins";
|
|
14
|
+
import {TerrenoApp} from "./terrenoApp";
|
|
15
15
|
|
|
16
16
|
interface FakeStrategyOutcome {
|
|
17
17
|
type: "success" | "redirect" | "fail";
|
|
@@ -142,8 +142,8 @@ describe("GitHub auth routes", () => {
|
|
|
142
142
|
router.get("/test", (_req, res) => res.json({ok: true}));
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
app =
|
|
146
|
-
addRoutes,
|
|
145
|
+
app = new TerrenoApp({
|
|
146
|
+
configureApp: addRoutes,
|
|
147
147
|
githubAuth: {
|
|
148
148
|
allowAccountLinking: true,
|
|
149
149
|
callbackURL: "http://localhost:9000/auth/github/callback",
|
|
@@ -152,7 +152,7 @@ describe("GitHub auth routes", () => {
|
|
|
152
152
|
},
|
|
153
153
|
skipListen: true,
|
|
154
154
|
userModel: GitHubTestUserModel as any,
|
|
155
|
-
});
|
|
155
|
+
}).build();
|
|
156
156
|
agent = supertest.agent(app);
|
|
157
157
|
});
|
|
158
158
|
|
|
@@ -249,11 +249,11 @@ describe("GitHub auth disabled", () => {
|
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
// Setup server WITHOUT GitHub auth
|
|
252
|
-
app =
|
|
253
|
-
addRoutes,
|
|
252
|
+
app = new TerrenoApp({
|
|
253
|
+
configureApp: addRoutes,
|
|
254
254
|
skipListen: true,
|
|
255
255
|
userModel: GitHubTestUserModel as any,
|
|
256
|
-
});
|
|
256
|
+
}).build();
|
|
257
257
|
agent = supertest.agent(app);
|
|
258
258
|
});
|
|
259
259
|
|
|
@@ -517,8 +517,8 @@ describe("addGitHubAuthRoutes link endpoints", () => {
|
|
|
517
517
|
router.get("/test", (_req, res) => res.json({ok: true}));
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
-
app =
|
|
521
|
-
addRoutes,
|
|
520
|
+
app = new TerrenoApp({
|
|
521
|
+
configureApp: addRoutes,
|
|
522
522
|
githubAuth: {
|
|
523
523
|
allowAccountLinking: true,
|
|
524
524
|
callbackURL: "http://localhost:9000/auth/github/callback",
|
|
@@ -527,7 +527,7 @@ describe("addGitHubAuthRoutes link endpoints", () => {
|
|
|
527
527
|
},
|
|
528
528
|
skipListen: true,
|
|
529
529
|
userModel: GitHubTestUserModel as any,
|
|
530
|
-
});
|
|
530
|
+
}).build();
|
|
531
531
|
agent = supertest.agent(app);
|
|
532
532
|
});
|
|
533
533
|
|
|
@@ -586,10 +586,10 @@ describe("GitHub callback handler (fake strategy)", () => {
|
|
|
586
586
|
router.get("/test", (_req, res) => res.json({ok: true}));
|
|
587
587
|
}
|
|
588
588
|
|
|
589
|
-
app =
|
|
590
|
-
|
|
589
|
+
app = new TerrenoApp({
|
|
590
|
+
beforeJsonSetup: (a) => {
|
|
591
591
|
// The handler reads (req as unknown as {session?: {returnTo?: string}}).session?.returnTo.
|
|
592
|
-
//
|
|
592
|
+
// TerrenoApp does not install express-session, so prime a fake session from a request
|
|
593
593
|
// header for tests.
|
|
594
594
|
a.use((req, _res, next) => {
|
|
595
595
|
const headerReturnTo = req.headers["x-mock-return-to"];
|
|
@@ -599,7 +599,7 @@ describe("GitHub callback handler (fake strategy)", () => {
|
|
|
599
599
|
next();
|
|
600
600
|
});
|
|
601
601
|
},
|
|
602
|
-
addRoutes,
|
|
602
|
+
configureApp: addRoutes,
|
|
603
603
|
githubAuth: {
|
|
604
604
|
allowAccountLinking: true,
|
|
605
605
|
callbackURL: "http://localhost:9000/auth/github/callback",
|
|
@@ -608,8 +608,8 @@ describe("GitHub callback handler (fake strategy)", () => {
|
|
|
608
608
|
},
|
|
609
609
|
skipListen: true,
|
|
610
610
|
userModel: GitHubTestUserModel as unknown as UserModel,
|
|
611
|
-
});
|
|
612
|
-
// Swap the github strategy with our fake after
|
|
611
|
+
}).build();
|
|
612
|
+
// Swap the github strategy with our fake after TerrenoApp registered it.
|
|
613
613
|
installFakeGithubStrategy();
|
|
614
614
|
agent = supertest.agent(app);
|
|
615
615
|
});
|
|
@@ -691,8 +691,8 @@ describe("GET /auth/github/link with JWT (fake strategy)", () => {
|
|
|
691
691
|
router.get("/test", (_req, res) => res.json({ok: true}));
|
|
692
692
|
}
|
|
693
693
|
|
|
694
|
-
app =
|
|
695
|
-
addRoutes,
|
|
694
|
+
app = new TerrenoApp({
|
|
695
|
+
configureApp: addRoutes,
|
|
696
696
|
githubAuth: {
|
|
697
697
|
allowAccountLinking: true,
|
|
698
698
|
callbackURL: "http://localhost:9000/auth/github/callback",
|
|
@@ -701,7 +701,7 @@ describe("GET /auth/github/link with JWT (fake strategy)", () => {
|
|
|
701
701
|
},
|
|
702
702
|
skipListen: true,
|
|
703
703
|
userModel: GitHubTestUserModel as unknown as UserModel,
|
|
704
|
-
});
|
|
704
|
+
}).build();
|
|
705
705
|
installFakeGithubStrategy();
|
|
706
706
|
agent = supertest.agent(app);
|
|
707
707
|
});
|
|
@@ -746,8 +746,8 @@ describe("DELETE /auth/github/unlink edge cases", () => {
|
|
|
746
746
|
router.get("/test", (_req, res) => res.json({ok: true}));
|
|
747
747
|
}
|
|
748
748
|
|
|
749
|
-
app =
|
|
750
|
-
addRoutes,
|
|
749
|
+
app = new TerrenoApp({
|
|
750
|
+
configureApp: addRoutes,
|
|
751
751
|
githubAuth: {
|
|
752
752
|
allowAccountLinking: true,
|
|
753
753
|
callbackURL: "http://localhost:9000/auth/github/callback",
|
|
@@ -756,7 +756,7 @@ describe("DELETE /auth/github/unlink edge cases", () => {
|
|
|
756
756
|
},
|
|
757
757
|
skipListen: true,
|
|
758
758
|
userModel: GitHubTestUserModel as unknown as UserModel,
|
|
759
|
-
});
|
|
759
|
+
}).build();
|
|
760
760
|
installFakeGithubStrategy();
|
|
761
761
|
agent = supertest.agent(app);
|
|
762
762
|
});
|
package/src/logger.test.ts
CHANGED
|
@@ -2,9 +2,19 @@ import {afterEach, beforeEach, describe, expect, it} from "bun:test";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import {Writable} from "node:stream";
|
|
5
6
|
import winston from "winston";
|
|
6
7
|
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
createFeatureFlaggedLogger,
|
|
10
|
+
createScopedLogger,
|
|
11
|
+
formatLogContextSuffix,
|
|
12
|
+
logger,
|
|
13
|
+
setupLogging,
|
|
14
|
+
type TerrenoRequestLogEntry,
|
|
15
|
+
winstonLogger,
|
|
16
|
+
} from "./logger";
|
|
17
|
+
import {runWithRequestContext} from "./requestContext";
|
|
8
18
|
|
|
9
19
|
describe("logger", () => {
|
|
10
20
|
const OLD_ENV = process.env;
|
|
@@ -47,6 +57,420 @@ describe("logger", () => {
|
|
|
47
57
|
});
|
|
48
58
|
});
|
|
49
59
|
|
|
60
|
+
describe("formatLogContextSuffix", () => {
|
|
61
|
+
it("includes request ids and sorts terrenoLabels for stable output", () => {
|
|
62
|
+
const suffix = formatLogContextSuffix({
|
|
63
|
+
requestId: "r1",
|
|
64
|
+
terrenoLabels: {alpha: "a", zebra: "z"},
|
|
65
|
+
});
|
|
66
|
+
expect(suffix).toContain("requestId=r1");
|
|
67
|
+
expect(suffix.indexOf("alpha=a")).toBeLessThan(suffix.indexOf("zebra=z"));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("returns empty string when no fields are set", () => {
|
|
71
|
+
expect(formatLogContextSuffix({})).toBe("");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("includes terrenoLogPrefix in suffix", () => {
|
|
75
|
+
const suffix = formatLogContextSuffix({
|
|
76
|
+
requestId: "r1",
|
|
77
|
+
terrenoLogPrefix: "[Job]",
|
|
78
|
+
});
|
|
79
|
+
expect(suffix).toContain("logPrefix=[Job]");
|
|
80
|
+
expect(suffix).toContain("requestId=r1");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("createScopedLogger", () => {
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
setupLogging({disableFileLogging: true});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("returns the global logger when prefix and labels are empty", () => {
|
|
90
|
+
expect(createScopedLogger({})).toBe(logger);
|
|
91
|
+
expect(createScopedLogger({labels: {skipped: undefined}})).toBe(logger);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("prefixes messages when only prefix is set", () => {
|
|
95
|
+
const lines: string[] = [];
|
|
96
|
+
const snapshots: Array<{terrenoLogPrefix?: unknown}> = [];
|
|
97
|
+
const captureTransport = new winston.transports.Stream({
|
|
98
|
+
format: winston.format.combine(
|
|
99
|
+
winston.format((info) => {
|
|
100
|
+
snapshots.push({
|
|
101
|
+
terrenoLogPrefix: (info as {terrenoLogPrefix?: unknown}).terrenoLogPrefix,
|
|
102
|
+
});
|
|
103
|
+
return info;
|
|
104
|
+
})(),
|
|
105
|
+
winston.format.printf((info) => {
|
|
106
|
+
const msg = typeof info.message === "string" ? info.message : String(info.message);
|
|
107
|
+
return `${info.level}: ${msg}`;
|
|
108
|
+
})
|
|
109
|
+
),
|
|
110
|
+
stream: new Writable({
|
|
111
|
+
write(chunk, _encoding, callback): void {
|
|
112
|
+
lines.push(chunk.toString().trim());
|
|
113
|
+
callback();
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
winstonLogger.add(captureTransport);
|
|
118
|
+
try {
|
|
119
|
+
createScopedLogger({prefix: "[ScopedTest]"}).info("hello");
|
|
120
|
+
} finally {
|
|
121
|
+
winstonLogger.remove(captureTransport);
|
|
122
|
+
}
|
|
123
|
+
expect(lines.some((l) => l.includes("[ScopedTest]") && l.includes("hello"))).toBe(true);
|
|
124
|
+
expect(snapshots.some((s) => s.terrenoLogPrefix === "[ScopedTest]")).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("attaches terrenoLabels to winston metadata for structured transports", () => {
|
|
128
|
+
const snapshots: Array<{level?: string; message?: unknown; terrenoLabels?: unknown}> = [];
|
|
129
|
+
const captureTransport = new winston.transports.Stream({
|
|
130
|
+
format: winston.format((info) => {
|
|
131
|
+
snapshots.push({
|
|
132
|
+
level: info.level,
|
|
133
|
+
message: info.message,
|
|
134
|
+
terrenoLabels: (info as {terrenoLabels?: unknown}).terrenoLabels,
|
|
135
|
+
});
|
|
136
|
+
return info;
|
|
137
|
+
})(),
|
|
138
|
+
stream: new Writable({
|
|
139
|
+
write(_chunk, _encoding, callback): void {
|
|
140
|
+
callback();
|
|
141
|
+
},
|
|
142
|
+
}),
|
|
143
|
+
});
|
|
144
|
+
winstonLogger.add(captureTransport);
|
|
145
|
+
try {
|
|
146
|
+
createScopedLogger({labels: {billingId: "b1"}}).warn("charged");
|
|
147
|
+
} finally {
|
|
148
|
+
winstonLogger.remove(captureTransport);
|
|
149
|
+
}
|
|
150
|
+
expect(
|
|
151
|
+
snapshots.some(
|
|
152
|
+
(s) => (s.terrenoLabels as {billingId?: string} | undefined)?.billingId === "b1"
|
|
153
|
+
)
|
|
154
|
+
).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("includes terrenoLogPrefix alongside terrenoLabels in metadata", () => {
|
|
158
|
+
const snapshots: Array<{terrenoLogPrefix?: unknown; terrenoLabels?: unknown}> = [];
|
|
159
|
+
const captureTransport = new winston.transports.Stream({
|
|
160
|
+
format: winston.format((info) => {
|
|
161
|
+
snapshots.push({
|
|
162
|
+
terrenoLabels: (info as {terrenoLabels?: unknown}).terrenoLabels,
|
|
163
|
+
terrenoLogPrefix: (info as {terrenoLogPrefix?: unknown}).terrenoLogPrefix,
|
|
164
|
+
});
|
|
165
|
+
return info;
|
|
166
|
+
})(),
|
|
167
|
+
stream: new Writable({
|
|
168
|
+
write(_chunk, _encoding, callback): void {
|
|
169
|
+
callback();
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
});
|
|
173
|
+
winstonLogger.add(captureTransport);
|
|
174
|
+
try {
|
|
175
|
+
createScopedLogger({labels: {x: "1"}, prefix: "[Both]"}).info("m");
|
|
176
|
+
} finally {
|
|
177
|
+
winstonLogger.remove(captureTransport);
|
|
178
|
+
}
|
|
179
|
+
expect(
|
|
180
|
+
snapshots.some(
|
|
181
|
+
(s) =>
|
|
182
|
+
(s.terrenoLabels as {x?: string} | undefined)?.x === "1" &&
|
|
183
|
+
(s.terrenoLogPrefix as string | undefined) === "[Both]"
|
|
184
|
+
)
|
|
185
|
+
).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("merges all request context fields including jobId, sessionId, spanId, traceId, traceSampled", () => {
|
|
189
|
+
const snapshots: Array<Record<string, unknown>> = [];
|
|
190
|
+
const captureTransport = new winston.transports.Stream({
|
|
191
|
+
format: winston.format((info) => {
|
|
192
|
+
snapshots.push({
|
|
193
|
+
jobId: info.jobId,
|
|
194
|
+
requestId: info.requestId,
|
|
195
|
+
sessionId: info.sessionId,
|
|
196
|
+
spanId: info.spanId,
|
|
197
|
+
traceId: info.traceId,
|
|
198
|
+
traceSampled: info.traceSampled,
|
|
199
|
+
userId: info.userId,
|
|
200
|
+
});
|
|
201
|
+
return info;
|
|
202
|
+
})(),
|
|
203
|
+
stream: new Writable({
|
|
204
|
+
write(_chunk, _encoding, callback): void {
|
|
205
|
+
callback();
|
|
206
|
+
},
|
|
207
|
+
}),
|
|
208
|
+
});
|
|
209
|
+
winstonLogger.add(captureTransport);
|
|
210
|
+
try {
|
|
211
|
+
runWithRequestContext(
|
|
212
|
+
{
|
|
213
|
+
jobId: "job-42",
|
|
214
|
+
requestId: "req-full",
|
|
215
|
+
sessionId: "sess-7",
|
|
216
|
+
spanId: "span-abc",
|
|
217
|
+
traceId: "trace-xyz",
|
|
218
|
+
traceSampled: true,
|
|
219
|
+
userId: "user-1",
|
|
220
|
+
},
|
|
221
|
+
() => {
|
|
222
|
+
logger.info("full context");
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
} finally {
|
|
226
|
+
winstonLogger.remove(captureTransport);
|
|
227
|
+
}
|
|
228
|
+
const entry = snapshots.find((s) => s.requestId === "req-full");
|
|
229
|
+
expect(entry).toBeDefined();
|
|
230
|
+
expect(entry?.jobId).toBe("job-42");
|
|
231
|
+
expect(entry?.sessionId).toBe("sess-7");
|
|
232
|
+
expect(entry?.spanId).toBe("span-abc");
|
|
233
|
+
expect(entry?.traceId).toBe("trace-xyz");
|
|
234
|
+
expect(entry?.traceSampled).toBe(true);
|
|
235
|
+
expect(entry?.userId).toBe("user-1");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("exercises all scoped logger methods (debug, error, catch)", () => {
|
|
239
|
+
const lines: string[] = [];
|
|
240
|
+
const captureTransport = new winston.transports.Stream({
|
|
241
|
+
format: winston.format.printf((info) => {
|
|
242
|
+
const msg = typeof info.message === "string" ? info.message : String(info.message);
|
|
243
|
+
return `${info.level}: ${msg}`;
|
|
244
|
+
}),
|
|
245
|
+
stream: new Writable({
|
|
246
|
+
write(chunk, _encoding, callback): void {
|
|
247
|
+
lines.push(chunk.toString().trim());
|
|
248
|
+
callback();
|
|
249
|
+
},
|
|
250
|
+
}),
|
|
251
|
+
});
|
|
252
|
+
winstonLogger.add(captureTransport);
|
|
253
|
+
try {
|
|
254
|
+
const scoped = createScopedLogger({prefix: "[Methods]"});
|
|
255
|
+
scoped.debug("d-msg");
|
|
256
|
+
scoped.error("e-msg");
|
|
257
|
+
scoped.catch(new Error("caught-msg"));
|
|
258
|
+
} finally {
|
|
259
|
+
winstonLogger.remove(captureTransport);
|
|
260
|
+
}
|
|
261
|
+
expect(lines.some((l) => l.includes("[Methods]") && l.includes("d-msg"))).toBe(true);
|
|
262
|
+
expect(lines.some((l) => l.includes("[Methods]") && l.includes("e-msg"))).toBe(true);
|
|
263
|
+
expect(lines.some((l) => l.includes("[Methods]") && l.includes("caught-msg"))).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("scoped logger catch with Sentry enabled and Error instance", () => {
|
|
267
|
+
const OLD_ENV = process.env;
|
|
268
|
+
process.env = {...OLD_ENV, USE_SENTRY_LOGGING: "true"};
|
|
269
|
+
try {
|
|
270
|
+
const scoped = createScopedLogger({prefix: "[Sentry]"});
|
|
271
|
+
expect(() => scoped.catch(new Error("sentry scoped error"))).not.toThrow();
|
|
272
|
+
} finally {
|
|
273
|
+
process.env = OLD_ENV;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("attaches terrenoRequestLog while request ALS scope is active", () => {
|
|
278
|
+
const snapshots: Array<{terrenoRequestLog?: TerrenoRequestLogEntry}> = [];
|
|
279
|
+
const captureTransport = new winston.transports.Stream({
|
|
280
|
+
format: winston.format((info) => {
|
|
281
|
+
snapshots.push({
|
|
282
|
+
terrenoRequestLog: (info as {terrenoRequestLog?: TerrenoRequestLogEntry})
|
|
283
|
+
.terrenoRequestLog,
|
|
284
|
+
});
|
|
285
|
+
return info;
|
|
286
|
+
})(),
|
|
287
|
+
stream: new Writable({
|
|
288
|
+
write(_chunk, _encoding, callback): void {
|
|
289
|
+
callback();
|
|
290
|
+
},
|
|
291
|
+
}),
|
|
292
|
+
});
|
|
293
|
+
winstonLogger.add(captureTransport);
|
|
294
|
+
try {
|
|
295
|
+
runWithRequestContext({requestId: "req-als-1", userId: "user-99"}, () => {
|
|
296
|
+
logger.info("in scope");
|
|
297
|
+
});
|
|
298
|
+
runWithRequestContext({requestId: "req-als-2"}, () => {
|
|
299
|
+
logger.info("anon");
|
|
300
|
+
});
|
|
301
|
+
} finally {
|
|
302
|
+
winstonLogger.remove(captureTransport);
|
|
303
|
+
}
|
|
304
|
+
const withUser = snapshots.find((s) => s.terrenoRequestLog?.requestId === "req-als-1");
|
|
305
|
+
expect(withUser?.terrenoRequestLog?.userId).toBe("user-99");
|
|
306
|
+
const anon = snapshots.find((s) => s.terrenoRequestLog?.requestId === "req-als-2");
|
|
307
|
+
expect(anon?.terrenoRequestLog?.userId).toBeNull();
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe("createFeatureFlaggedLogger", () => {
|
|
312
|
+
afterEach(() => {
|
|
313
|
+
setupLogging({disableFileLogging: true});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("drops info lines while disabled", () => {
|
|
317
|
+
let hits = 0;
|
|
318
|
+
const captureTransport = new winston.transports.Stream({
|
|
319
|
+
format: winston.format.printf(() => {
|
|
320
|
+
hits += 1;
|
|
321
|
+
return "";
|
|
322
|
+
}),
|
|
323
|
+
stream: new Writable({
|
|
324
|
+
write(_chunk, _encoding, callback): void {
|
|
325
|
+
callback();
|
|
326
|
+
},
|
|
327
|
+
}),
|
|
328
|
+
});
|
|
329
|
+
winstonLogger.add(captureTransport);
|
|
330
|
+
try {
|
|
331
|
+
const log = createFeatureFlaggedLogger({isEnabled: () => false, target: logger});
|
|
332
|
+
log.info("hidden");
|
|
333
|
+
} finally {
|
|
334
|
+
winstonLogger.remove(captureTransport);
|
|
335
|
+
}
|
|
336
|
+
expect(hits).toBe(0);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("forwards lines when enabled", () => {
|
|
340
|
+
let hits = 0;
|
|
341
|
+
const captureTransport = new winston.transports.Stream({
|
|
342
|
+
format: winston.format.printf(() => {
|
|
343
|
+
hits += 1;
|
|
344
|
+
return "";
|
|
345
|
+
}),
|
|
346
|
+
stream: new Writable({
|
|
347
|
+
write(_chunk, _encoding, callback): void {
|
|
348
|
+
callback();
|
|
349
|
+
},
|
|
350
|
+
}),
|
|
351
|
+
});
|
|
352
|
+
winstonLogger.add(captureTransport);
|
|
353
|
+
try {
|
|
354
|
+
const log = createFeatureFlaggedLogger({isEnabled: () => true, target: logger});
|
|
355
|
+
log.info("visible");
|
|
356
|
+
} finally {
|
|
357
|
+
winstonLogger.remove(captureTransport);
|
|
358
|
+
}
|
|
359
|
+
expect(hits).toBeGreaterThan(0);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("forwards catch while disabled when gateCatch is false", () => {
|
|
363
|
+
let hits = 0;
|
|
364
|
+
const captureTransport = new winston.transports.Stream({
|
|
365
|
+
format: winston.format.printf(() => {
|
|
366
|
+
hits += 1;
|
|
367
|
+
return "";
|
|
368
|
+
}),
|
|
369
|
+
stream: new Writable({
|
|
370
|
+
write(_chunk, _encoding, callback): void {
|
|
371
|
+
callback();
|
|
372
|
+
},
|
|
373
|
+
}),
|
|
374
|
+
});
|
|
375
|
+
winstonLogger.add(captureTransport);
|
|
376
|
+
try {
|
|
377
|
+
const log = createFeatureFlaggedLogger({
|
|
378
|
+
gateCatch: false,
|
|
379
|
+
isEnabled: () => false,
|
|
380
|
+
target: logger,
|
|
381
|
+
});
|
|
382
|
+
log.catch(new Error("still-logged"));
|
|
383
|
+
} finally {
|
|
384
|
+
winstonLogger.remove(captureTransport);
|
|
385
|
+
}
|
|
386
|
+
expect(hits).toBeGreaterThan(0);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("forwards all methods when enabled (debug, warn, error)", () => {
|
|
390
|
+
const levels: string[] = [];
|
|
391
|
+
const captureTransport = new winston.transports.Stream({
|
|
392
|
+
format: winston.format((info) => {
|
|
393
|
+
levels.push(info.level);
|
|
394
|
+
return info;
|
|
395
|
+
})(),
|
|
396
|
+
stream: new Writable({
|
|
397
|
+
write(_chunk, _encoding, callback): void {
|
|
398
|
+
callback();
|
|
399
|
+
},
|
|
400
|
+
}),
|
|
401
|
+
});
|
|
402
|
+
winstonLogger.add(captureTransport);
|
|
403
|
+
try {
|
|
404
|
+
const log = createFeatureFlaggedLogger({isEnabled: () => true, target: logger});
|
|
405
|
+
log.debug("d");
|
|
406
|
+
log.warn("w");
|
|
407
|
+
log.error("e");
|
|
408
|
+
} finally {
|
|
409
|
+
winstonLogger.remove(captureTransport);
|
|
410
|
+
}
|
|
411
|
+
expect(levels.some((l) => l === "debug")).toBe(true);
|
|
412
|
+
expect(levels.some((l) => l === "warn")).toBe(true);
|
|
413
|
+
expect(levels.some((l) => l === "error")).toBe(true);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("drops debug, warn, error lines when disabled", () => {
|
|
417
|
+
let hits = 0;
|
|
418
|
+
const captureTransport = new winston.transports.Stream({
|
|
419
|
+
format: winston.format.printf(() => {
|
|
420
|
+
hits += 1;
|
|
421
|
+
return "";
|
|
422
|
+
}),
|
|
423
|
+
stream: new Writable({
|
|
424
|
+
write(_chunk, _encoding, callback): void {
|
|
425
|
+
callback();
|
|
426
|
+
},
|
|
427
|
+
}),
|
|
428
|
+
});
|
|
429
|
+
winstonLogger.add(captureTransport);
|
|
430
|
+
try {
|
|
431
|
+
const log = createFeatureFlaggedLogger({isEnabled: () => false, target: logger});
|
|
432
|
+
log.debug("hidden-d");
|
|
433
|
+
log.warn("hidden-w");
|
|
434
|
+
log.error("hidden-e");
|
|
435
|
+
} finally {
|
|
436
|
+
winstonLogger.remove(captureTransport);
|
|
437
|
+
}
|
|
438
|
+
expect(hits).toBe(0);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("uses the global logger as default target when target is not provided", () => {
|
|
442
|
+
const log = createFeatureFlaggedLogger({isEnabled: () => true});
|
|
443
|
+
expect(() => log.info("default target")).not.toThrow();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("drops catch while disabled when gateCatch is true", () => {
|
|
447
|
+
let hits = 0;
|
|
448
|
+
const captureTransport = new winston.transports.Stream({
|
|
449
|
+
format: winston.format.printf(() => {
|
|
450
|
+
hits += 1;
|
|
451
|
+
return "";
|
|
452
|
+
}),
|
|
453
|
+
stream: new Writable({
|
|
454
|
+
write(_chunk, _encoding, callback): void {
|
|
455
|
+
callback();
|
|
456
|
+
},
|
|
457
|
+
}),
|
|
458
|
+
});
|
|
459
|
+
winstonLogger.add(captureTransport);
|
|
460
|
+
try {
|
|
461
|
+
const log = createFeatureFlaggedLogger({
|
|
462
|
+
gateCatch: true,
|
|
463
|
+
isEnabled: () => false,
|
|
464
|
+
target: logger,
|
|
465
|
+
});
|
|
466
|
+
log.catch(new Error("suppressed"));
|
|
467
|
+
} finally {
|
|
468
|
+
winstonLogger.remove(captureTransport);
|
|
469
|
+
}
|
|
470
|
+
expect(hits).toBe(0);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
50
474
|
describe("setupLogging", () => {
|
|
51
475
|
let tempDir: string;
|
|
52
476
|
|
|
@@ -146,4 +570,45 @@ describe("setupLogging", () => {
|
|
|
146
570
|
});
|
|
147
571
|
expect(true).toBe(true);
|
|
148
572
|
});
|
|
573
|
+
|
|
574
|
+
it("console format includes timestamps when showConsoleTimestamps is true", () => {
|
|
575
|
+
const lines: string[] = [];
|
|
576
|
+
setupLogging({
|
|
577
|
+
disableFileLogging: true,
|
|
578
|
+
showConsoleTimestamps: true,
|
|
579
|
+
});
|
|
580
|
+
const captureTransport = new winston.transports.Stream({
|
|
581
|
+
format: winston.format.combine(
|
|
582
|
+
winston.format.timestamp(),
|
|
583
|
+
winston.format.printf((info) => {
|
|
584
|
+
if (info.timestamp) {
|
|
585
|
+
return `${info.timestamp} - ${info.level}: ${info.message}`;
|
|
586
|
+
}
|
|
587
|
+
return `${info.level}: ${info.message}`;
|
|
588
|
+
})
|
|
589
|
+
),
|
|
590
|
+
stream: new Writable({
|
|
591
|
+
write(chunk, _encoding, callback): void {
|
|
592
|
+
lines.push(chunk.toString().trim());
|
|
593
|
+
callback();
|
|
594
|
+
},
|
|
595
|
+
}),
|
|
596
|
+
});
|
|
597
|
+
winstonLogger.add(captureTransport);
|
|
598
|
+
try {
|
|
599
|
+
logger.info("timestamp-test");
|
|
600
|
+
} finally {
|
|
601
|
+
winstonLogger.remove(captureTransport);
|
|
602
|
+
}
|
|
603
|
+
expect(lines.some((l) => l.includes("timestamp-test"))).toBe(true);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it("disableTerrenoDevJsonlLog skips the dev JSONL transport", () => {
|
|
607
|
+
expect(() =>
|
|
608
|
+
setupLogging({
|
|
609
|
+
disableFileLogging: true,
|
|
610
|
+
disableTerrenoDevJsonlLog: true,
|
|
611
|
+
})
|
|
612
|
+
).not.toThrow();
|
|
613
|
+
});
|
|
149
614
|
});
|