@devopsplaybook.io/common-utils 1.10.1-beta.23.b1906d7 → 1.11.0-beta.24.d8f9aef

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.
Files changed (69) hide show
  1. package/README.md +73 -27
  2. package/dist/src/ConfigBase.d.ts +31 -0
  3. package/dist/src/ConfigBase.js +58 -16
  4. package/dist/src/DbUtils.d.ts +20 -3
  5. package/dist/src/DbUtils.js +93 -2
  6. package/dist/src/DbUtilsNoTelemetry.d.ts +4 -1
  7. package/dist/src/DbUtilsNoTelemetry.js +62 -6
  8. package/dist/src/PostgresDbUtils.d.ts +41 -10
  9. package/dist/src/PostgresDbUtils.js +357 -304
  10. package/dist/src/SqlDbUtils.d.ts +12 -4
  11. package/dist/src/SqlDbUtils.js +76 -30
  12. package/dist/src/users/Auth.d.ts +11 -1
  13. package/dist/src/users/Auth.js +160 -44
  14. package/dist/src/users/User.d.ts +10 -0
  15. package/dist/src/users/User.js +30 -10
  16. package/dist/src/users/UserApiToken.d.ts +4 -0
  17. package/dist/src/users/UserApiToken.js +11 -0
  18. package/dist/src/users/UsersApiTokensData.d.ts +12 -0
  19. package/dist/src/users/UsersApiTokensData.js +130 -33
  20. package/dist/src/users/UsersData.d.ts +20 -1
  21. package/dist/src/users/UsersData.js +150 -52
  22. package/dist/src/users/UsersRoutes.js +178 -61
  23. package/dist/src/users/index.d.ts +8 -0
  24. package/dist/src/users/index.js +24 -0
  25. package/package.json +58 -1
  26. package/.github/workflows/main-build.yml +0 -18
  27. package/.github/workflows/pr-check.yml +0 -27
  28. package/.github/workflows/reusable-merge-build.yml +0 -197
  29. package/.github/workflows/reusable-npm-merge.yml +0 -135
  30. package/.github/workflows/reusable-npm-pr.yml +0 -183
  31. package/.github/workflows/reusable-npm-upgrade.yml +0 -92
  32. package/.github/workflows/reusable-pr-verify.yml +0 -181
  33. package/AGENTS.md +0 -105
  34. package/index.ts +0 -18
  35. package/jest.config.js +0 -17
  36. package/prettierrc.json +0 -5
  37. package/src/ConfigBase.spec.ts +0 -108
  38. package/src/ConfigBase.ts +0 -297
  39. package/src/DbUtils.spec.ts +0 -23
  40. package/src/DbUtils.ts +0 -116
  41. package/src/DbUtilsNoTelemetry.spec.ts +0 -168
  42. package/src/DbUtilsNoTelemetry.ts +0 -117
  43. package/src/LLM.spec.ts +0 -303
  44. package/src/LLM.ts +0 -204
  45. package/src/Notifications.spec.ts +0 -265
  46. package/src/Notifications.ts +0 -201
  47. package/src/OTelContext.spec.ts +0 -58
  48. package/src/OTelContext.ts +0 -63
  49. package/src/PostgresDbUtils.spec.ts +0 -153
  50. package/src/PostgresDbUtils.ts +0 -666
  51. package/src/SqlDbUtils.spec.ts +0 -108
  52. package/src/SqlDbUtils.ts +0 -152
  53. package/src/SystemCommand.spec.ts +0 -18
  54. package/src/SystemCommand.ts +0 -23
  55. package/src/Timeout.spec.ts +0 -18
  56. package/src/Timeout.ts +0 -12
  57. package/src/users/Auth.spec.ts +0 -268
  58. package/src/users/Auth.ts +0 -202
  59. package/src/users/User.ts +0 -75
  60. package/src/users/UserApiToken.ts +0 -55
  61. package/src/users/UserPassword.spec.ts +0 -28
  62. package/src/users/UserPassword.ts +0 -20
  63. package/src/users/UserSession.ts +0 -9
  64. package/src/users/UsersApiTokensData.spec.ts +0 -158
  65. package/src/users/UsersApiTokensData.ts +0 -125
  66. package/src/users/UsersData.ts +0 -141
  67. package/src/users/UsersRoutes.ts +0 -374
  68. package/tsconfig.json +0 -15
  69. package/tsconfig.spec.json +0 -8
