@techstream/quark-core 1.1.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/.turbo/turbo-lint.log +7 -0
- package/.turbo/turbo-test.log +1376 -0
- package/README.md +419 -0
- package/package.json +29 -0
- package/src/auth/index.js +127 -0
- package/src/auth/password.js +9 -0
- package/src/auth.test.js +90 -0
- package/src/authorization.js +235 -0
- package/src/authorization.test.js +314 -0
- package/src/cache.js +137 -0
- package/src/cache.test.js +217 -0
- package/src/csrf.js +118 -0
- package/src/csrf.test.js +157 -0
- package/src/email.js +140 -0
- package/src/email.test.js +259 -0
- package/src/error-reporter.js +266 -0
- package/src/error-reporter.test.js +236 -0
- package/src/errors.js +192 -0
- package/src/errors.test.js +128 -0
- package/src/index.js +32 -0
- package/src/logger.js +182 -0
- package/src/logger.test.js +287 -0
- package/src/mailhog.js +43 -0
- package/src/queue/index.js +214 -0
- package/src/rate-limiter.js +253 -0
- package/src/rate-limiter.test.js +130 -0
- package/src/redis.js +96 -0
- package/src/testing/factories.js +93 -0
- package/src/testing/helpers.js +266 -0
- package/src/testing/index.js +46 -0
- package/src/testing/mocks.js +480 -0
- package/src/testing/testing.test.js +543 -0
- package/src/types.js +74 -0
- package/src/utils.js +219 -0
- package/src/utils.test.js +193 -0
- package/src/validation.js +26 -0
- package/test-imports.mjs +21 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { beforeEach, describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
createSentryAdapter,
|
|
5
|
+
ErrorReporter,
|
|
6
|
+
errorReporter,
|
|
7
|
+
} from "./error-reporter.js";
|
|
8
|
+
|
|
9
|
+
describe("ErrorReporter", () => {
|
|
10
|
+
/** @type {ErrorReporter} */
|
|
11
|
+
let reporter;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
reporter = new ErrorReporter();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("default instance", () => {
|
|
18
|
+
it("has console adapter registered by default", () => {
|
|
19
|
+
assert.equal(reporter.adapters.length, 1);
|
|
20
|
+
assert.equal(reporter.adapters[0].name, "console");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("global singleton has console adapter", () => {
|
|
24
|
+
const names = errorReporter.adapters.map((a) => a.name);
|
|
25
|
+
assert.ok(names.includes("console"));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("use()", () => {
|
|
30
|
+
it("adds an adapter", () => {
|
|
31
|
+
const adapter = { name: "test", report: () => {} };
|
|
32
|
+
reporter.use(adapter);
|
|
33
|
+
assert.equal(reporter.adapters.length, 2);
|
|
34
|
+
assert.equal(reporter.adapters[1].name, "test");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("throws if adapter has no name", () => {
|
|
38
|
+
assert.throws(() => reporter.use({ report: () => {} }), /name/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("throws if adapter has no report function", () => {
|
|
42
|
+
assert.throws(() => reporter.use({ name: "bad" }), /report/);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("report()", () => {
|
|
47
|
+
it("calls all registered adapters", () => {
|
|
48
|
+
const calls = [];
|
|
49
|
+
const adapter1 = {
|
|
50
|
+
name: "a1",
|
|
51
|
+
report: (err, ctx) => calls.push({ adapter: "a1", err, ctx }),
|
|
52
|
+
};
|
|
53
|
+
const adapter2 = {
|
|
54
|
+
name: "a2",
|
|
55
|
+
report: (err, ctx) => calls.push({ adapter: "a2", err, ctx }),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
reporter.use(adapter1);
|
|
59
|
+
reporter.use(adapter2);
|
|
60
|
+
|
|
61
|
+
const error = new Error("test error");
|
|
62
|
+
reporter.report(error, { requestId: "123" });
|
|
63
|
+
|
|
64
|
+
// console adapter + 2 custom = 3 calls
|
|
65
|
+
assert.equal(calls.length, 2);
|
|
66
|
+
assert.equal(calls[0].adapter, "a1");
|
|
67
|
+
assert.equal(calls[1].adapter, "a2");
|
|
68
|
+
assert.equal(calls[0].err, error);
|
|
69
|
+
assert.equal(calls[0].ctx.requestId, "123");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("swallows adapter errors without throwing", () => {
|
|
73
|
+
const failAdapter = {
|
|
74
|
+
name: "fail",
|
|
75
|
+
report: () => {
|
|
76
|
+
throw new Error("adapter crash");
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
reporter.use(failAdapter);
|
|
80
|
+
|
|
81
|
+
assert.doesNotThrow(() => reporter.report(new Error("test")));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("captureMessage()", () => {
|
|
86
|
+
it("calls adapters that support captureMessage", () => {
|
|
87
|
+
const messages = [];
|
|
88
|
+
const adapter = {
|
|
89
|
+
name: "msg",
|
|
90
|
+
report: () => {},
|
|
91
|
+
captureMessage: (msg, level, ctx) => messages.push({ msg, level, ctx }),
|
|
92
|
+
};
|
|
93
|
+
reporter.use(adapter);
|
|
94
|
+
|
|
95
|
+
reporter.captureMessage("hello", "warn", { extra: true });
|
|
96
|
+
|
|
97
|
+
assert.equal(messages.length, 1);
|
|
98
|
+
assert.equal(messages[0].msg, "hello");
|
|
99
|
+
assert.equal(messages[0].level, "warn");
|
|
100
|
+
assert.equal(messages[0].ctx.extra, true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("skips adapters without captureMessage", () => {
|
|
104
|
+
const adapter = { name: "no-msg", report: () => {} };
|
|
105
|
+
reporter.use(adapter);
|
|
106
|
+
|
|
107
|
+
assert.doesNotThrow(() => reporter.captureMessage("hello"));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("setUser()", () => {
|
|
112
|
+
it("includes user in subsequent reports", () => {
|
|
113
|
+
const reported = [];
|
|
114
|
+
const adapter = {
|
|
115
|
+
name: "user-test",
|
|
116
|
+
report: (_err, ctx) => reported.push(ctx),
|
|
117
|
+
};
|
|
118
|
+
reporter.use(adapter);
|
|
119
|
+
|
|
120
|
+
reporter.setUser({ id: "u1", email: "a@b.com" });
|
|
121
|
+
reporter.report(new Error("test"));
|
|
122
|
+
|
|
123
|
+
assert.equal(reported.length, 1);
|
|
124
|
+
assert.deepEqual(reported[0].user, { id: "u1", email: "a@b.com" });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("forwards user to adapters with setUser", () => {
|
|
128
|
+
let receivedUser = null;
|
|
129
|
+
const adapter = {
|
|
130
|
+
name: "user-fwd",
|
|
131
|
+
report: () => {},
|
|
132
|
+
setUser: (u) => {
|
|
133
|
+
receivedUser = u;
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
reporter.use(adapter);
|
|
137
|
+
|
|
138
|
+
reporter.setUser({ id: "u2" });
|
|
139
|
+
assert.deepEqual(receivedUser, { id: "u2" });
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("addBreadcrumb()", () => {
|
|
144
|
+
it("stores breadcrumbs and attaches them to report context", () => {
|
|
145
|
+
const reported = [];
|
|
146
|
+
const adapter = {
|
|
147
|
+
name: "bc-test",
|
|
148
|
+
report: (_err, ctx) => reported.push(ctx),
|
|
149
|
+
};
|
|
150
|
+
reporter.use(adapter);
|
|
151
|
+
|
|
152
|
+
reporter.addBreadcrumb("clicked button", "ui", { buttonId: "submit" });
|
|
153
|
+
reporter.addBreadcrumb("navigated", "navigation");
|
|
154
|
+
reporter.report(new Error("test"));
|
|
155
|
+
|
|
156
|
+
assert.equal(reported.length, 1);
|
|
157
|
+
assert.equal(reported[0].breadcrumbs.length, 2);
|
|
158
|
+
assert.equal(reported[0].breadcrumbs[0].message, "clicked button");
|
|
159
|
+
assert.equal(reported[0].breadcrumbs[0].category, "ui");
|
|
160
|
+
assert.deepEqual(reported[0].breadcrumbs[0].data, { buttonId: "submit" });
|
|
161
|
+
assert.equal(reported[0].breadcrumbs[1].message, "navigated");
|
|
162
|
+
assert.equal(reported[0].breadcrumbs[1].category, "navigation");
|
|
163
|
+
assert.ok(reported[0].breadcrumbs[0].timestamp);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("circular buffer keeps max 50 breadcrumbs", () => {
|
|
167
|
+
for (let i = 0; i < 60; i++) {
|
|
168
|
+
reporter.addBreadcrumb(`crumb-${i}`, "test");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const crumbs = reporter.breadcrumbs;
|
|
172
|
+
assert.equal(crumbs.length, 50);
|
|
173
|
+
// First 10 should have been evicted, first remaining is crumb-10
|
|
174
|
+
assert.equal(crumbs[0].message, "crumb-10");
|
|
175
|
+
assert.equal(crumbs[49].message, "crumb-59");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("forwards breadcrumbs to adapters that support it", () => {
|
|
179
|
+
const received = [];
|
|
180
|
+
const adapter = {
|
|
181
|
+
name: "bc-fwd",
|
|
182
|
+
report: () => {},
|
|
183
|
+
addBreadcrumb: (msg, cat, data) => received.push({ msg, cat, data }),
|
|
184
|
+
};
|
|
185
|
+
reporter.use(adapter);
|
|
186
|
+
|
|
187
|
+
reporter.addBreadcrumb("test", "cat", { key: "val" });
|
|
188
|
+
|
|
189
|
+
assert.equal(received.length, 1);
|
|
190
|
+
assert.equal(received[0].msg, "test");
|
|
191
|
+
assert.equal(received[0].cat, "cat");
|
|
192
|
+
assert.deepEqual(received[0].data, { key: "val" });
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("custom adapter receives correct data", () => {
|
|
197
|
+
it("passes error and enriched context", () => {
|
|
198
|
+
const reported = [];
|
|
199
|
+
const adapter = {
|
|
200
|
+
name: "custom",
|
|
201
|
+
report: (err, ctx) => reported.push({ err, ctx }),
|
|
202
|
+
};
|
|
203
|
+
reporter.use(adapter);
|
|
204
|
+
|
|
205
|
+
reporter.setUser({ id: "u1" });
|
|
206
|
+
reporter.addBreadcrumb("init", "app");
|
|
207
|
+
|
|
208
|
+
const error = new Error("custom test");
|
|
209
|
+
reporter.report(error, { route: "/api/test" });
|
|
210
|
+
|
|
211
|
+
assert.equal(reported.length, 1);
|
|
212
|
+
assert.equal(reported[0].err, error);
|
|
213
|
+
assert.equal(reported[0].ctx.route, "/api/test");
|
|
214
|
+
assert.deepEqual(reported[0].ctx.user, { id: "u1" });
|
|
215
|
+
assert.equal(reported[0].ctx.breadcrumbs.length, 1);
|
|
216
|
+
assert.equal(reported[0].ctx.breadcrumbs[0].message, "init");
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe("createSentryAdapter()", () => {
|
|
221
|
+
it("returns a valid adapter shape", () => {
|
|
222
|
+
const adapter = createSentryAdapter();
|
|
223
|
+
assert.equal(adapter.name, "sentry");
|
|
224
|
+
assert.equal(typeof adapter.report, "function");
|
|
225
|
+
assert.equal(typeof adapter.captureMessage, "function");
|
|
226
|
+
assert.equal(typeof adapter.setUser, "function");
|
|
227
|
+
assert.equal(typeof adapter.addBreadcrumb, "function");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("can be registered without errors", () => {
|
|
231
|
+
const adapter = createSentryAdapter();
|
|
232
|
+
assert.doesNotThrow(() => reporter.use(adapter));
|
|
233
|
+
assert.doesNotThrow(() => reporter.report(new Error("test")));
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @techstream/quark-core - Error Handling Module
|
|
3
|
+
* Standardized error types and utilities for consistent error handling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { errorReporter } from "./error-reporter.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Base application error class
|
|
10
|
+
*/
|
|
11
|
+
export class AppError extends Error {
|
|
12
|
+
constructor(message, statusCode = 500, code = "INTERNAL_ERROR") {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = this.constructor.name;
|
|
15
|
+
this.statusCode = statusCode;
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.timestamp = new Date().toISOString();
|
|
18
|
+
Error.captureStackTrace(this, this.constructor);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
toJSON() {
|
|
22
|
+
return {
|
|
23
|
+
name: this.name,
|
|
24
|
+
message: this.message,
|
|
25
|
+
code: this.code,
|
|
26
|
+
statusCode: this.statusCode,
|
|
27
|
+
timestamp: this.timestamp,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 400 Bad Request Error
|
|
34
|
+
*/
|
|
35
|
+
export class ValidationError extends AppError {
|
|
36
|
+
constructor(message, details = null) {
|
|
37
|
+
super(message, 400, "VALIDATION_ERROR");
|
|
38
|
+
this.details = details;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
toJSON() {
|
|
42
|
+
return {
|
|
43
|
+
...super.toJSON(),
|
|
44
|
+
details: this.details,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 401 Unauthorized Error
|
|
51
|
+
*/
|
|
52
|
+
export class UnauthorizedError extends AppError {
|
|
53
|
+
constructor(message = "Authentication required") {
|
|
54
|
+
super(message, 401, "UNAUTHORIZED");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 403 Forbidden Error
|
|
60
|
+
*/
|
|
61
|
+
export class ForbiddenError extends AppError {
|
|
62
|
+
constructor(message = "Access denied") {
|
|
63
|
+
super(message, 403, "FORBIDDEN");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 404 Not Found Error
|
|
69
|
+
*/
|
|
70
|
+
export class NotFoundError extends AppError {
|
|
71
|
+
constructor(message = "Resource not found") {
|
|
72
|
+
super(message, 404, "NOT_FOUND");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 409 Conflict Error
|
|
78
|
+
*/
|
|
79
|
+
export class ConflictError extends AppError {
|
|
80
|
+
constructor(message = "Resource conflict") {
|
|
81
|
+
super(message, 409, "CONFLICT");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 429 Rate Limit Error
|
|
87
|
+
*/
|
|
88
|
+
export class RateLimitError extends AppError {
|
|
89
|
+
constructor(message = "Too many requests", retryAfter = 60) {
|
|
90
|
+
super(message, 429, "RATE_LIMIT");
|
|
91
|
+
this.retryAfter = retryAfter;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
toJSON() {
|
|
95
|
+
return {
|
|
96
|
+
...super.toJSON(),
|
|
97
|
+
retryAfter: this.retryAfter,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Database Error
|
|
104
|
+
*/
|
|
105
|
+
export class DatabaseError extends AppError {
|
|
106
|
+
constructor(message = "Database error", originalError = null) {
|
|
107
|
+
super(message, 500, "DATABASE_ERROR");
|
|
108
|
+
this.originalError = originalError;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* External Service Error
|
|
114
|
+
*/
|
|
115
|
+
export class ServiceError extends AppError {
|
|
116
|
+
constructor(serviceName, message = "Service error", statusCode = 502) {
|
|
117
|
+
super(message, statusCode, "SERVICE_ERROR");
|
|
118
|
+
this.serviceName = serviceName;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
toJSON() {
|
|
122
|
+
return {
|
|
123
|
+
...super.toJSON(),
|
|
124
|
+
serviceName: this.serviceName,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Safely extracts error message from various error types
|
|
131
|
+
* @param {Error|string|Object} error - The error to extract from
|
|
132
|
+
* @returns {string} Extracted error message
|
|
133
|
+
*/
|
|
134
|
+
export const getErrorMessage = (error) => {
|
|
135
|
+
if (typeof error === "string") return error;
|
|
136
|
+
if (error instanceof Error) return error.message;
|
|
137
|
+
if (error && typeof error === "object" && error.message) return error.message;
|
|
138
|
+
return "An unknown error occurred";
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Safely extracts status code from error
|
|
143
|
+
* @param {Error|AppError} error - The error to extract from
|
|
144
|
+
* @returns {number} HTTP status code
|
|
145
|
+
*/
|
|
146
|
+
export const getStatusCode = (error) => {
|
|
147
|
+
if (error instanceof AppError) return error.statusCode;
|
|
148
|
+
if (error.statusCode) return error.statusCode;
|
|
149
|
+
return 500;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Converts any error to an AppError instance
|
|
154
|
+
* @param {Error|AppError|any} error - The error to normalize
|
|
155
|
+
* @returns {AppError} Normalized error
|
|
156
|
+
*/
|
|
157
|
+
export const normalizeError = (error) => {
|
|
158
|
+
if (error instanceof AppError) return error;
|
|
159
|
+
if (error instanceof Error) {
|
|
160
|
+
return new AppError(error.message, 500, "INTERNAL_ERROR");
|
|
161
|
+
}
|
|
162
|
+
return new AppError(
|
|
163
|
+
typeof error === "string" ? error : "An unknown error occurred",
|
|
164
|
+
500,
|
|
165
|
+
"INTERNAL_ERROR",
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Logs error with context
|
|
171
|
+
* @param {Error} error - The error to log
|
|
172
|
+
* @param {Object} context - Additional context
|
|
173
|
+
*/
|
|
174
|
+
export const logError = (error, context = {}) => {
|
|
175
|
+
errorReporter.report(error, context);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Wraps async function and handles errors consistently
|
|
180
|
+
* @param {Function} fn - Async function to wrap
|
|
181
|
+
* @returns {Function} Wrapped function
|
|
182
|
+
*/
|
|
183
|
+
export const withErrorHandling = (fn) => {
|
|
184
|
+
return async (...args) => {
|
|
185
|
+
try {
|
|
186
|
+
return await fn(...args);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
errorReporter.report(error);
|
|
189
|
+
throw normalizeError(error);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
AppError,
|
|
5
|
+
ConflictError,
|
|
6
|
+
DatabaseError,
|
|
7
|
+
ForbiddenError,
|
|
8
|
+
getErrorMessage,
|
|
9
|
+
getStatusCode,
|
|
10
|
+
logError,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
normalizeError,
|
|
13
|
+
RateLimitError,
|
|
14
|
+
ServiceError,
|
|
15
|
+
UnauthorizedError,
|
|
16
|
+
ValidationError,
|
|
17
|
+
} from "../src/errors.js";
|
|
18
|
+
|
|
19
|
+
test("Error Module", async (t) => {
|
|
20
|
+
await t.test("AppError has correct properties", () => {
|
|
21
|
+
const error = new AppError("Test error", 500, "TEST_ERROR");
|
|
22
|
+
assert(error.message === "Test error");
|
|
23
|
+
assert(error.statusCode === 500);
|
|
24
|
+
assert(error.code === "TEST_ERROR");
|
|
25
|
+
assert(error.name === "AppError");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await t.test("AppError.toJSON() serializes correctly", () => {
|
|
29
|
+
const error = new AppError("Test", 500, "TEST");
|
|
30
|
+
const json = error.toJSON();
|
|
31
|
+
assert(json.message === "Test");
|
|
32
|
+
assert(json.statusCode === 500);
|
|
33
|
+
assert(json.code === "TEST");
|
|
34
|
+
assert(json.timestamp);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await t.test("ValidationError has status 400", () => {
|
|
38
|
+
const error = new ValidationError("Invalid input");
|
|
39
|
+
assert(error.statusCode === 400);
|
|
40
|
+
assert(error.code === "VALIDATION_ERROR");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await t.test("UnauthorizedError has status 401", () => {
|
|
44
|
+
const error = new UnauthorizedError();
|
|
45
|
+
assert(error.statusCode === 401);
|
|
46
|
+
assert(error.code === "UNAUTHORIZED");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await t.test("ForbiddenError has status 403", () => {
|
|
50
|
+
const error = new ForbiddenError();
|
|
51
|
+
assert(error.statusCode === 403);
|
|
52
|
+
assert(error.code === "FORBIDDEN");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await t.test("NotFoundError has status 404", () => {
|
|
56
|
+
const error = new NotFoundError();
|
|
57
|
+
assert(error.statusCode === 404);
|
|
58
|
+
assert(error.code === "NOT_FOUND");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await t.test("ConflictError has status 409", () => {
|
|
62
|
+
const error = new ConflictError();
|
|
63
|
+
assert(error.statusCode === 409);
|
|
64
|
+
assert(error.code === "CONFLICT");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await t.test("RateLimitError has status 429", () => {
|
|
68
|
+
const error = new RateLimitError("Too many requests", 60);
|
|
69
|
+
assert(error.statusCode === 429);
|
|
70
|
+
assert(error.code === "RATE_LIMIT");
|
|
71
|
+
assert(error.retryAfter === 60);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await t.test("DatabaseError wraps database errors", () => {
|
|
75
|
+
const originalError = new Error("Connection failed");
|
|
76
|
+
const error = new DatabaseError("DB error", originalError);
|
|
77
|
+
assert(error.statusCode === 500);
|
|
78
|
+
assert(error.originalError === originalError);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await t.test("ServiceError includes service name", () => {
|
|
82
|
+
const error = new ServiceError("PaymentAPI", "Payment failed");
|
|
83
|
+
assert(error.serviceName === "PaymentAPI");
|
|
84
|
+
assert(error.statusCode === 502);
|
|
85
|
+
const json = error.toJSON();
|
|
86
|
+
assert(json.serviceName === "PaymentAPI");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await t.test("getErrorMessage extracts from various types", () => {
|
|
90
|
+
assert(getErrorMessage("string error") === "string error");
|
|
91
|
+
assert(getErrorMessage(new Error("error message")) === "error message");
|
|
92
|
+
assert(getErrorMessage({ message: "obj error" }) === "obj error");
|
|
93
|
+
assert(getErrorMessage({}).includes("unknown")); // Changed from null to empty object
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await t.test("getStatusCode returns correct code", () => {
|
|
97
|
+
assert(getStatusCode(new ValidationError("")) === 400);
|
|
98
|
+
assert(getStatusCode(new UnauthorizedError()) === 401);
|
|
99
|
+
assert(getStatusCode(new NotFoundError()) === 404);
|
|
100
|
+
assert(getStatusCode(new Error()) === 500);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await t.test("normalizeError converts to AppError", () => {
|
|
104
|
+
const error = normalizeError(new Error("test"));
|
|
105
|
+
assert(error instanceof AppError);
|
|
106
|
+
assert(error.message === "test");
|
|
107
|
+
assert(error.statusCode === 500);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await t.test("normalizeError handles string errors", () => {
|
|
111
|
+
const error = normalizeError("string error");
|
|
112
|
+
assert(error instanceof AppError);
|
|
113
|
+
assert(error.message === "string error");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await t.test("normalizeError handles non-Error objects", () => {
|
|
117
|
+
const error = normalizeError({ some: "object" });
|
|
118
|
+
assert(error instanceof AppError);
|
|
119
|
+
assert(error.message.includes("unknown"));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await t.test("logError handles errors with context", () => {
|
|
123
|
+
// This should not throw
|
|
124
|
+
const error = new ValidationError("test");
|
|
125
|
+
logError(error, { userId: "123", action: "test" });
|
|
126
|
+
assert(true);
|
|
127
|
+
});
|
|
128
|
+
});
|
package/src/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Authorization exports
|
|
2
|
+
|
|
3
|
+
// Auth exports
|
|
4
|
+
export * from "./auth/index.js";
|
|
5
|
+
export * from "./authorization.js";
|
|
6
|
+
// Cache exports
|
|
7
|
+
export * from "./cache.js";
|
|
8
|
+
// CSRF protection exports
|
|
9
|
+
export * from "./csrf.js";
|
|
10
|
+
// Error Reporter exports
|
|
11
|
+
export * from "./error-reporter.js";
|
|
12
|
+
// Error exports
|
|
13
|
+
export * from "./errors.js";
|
|
14
|
+
// Logger exports
|
|
15
|
+
export * from "./logger.js";
|
|
16
|
+
// Mailhog exports
|
|
17
|
+
export * from "./mailhog.js";
|
|
18
|
+
// Email service exports
|
|
19
|
+
export * from "./email.js";
|
|
20
|
+
// Queue exports
|
|
21
|
+
export * from "./queue/index.js";
|
|
22
|
+
// Rate limiting exports
|
|
23
|
+
export * from "./rate-limiter.js";
|
|
24
|
+
// Redis exports
|
|
25
|
+
export * from "./redis.js";
|
|
26
|
+
|
|
27
|
+
// Utility exports
|
|
28
|
+
export * from "./utils.js";
|
|
29
|
+
export * from "./validation.js";
|
|
30
|
+
|
|
31
|
+
// Testing utilities (import from "@techstream/quark-core/testing" in test files)
|
|
32
|
+
// Not re-exported from main to avoid polluting production imports
|