@@ -1,108 +0,0 @@
1
-
2
- // Mock dependencies before imports
3
- jest.mock("better-sqlite3", () => {
4
- return jest.fn().mockImplementation(() => ({
5
- prepare: jest.fn().mockReturnValue({
6
- run: jest.fn().mockReturnValue({ changes: 0 }),
7
- all: jest.fn().mockReturnValue([]),
8
- }),
9
- exec: jest.fn(),
10
- }));
11
- });
12
-
13
- jest.mock("fs-extra", () => ({
14
- ensureDir: jest.fn().mockResolvedValue(undefined),
15
- readdir: jest.fn().mockResolvedValue(["init-0000.sql"]),
16
- readFileSync: jest
17
- .fn()
18
- .mockReturnValue(
19
- "CREATE TABLE IF NOT EXISTS metadata (type TEXT, value TEXT, dateCreated TEXT);",
20
- ),
21
- }));
22
-
23
- jest.mock("@devopsplaybook.io/otel-utils", () => ({
24
- StandardTracer: jest.fn(),
25
- StandardLogger: jest.fn(),
26
- ModuleLogger: jest.fn(),
27
- }));
28
-
29
- jest.mock("@opentelemetry/sdk-trace-base", () => ({
30
- Span: jest.fn(),
31
- }));
32
-
33
- jest.mock("@opentelemetry/api", () => ({
34
- SpanStatusCode: { ERROR: 2, OK: 1 },
35
- }));
36
-
37
- import "fs-extra";
38
-
39
- describe("SqlDbUtils", () => {
40
- let SqlDbUtils: typeof import("./SqlDbUtils");
41
-
42
- const mockSpan = {
43
- end: jest.fn(),
44
- addEvent: jest.fn(),
45
- setStatus: jest.fn(),
46
- };
47
-
48
- const mockModuleLogger = {
49
- info: jest.fn(),
50
- error: jest.fn(),
51
- warn: jest.fn(),
52
- };
53
-
54
- const mockTracer = {
55
- startSpan: jest.fn().mockReturnValue(mockSpan),
56
- };
57
-
58
- const mockLogger = {
59
- createModuleLogger: jest.fn().mockReturnValue(mockModuleLogger),
60
- };
61
-
62
- beforeEach(() => {
63
- jest.clearAllMocks();
64
- // Re-import to reset module state
65
- jest.isolateModules(() => {
66
- SqlDbUtils = require("./SqlDbUtils");
67
- });
68
- SqlDbUtils.SqlDbUtilsSetOTel(mockTracer as any, mockLogger as any);
69
- });
70
-
71
- it("SqlDbUtilsSetOTel should accept tracer and logger", () => {
72
- expect(() =>
73
- SqlDbUtils.SqlDbUtilsSetOTel(mockTracer as any, mockLogger as any),
74
- ).not.toThrow();
75
- });
76
-
77
- it("SqlDbUtilsExecSQL should call prepare and run", () => {
78
- const Database = require("better-sqlite3");
79
- const db = new Database("/tmp/test.db");
80
- const mockRun = jest.fn().mockReturnValue({ changes: 5 });
81
- db.prepare.mockReturnValue({ run: mockRun, all: jest.fn() });
82
-
83
- // We need to set the database variable in the module.
84
- // Since we can't directly set it, we test the exported function behavior
85
- // by verifying the span was started.
86
- expect(mockTracer.startSpan).not.toHaveBeenCalled();
87
- });
88
-
89
- it("SqlDbUtilsExecSQLFile should create a span", () => {
90
- // The function reads a file and executes it
91
- // We test that it creates a span with the right name
92
- const span = { end: jest.fn(), addEvent: jest.fn(), setStatus: jest.fn() };
93
- mockTracer.startSpan.mockReturnValue(span);
94
- // Since database is not initialized, this will throw - that's expected
95
- // We're just testing the span creation pattern
96
- expect(mockTracer.startSpan).toBeDefined();
97
- });
98
-
99
- it("SqlDbUtilsGetDatabase should return the database", () => {
100
- // Before init, database is undefined
101
- expect(SqlDbUtils.SqlDbUtilsGetDatabase()).toBeUndefined();
102
- });
103
-
104
- it("convertToPostgresPlaceholders is not exported from SqlDbUtils", () => {
105
- // It should be in DbUtils, not SqlDbUtils
106
- expect((SqlDbUtils as any).convertToPostgresPlaceholders).toBeUndefined();
107
- });
108
- });
package/src/SqlDbUtils.ts DELETED
@@ -1,152 +0,0 @@
1
- import Database from "better-sqlite3";
2
- import * as fs from "fs-extra";
3
- import { Span } from "@opentelemetry/sdk-trace-base";
4
- import { SpanStatusCode } from "@opentelemetry/api";
5
- import {
6
- StandardTracer,
7
- StandardLogger,
8
- ModuleLogger,
9
- } from "@devopsplaybook.io/otel-utils";
10
-
11
- /**
12
- * Configuration subset required by the SQLite module.
13
- */
14
- export interface SqlDbConfig {
15
- DATA_DIR: string;
16
- }
17
-
18
- let database: Database.Database;
19
- let tracer: StandardTracer;
20
- let logger: ModuleLogger;
21
-
22
- /**
23
- * Injects the OTel tracer and logger instances used by all SQL operations.
24
- * Must be called once at startup, before {@link SqlDbUtilsInit}.
25
- */
26
- export function SqlDbUtilsSetOTel(
27
- tracerIn: StandardTracer,
28
- loggerIn: StandardLogger,
29
- ): void {
30
- tracer = tracerIn;
31
- logger = loggerIn.createModuleLogger("SqlDbUtils");
32
- }
33
-
34
- /**
35
- * Opens the SQLite database and applies pending migration files from `sqlDir`.
36
- *
37
- * Migration files must follow the naming convention `init-NNNN.sql` and are
38
- * applied in lexicographic order. A `metadata` table tracks which migrations
39
- * have already been applied so they are idempotent.
40
- *
41
- * @param context Parent OTel span.
42
- * @param config Configuration with `DATA_DIR`.
43
- * @param sqlDir Absolute path to the directory containing SQL migration files.
44
- */
45
- export async function SqlDbUtilsInit(
46
- context: Span,
47
- config: SqlDbConfig,
48
- sqlDir: string,
49
- ): Promise<void> {
50
- const span = tracer.startSpan("SqlDbUtilsInit", context);
51
- await fs.ensureDir(config.DATA_DIR);
52
- database = new Database(`${config.DATA_DIR}/database.db`);
53
- SqlDbUtilsExecSQLFile(span, `${sqlDir}/init-0000.sql`);
54
- const initFiles = (await fs.readdir(sqlDir)).sort();
55
- let dbVersionApplied = 0;
56
- const rows = SqlDbUtilsQuerySQL(
57
- span,
58
- "SELECT MAX(value) as maxVersion FROM metadata WHERE type='db_version'",
59
- );
60
- if (rows.length > 0 && rows[0].maxVersion) {
61
- dbVersionApplied = Number(rows[0].maxVersion);
62
- }
63
- logger.info(`Current DB Version: ${dbVersionApplied}`, span);
64
- for (const initFile of initFiles) {
65
- const regex = /init-(\d+).sql/g;
66
- const match = regex.exec(initFile);
67
- if (match) {
68
- const dbVersionInitFile = Number(match[1]);
69
- if (dbVersionInitFile > dbVersionApplied) {
70
- logger.info(`Loading init file: ${initFile}`, span);
71
- SqlDbUtilsExecSQLFile(span, `${sqlDir}/${initFile}`);
72
- SqlDbUtilsExecSQL(
73
- span,
74
- "INSERT INTO metadata (type, value, dateCreated) VALUES ('db_version',?,?)",
75
- [dbVersionInitFile, new Date().toISOString()],
76
- );
77
- }
78
- }
79
- }
80
- span.end();
81
- }
82
-
83
- /** Returns the underlying `better-sqlite3` Database instance. */
84
- export function SqlDbUtilsGetDatabase(): Database.Database {
85
- return database;
86
- }
87
-
88
- /**
89
- * Execute a write SQL statement with OTel tracing.
90
- * @returns Number of rows changed.
91
- */
92
- export function SqlDbUtilsExecSQL(
93
- context: Span,
94
- sql: string,
95
- params: unknown[] = [],
96
- ): number {
97
- const span = tracer.startSpan("SqlDbUtilsExecSQL", context);
98
- try {
99
- const stmt = database.prepare(sql);
100
- const result = stmt.run(params);
101
- span.addEvent(`Impacted Rows: ${result.changes}`);
102
- span.end();
103
- return result.changes;
104
- } catch (error) {
105
- const err = error as Error;
106
- span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
107
- span.end();
108
- throw error;
109
- }
110
- }
111
-
112
- /** Execute an entire SQL file (used for migrations). */
113
- export function SqlDbUtilsExecSQLFile(context: Span, filename: string): void {
114
- const span = tracer.startSpan("SqlDbUtilsExecSQLFile", context);
115
- try {
116
- const sql = fs.readFileSync(filename).toString();
117
- database.exec(sql);
118
- span.end();
119
- } catch (error) {
120
- const err = error as Error;
121
- span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
122
- span.end();
123
- throw error;
124
- }
125
- }
126
-
127
- /**
128
- * Execute a read SQL query with OTel tracing.
129
- * @returns Array of row objects.
130
- */
131
- export function SqlDbUtilsQuerySQL(
132
- context: Span,
133
- sql: string,
134
- params: unknown[] = [],
135
- debug = false,
136
- ): any[] {
137
- const span = tracer.startSpan("SqlDbUtilsQuerySQL", context);
138
- if (debug) {
139
- console.log(sql);
140
- }
141
- try {
142
- const stmt = database.prepare(sql);
143
- const rows = stmt.all(params);
144
- span.end();
145
- return rows;
146
- } catch (error) {
147
- const err = error as Error;
148
- span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
149
- span.end();
150
- throw error;
151
- }
152
- }
@@ -1,18 +0,0 @@
1
- import { SystemCommandExecute } from "./SystemCommand";
2
-
3
- describe("SystemCommandExecute", () => {
4
- it("should resolve with stdout on success", async () => {
5
- const result = await SystemCommandExecute("echo hello");
6
- expect(result.trim()).toBe("hello");
7
- });
8
-
9
- it("should reject on command failure", async () => {
10
- await expect(SystemCommandExecute("exit 1")).rejects.toThrow();
11
- });
12
-
13
- it("should reject on non-existent command", async () => {
14
- await expect(
15
- SystemCommandExecute("nonexistent_command_xyz_123"),
16
- ).rejects.toThrow();
17
- });
18
- });
@@ -1,23 +0,0 @@
1
- import * as childProcess from "child_process";
2
-
3
- /**
4
- * Execute a shell command and return its stdout.
5
- *
6
- * @param command The command string to execute.
7
- * @param options Optional `child_process.exec` options.
8
- * @returns Resolves with stdout on success, rejects on error.
9
- */
10
- export function SystemCommandExecute(
11
- command: string,
12
- options?: childProcess.ExecOptions,
13
- ): Promise<string> {
14
- return new Promise<string>((resolve, reject) => {
15
- childProcess.exec(command, options || {}, (error, stdout) => {
16
- if (error) {
17
- reject(error);
18
- } else {
19
- resolve(String(stdout));
20
- }
21
- });
22
- });
23
- }
@@ -1,18 +0,0 @@
1
- import { TimeoutWait } from "./Timeout";
2
-
3
- describe("TimeoutWait", () => {
4
- it("should resolve after the specified duration", async () => {
5
- const start = Date.now();
6
- const delayMs = 50;
7
- await TimeoutWait(delayMs);
8
- const elapsed = Date.now() - start;
9
- expect(elapsed).toBeGreaterThanOrEqual(delayMs - 10);
10
- });
11
-
12
- it("should resolve immediately for duration 0", async () => {
13
- const start = Date.now();
14
- await TimeoutWait(0);
15
- const elapsed = Date.now() - start;
16
- expect(elapsed).toBeLessThan(50);
17
- });
18
- });
package/src/Timeout.ts DELETED
@@ -1,12 +0,0 @@
1
- /**
2
- * Promise-based wrapper around `setTimeout`.
3
- *
4
- * @param duration Delay in milliseconds.
5
- */
6
- export function TimeoutWait(duration: number): Promise<void> {
7
- return new Promise((resolve) => {
8
- setTimeout(() => {
9
- resolve();
10
- }, duration);
11
- });
12
- }
@@ -1,268 +0,0 @@
1
- jest.mock("uuid", () => ({
2
- v4: () => "mock-uuid-1234",
3
- }));
4
-
5
- jest.mock("../DbUtils", () => ({
6
- DbUtilsQuerySQL: jest.fn(),
7
- DbUtilsExecSQL: jest.fn(),
8
- }));
9
-
10
- jest.mock("./UsersApiTokensData", () => ({
11
- UsersApiTokensDataGetByTokenHash: jest.fn(),
12
- }));
13
-
14
- jest.mock("./UsersData", () => ({
15
- UsersDataGet: jest.fn(),
16
- }));
17
-
18
- import { StandardTracer } from "@devopsplaybook.io/otel-utils";
19
- import { DbUtilsExecSQL, DbUtilsQuerySQL } from "../DbUtils";
20
- import {
21
- AuthGenerateJWT,
22
- AuthGetUserSession,
23
- AuthHasScope,
24
- AuthInit,
25
- AuthMustBeAdmin,
26
- AuthMustBeAuthenticated,
27
- AuthSetOTel,
28
- } from "./Auth";
29
- import { User } from "./User";
30
- import { UsersApiTokensDataGetByTokenHash } from "./UsersApiTokensData";
31
- import { UsersDataGet } from "./UsersData";
32
-
33
- const mockedGetByHash = UsersApiTokensDataGetByTokenHash as jest.Mock;
34
- const mockedUsersDataGet = UsersDataGet as jest.Mock;
35
- const mockedQuery = DbUtilsQuerySQL as jest.Mock;
36
- const mockedExec = DbUtilsExecSQL as jest.Mock;
37
-
38
- const mockTracer = {
39
- startSpan: () => ({ end: () => undefined }),
40
- } as unknown as StandardTracer;
41
-
42
- function mockRes() {
43
- const res = {
44
- status: jest.fn().mockReturnThis(),
45
- send: jest.fn(),
46
- };
47
- return res;
48
- }
49
-
50
- function mockReq(authorization?: string) {
51
- return {
52
- headers: authorization ? { authorization } : {},
53
- };
54
- }
55
-
56
- async function expectAccessDenied(guard: Promise<void>, res: ReturnType<typeof mockRes>) {
57
- await expect(guard).rejects.toThrow("Access Denied");
58
- expect(res.status).toHaveBeenCalledWith(403);
59
- expect(res.send).toHaveBeenCalledWith({ error: "Access Denied" });
60
- }
61
-
62
- beforeAll(async () => {
63
- AuthSetOTel(mockTracer);
64
- const config = {
65
- JWT_KEY: "",
66
- JWT_VALIDITY_DURATION: 3600,
67
- DATABASE_TYPE: "sqlite" as const,
68
- };
69
- mockedQuery.mockResolvedValue([]);
70
- await AuthInit(null as never, config, ["traces", "metrics", "logs"]);
71
- });
72
-
73
- beforeEach(() => {
74
- mockedGetByHash.mockReset();
75
- mockedUsersDataGet.mockReset();
76
- });
77
-
78
- describe("JWT authentication", () => {
79
- it("should authenticate a valid JWT", async () => {
80
- const user = new User();
81
- user.name = "jwt-user";
82
- user.role = "user";
83
- user.scopes = ["traces"];
84
- const jwt = await AuthGenerateJWT(user);
85
-
86
- const res = mockRes();
87
- await AuthMustBeAuthenticated(mockReq(`Bearer ${jwt}`), res);
88
-
89
- expect(res.status).not.toHaveBeenCalled();
90
- });
91
-
92
- it("should reject a request without authorization header", async () => {
93
- const res = mockRes();
94
- await expectAccessDenied(
95
- AuthMustBeAuthenticated(mockReq(), res),
96
- res,
97
- );
98
- });
99
-
100
- it("should reject an invalid JWT", async () => {
101
- const res = mockRes();
102
- await expectAccessDenied(
103
- AuthMustBeAuthenticated(mockReq("Bearer not-a-jwt"), res),
104
- res,
105
- );
106
- expect(mockedGetByHash).toHaveBeenCalledTimes(1);
107
- });
108
- });
109
-
110
- describe("API token authentication", () => {
111
- it("should authenticate a valid API token and resolve the owning user session", async () => {
112
- const user = new User();
113
- user.id = "user-1";
114
- user.name = "token-user";
115
- user.role = "user";
116
- user.scopes = ["traces"];
117
- const apiToken = new User();
118
- apiToken.id = "token-1";
119
- mockedGetByHash.mockResolvedValue(apiToken);
120
- mockedUsersDataGet.mockResolvedValue(user);
121
-
122
- const res = mockRes();
123
- await AuthMustBeAuthenticated(mockReq("Bearer plain-api-token"), res);
124
- expect(res.status).not.toHaveBeenCalled();
125
-
126
- const session = await AuthGetUserSession(mockReq("Bearer plain-api-token"));
127
- expect(session.isAuthenticated).toBe(true);
128
- expect(session.userId).toBe("user-1");
129
- expect(session.userName).toBe("token-user");
130
- expect(session.role).toBe("user");
131
- expect(session.scopes).toEqual(["traces"]);
132
- });
133
-
134
- it("should pass AuthMustBeAdmin for an API token owned by an admin", async () => {
135
- const admin = new User();
136
- admin.id = "admin-1";
137
- admin.name = "token-admin";
138
- admin.role = "admin";
139
- mockedGetByHash.mockResolvedValue(new User());
140
- mockedUsersDataGet.mockResolvedValue(admin);
141
-
142
- const res = mockRes();
143
- await AuthMustBeAdmin(mockReq("Bearer admin-api-token"), res);
144
- expect(res.status).not.toHaveBeenCalled();
145
-
146
- // Admin tokens get the full scope set, mirroring JWT semantics
147
- const session = await AuthGetUserSession(mockReq("Bearer admin-api-token"));
148
- expect(session.scopes).toEqual(["traces", "metrics", "logs"]);
149
- });
150
-
151
- it("should fail AuthMustBeAdmin for an API token owned by a non-admin", async () => {
152
- const user = new User();
153
- user.id = "user-1";
154
- user.name = "token-user";
155
- user.role = "user";
156
- user.scopes = ["traces"];
157
- mockedGetByHash.mockResolvedValue(new User());
158
- mockedUsersDataGet.mockResolvedValue(user);
159
-
160
- const res = mockRes();
161
- await expectAccessDenied(
162
- AuthMustBeAdmin(mockReq("Bearer user-api-token"), res),
163
- res,
164
- );
165
- });
166
-
167
- it("should pass AuthHasScope when the owning user has the scope", async () => {
168
- const user = new User();
169
- user.id = "user-1";
170
- user.name = "token-user";
171
- user.role = "user";
172
- user.scopes = ["traces"];
173
- mockedGetByHash.mockResolvedValue(new User());
174
- mockedUsersDataGet.mockResolvedValue(user);
175
-
176
- const res = mockRes();
177
- await AuthHasScope(mockReq("Bearer scoped-api-token"), res, "traces");
178
- expect(res.status).not.toHaveBeenCalled();
179
- });
180
-
181
- it("should fail AuthHasScope when the owning user lacks the scope", async () => {
182
- const user = new User();
183
- user.id = "user-1";
184
- user.name = "token-user";
185
- user.role = "user";
186
- user.scopes = ["traces"];
187
- mockedGetByHash.mockResolvedValue(new User());
188
- mockedUsersDataGet.mockResolvedValue(user);
189
-
190
- const res = mockRes();
191
- await expectAccessDenied(
192
- AuthHasScope(mockReq("Bearer scoped-api-token"), res, "metrics"),
193
- res,
194
- );
195
- });
196
-
197
- it("should reject an unknown or revoked API token", async () => {
198
- mockedGetByHash.mockResolvedValue(null);
199
-
200
- const res = mockRes();
201
- await expectAccessDenied(
202
- AuthMustBeAuthenticated(mockReq("Bearer revoked-token"), res),
203
- res,
204
- );
205
- expect(mockedUsersDataGet).not.toHaveBeenCalled();
206
- });
207
-
208
- it("should reject an API token whose user no longer exists", async () => {
209
- mockedGetByHash.mockResolvedValue(new User());
210
- mockedUsersDataGet.mockResolvedValue(null);
211
-
212
- const res = mockRes();
213
- await expectAccessDenied(
214
- AuthMustBeAuthenticated(mockReq("Bearer orphan-token"), res),
215
- res,
216
- );
217
- });
218
-
219
- it("should resolve the token only once per request (payload caching)", async () => {
220
- const user = new User();
221
- user.id = "user-1";
222
- user.name = "token-user";
223
- user.role = "user";
224
- user.scopes = ["traces"];
225
- mockedGetByHash.mockResolvedValue(new User());
226
- mockedUsersDataGet.mockResolvedValue(user);
227
-
228
- const req = mockReq("Bearer cached-token");
229
- await AuthMustBeAuthenticated(req, mockRes());
230
- await AuthHasScope(req, mockRes(), "traces");
231
- await AuthGetUserSession(req);
232
-
233
- expect(mockedGetByHash).toHaveBeenCalledTimes(1);
234
- expect(mockedUsersDataGet).toHaveBeenCalledTimes(1);
235
- });
236
- });
237
-
238
- describe("AuthInit", () => {
239
- it("should load the existing JWT key from metadata", async () => {
240
- mockedExec.mockReset();
241
- mockedQuery.mockReset();
242
- const config = {
243
- JWT_KEY: "",
244
- JWT_VALIDITY_DURATION: 3600,
245
- DATABASE_TYPE: "sqlite" as const,
246
- };
247
- mockedQuery.mockResolvedValue([{ value: "stored-key" }]);
248
- await AuthInit(null as never, config, ["traces"]);
249
-
250
- expect(config.JWT_KEY).toBe("stored-key");
251
- expect(mockedExec).not.toHaveBeenCalled();
252
- });
253
-
254
- it("should generate and persist a JWT key when none is stored", async () => {
255
- mockedExec.mockReset();
256
- mockedQuery.mockReset();
257
- const config = {
258
- JWT_KEY: "",
259
- JWT_VALIDITY_DURATION: 3600,
260
- DATABASE_TYPE: "sqlite" as const,
261
- };
262
- mockedQuery.mockResolvedValue([]);
263
- await AuthInit(null as never, config, ["traces"]);
264
-
265
- expect(config.JWT_KEY).toBe("mock-uuid-1234");
266
- expect(mockedExec).toHaveBeenCalledTimes(1);
267
- });
268
- });