@twasik4/pocket-service 1.0.4 → 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/CHANGELOG.md +34 -0
- package/LICENSE +21 -0
- package/README.md +637 -297
- package/dist/index.d.mts +747 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +4 -0
- package/dist/index.mjs.map +1 -0
- package/dist/runtime/worker-thread.d.mts +1 -0
- package/dist/runtime/worker-thread.mjs +2 -0
- package/dist/runtime/worker-thread.mjs.map +1 -0
- package/package.json +53 -10
- package/dist/index.d.ts +0 -734
- package/dist/index.js +0 -6
- package/dist/index.js.map +0 -1
- package/dist/worker-thread.d.ts +0 -2
- package/dist/worker-thread.js +0 -2
- package/dist/worker-thread.js.map +0 -1
- package/src/index.ts +0 -1
- package/src/worker-thread.ts +0 -159
- package/src/workers/auth/strategies.ts +0 -304
- package/src/workers/db/clickhouse/index.ts +0 -76
- package/src/workers/db/mongo/collection.ts +0 -293
- package/src/workers/db/mongo/mongo.ts +0 -401
- package/src/workers/express-types.ts +0 -442
- package/src/workers/index.ts +0 -7
- package/src/workers/logger.ts +0 -19
- package/src/workers/service.ts +0 -1015
- package/src/workers/stream-handler.ts +0 -25
- package/src/workers/types.ts +0 -59
- package/src/workers/utils.ts +0 -19
- package/tests/auth-strategies.spec.ts +0 -150
- package/tests/clickhouse.spec.ts +0 -102
- package/tests/express-types.spec.ts +0 -232
- package/tests/mongo-advanced.spec.ts +0 -172
- package/tests/mongo.spec.ts +0 -141
- package/tests/redis.spec.ts +0 -36
- package/tests/service.spec.ts +0 -82
- package/tests/utils.spec.ts +0 -30
- package/tsconfig.json +0 -12
- package/tsup.config.ts +0 -11
- package/vitest.config.ts +0 -8
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export type RedisStreamHandlerContext = {
|
|
2
|
-
service: string;
|
|
3
|
-
emit: (ev: {
|
|
4
|
-
type: string;
|
|
5
|
-
data: Record<string, string | number | boolean | null>;
|
|
6
|
-
}) => Promise<void>;
|
|
7
|
-
log: {
|
|
8
|
-
info: (msg: string) => void;
|
|
9
|
-
warn: (msg: string) => void;
|
|
10
|
-
error: (msg: string) => void;
|
|
11
|
-
};
|
|
12
|
-
};
|
|
13
|
-
export type RedisStreamEventHandler = {
|
|
14
|
-
type: string;
|
|
15
|
-
execute: (
|
|
16
|
-
ev: Record<string, any>,
|
|
17
|
-
ctx: RedisStreamHandlerContext,
|
|
18
|
-
) => Promise<void>;
|
|
19
|
-
};
|
|
20
|
-
export function redisStreamHandler(
|
|
21
|
-
type: string,
|
|
22
|
-
fn: RedisStreamEventHandler["execute"],
|
|
23
|
-
): RedisStreamEventHandler {
|
|
24
|
-
return { type, execute: fn };
|
|
25
|
-
}
|
package/src/workers/types.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { NextFunction, Request, Response } from "express";
|
|
2
|
-
import { Logger } from "winston";
|
|
3
|
-
|
|
4
|
-
export type Message = {
|
|
5
|
-
type: string;
|
|
6
|
-
userId: string;
|
|
7
|
-
teamId: string;
|
|
8
|
-
timestamp: number;
|
|
9
|
-
payload: Record<string, string | number | boolean | null>;
|
|
10
|
-
};
|
|
11
|
-
export type ExpressMiddleware = (
|
|
12
|
-
req: Request,
|
|
13
|
-
res: Response,
|
|
14
|
-
next: NextFunction,
|
|
15
|
-
) => void;
|
|
16
|
-
export type BaseExpressOptions = {
|
|
17
|
-
/**
|
|
18
|
-
* Don't include the default health check and metrics routes.
|
|
19
|
-
*/
|
|
20
|
-
omitDefaultRoutes?: boolean;
|
|
21
|
-
/**
|
|
22
|
-
* Whether to parse incoming request bodies as JSON.
|
|
23
|
-
*/
|
|
24
|
-
asJson: boolean;
|
|
25
|
-
/**
|
|
26
|
-
* An array of custom Express middleware functions to apply to the Express app globally (before all routes).
|
|
27
|
-
*/
|
|
28
|
-
customMiddleware?: ExpressMiddleware[];
|
|
29
|
-
/**
|
|
30
|
-
* Whether to allow CORS requests with credentials (cookies, authorization headers, etc.). This option is only relevant if CORS is enabled via either `corsFn` or `corsWhitelist`.
|
|
31
|
-
*/
|
|
32
|
-
credentials?: boolean;
|
|
33
|
-
};
|
|
34
|
-
export type ExpressOptionsWithCorsFn = BaseExpressOptions & {
|
|
35
|
-
/**
|
|
36
|
-
* A function to determine whether to allow CORS requests from a given origin.
|
|
37
|
-
* @param origin The origin of the request.
|
|
38
|
-
* @param callback A callback function to indicate whether the origin is allowed.
|
|
39
|
-
*/
|
|
40
|
-
corsFn: (
|
|
41
|
-
origin: string | undefined,
|
|
42
|
-
callback: (err: Error | null, allow?: boolean) => void,
|
|
43
|
-
) => void;
|
|
44
|
-
};
|
|
45
|
-
export type ExpressOptionsWithCorsWhitelist = BaseExpressOptions & {
|
|
46
|
-
/**
|
|
47
|
-
* An array of allowed origins for CORS requests. If the origin of an incoming request is in this whitelist, the request will be allowed.
|
|
48
|
-
*/
|
|
49
|
-
corsWhitelist: string[];
|
|
50
|
-
};
|
|
51
|
-
export type ExpressOptions =
|
|
52
|
-
| ExpressOptionsWithCorsFn
|
|
53
|
-
| ExpressOptionsWithCorsWhitelist;
|
|
54
|
-
export type CustomModuleFactory = (log: Logger) => boolean | Promise<boolean>;
|
|
55
|
-
export type CustomModules = {
|
|
56
|
-
init: CustomModuleFactory;
|
|
57
|
-
shutdown?: CustomModuleFactory;
|
|
58
|
-
name: string;
|
|
59
|
-
}[];
|
package/src/workers/utils.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import * as crypto from "crypto";
|
|
2
|
-
|
|
3
|
-
export function generateRandomHexString(length: number): string {
|
|
4
|
-
if (length % 2 !== 0) {
|
|
5
|
-
throw new Error("Length must be an even number");
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const numBytes = length / 2;
|
|
9
|
-
const randomBytes = crypto.randomBytes(numBytes);
|
|
10
|
-
return randomBytes.toString("hex");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function getUseableDatesFromMs(ms: number) {
|
|
14
|
-
const seconds = Math.floor((ms / 1000) % 60);
|
|
15
|
-
const minutes = Math.floor((ms / (1000 * 60)) % 60);
|
|
16
|
-
const hours = Math.floor((ms / (1000 * 60 * 60)) % 24);
|
|
17
|
-
const days = Math.floor(ms / (1000 * 60 * 60 * 24));
|
|
18
|
-
return { days, hours, minutes, seconds };
|
|
19
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/// <reference types="vitest/globals" />
|
|
2
|
-
import type { Request } from "express";
|
|
3
|
-
import { SignJWT } from "jose";
|
|
4
|
-
import {
|
|
5
|
-
createJoseJwtVerifier,
|
|
6
|
-
createJwtAuthStrategy,
|
|
7
|
-
createMtlsAuthStrategy,
|
|
8
|
-
} from "../src/workers/auth/strategies";
|
|
9
|
-
import type { RouteDefinition } from "../src/workers/express-types";
|
|
10
|
-
|
|
11
|
-
const route: RouteDefinition<any, any, boolean> = {
|
|
12
|
-
method: "GET",
|
|
13
|
-
fullPath: "/secure",
|
|
14
|
-
requireAuth: true,
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
describe("Auth strategies", () => {
|
|
18
|
-
it("maps JWT claims to authenticated user using Bearer token", async () => {
|
|
19
|
-
const strategy = createJwtAuthStrategy({
|
|
20
|
-
verifyToken: async () => ({
|
|
21
|
-
sub: "user-1",
|
|
22
|
-
team: "core",
|
|
23
|
-
flags: ["a", "b"],
|
|
24
|
-
ignoredObject: { x: 1 },
|
|
25
|
-
}),
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const req = {
|
|
29
|
-
headers: { authorization: "Bearer signed-token" },
|
|
30
|
-
} as unknown as Request;
|
|
31
|
-
|
|
32
|
-
await expect(strategy.authenticate(req, route)).resolves.toEqual({
|
|
33
|
-
userId: "user-1",
|
|
34
|
-
team: "core",
|
|
35
|
-
flags: ["a", "b"],
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("returns null when token prefix does not match", async () => {
|
|
40
|
-
const verifyToken = vi.fn();
|
|
41
|
-
const strategy = createJwtAuthStrategy({
|
|
42
|
-
verifyToken,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const req = {
|
|
46
|
-
headers: { authorization: "Token signed-token" },
|
|
47
|
-
} as unknown as Request;
|
|
48
|
-
|
|
49
|
-
await expect(strategy.authenticate(req, route)).resolves.toBeNull();
|
|
50
|
-
expect(verifyToken).not.toHaveBeenCalled();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("supports custom payload mapping", async () => {
|
|
54
|
-
const strategy = createJwtAuthStrategy({
|
|
55
|
-
verifyToken: async () => ({ sub: "user-2", role: "admin" }),
|
|
56
|
-
mapPayloadToUser: async (claims) => ({
|
|
57
|
-
userId: String(claims.sub),
|
|
58
|
-
role: String(claims.role),
|
|
59
|
-
}),
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const req = {
|
|
63
|
-
headers: { authorization: "Bearer signed-token" },
|
|
64
|
-
} as unknown as Request;
|
|
65
|
-
|
|
66
|
-
await expect(strategy.authenticate(req, route)).resolves.toEqual({
|
|
67
|
-
userId: "user-2",
|
|
68
|
-
role: "admin",
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("verifies HS256 JWT using jose verifier", async () => {
|
|
73
|
-
const secret = "test-secret";
|
|
74
|
-
const token = await new SignJWT({ scope: "api:read" })
|
|
75
|
-
.setProtectedHeader({ alg: "HS256" })
|
|
76
|
-
.setSubject("subject-1")
|
|
77
|
-
.setIssuer("issuer-a")
|
|
78
|
-
.setAudience("audience-a")
|
|
79
|
-
.setIssuedAt()
|
|
80
|
-
.setExpirationTime("10m")
|
|
81
|
-
.sign(new TextEncoder().encode(secret));
|
|
82
|
-
|
|
83
|
-
const verify = createJoseJwtVerifier({
|
|
84
|
-
secret,
|
|
85
|
-
issuer: "issuer-a",
|
|
86
|
-
audience: "audience-a",
|
|
87
|
-
requiredClaims: ["sub", "scope"],
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
await expect(verify(token, {} as Request, route)).resolves.toMatchObject({
|
|
91
|
-
sub: "subject-1",
|
|
92
|
-
scope: "api:read",
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("returns null when required JWT claims are missing", async () => {
|
|
97
|
-
const secret = "test-secret";
|
|
98
|
-
const token = await new SignJWT({ role: "reader" })
|
|
99
|
-
.setProtectedHeader({ alg: "HS256" })
|
|
100
|
-
.setSubject("subject-2")
|
|
101
|
-
.setIssuedAt()
|
|
102
|
-
.setExpirationTime("10m")
|
|
103
|
-
.sign(new TextEncoder().encode(secret));
|
|
104
|
-
|
|
105
|
-
const verify = createJoseJwtVerifier({
|
|
106
|
-
secret,
|
|
107
|
-
requiredClaims: ["sub", "scope"],
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
await expect(verify(token, {} as Request, route)).resolves.toBeNull();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("extracts mTLS identity from URI SAN", async () => {
|
|
114
|
-
const strategy = createMtlsAuthStrategy();
|
|
115
|
-
const req = {
|
|
116
|
-
socket: {
|
|
117
|
-
authorized: true,
|
|
118
|
-
getPeerCertificate: () => ({
|
|
119
|
-
subjectaltname:
|
|
120
|
-
"DNS:service.local, URI:spiffe://cluster/ns/default/sa/api",
|
|
121
|
-
subject: { CN: "subject-cn" },
|
|
122
|
-
issuer: { CN: "issuer-cn" },
|
|
123
|
-
fingerprint256: "fingerprint",
|
|
124
|
-
serialNumber: "serial",
|
|
125
|
-
}),
|
|
126
|
-
},
|
|
127
|
-
} as unknown as Request;
|
|
128
|
-
|
|
129
|
-
await expect(strategy.authenticate(req, route)).resolves.toMatchObject({
|
|
130
|
-
userId: "spiffe://cluster/ns/default/sa/api",
|
|
131
|
-
certSubjectCn: "subject-cn",
|
|
132
|
-
certIssuerCn: "issuer-cn",
|
|
133
|
-
certFingerprint256: "fingerprint",
|
|
134
|
-
certSerialNumber: "serial",
|
|
135
|
-
mtlsAuthorized: true,
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("rejects unauthorized mTLS request by default", async () => {
|
|
140
|
-
const strategy = createMtlsAuthStrategy();
|
|
141
|
-
const req = {
|
|
142
|
-
socket: {
|
|
143
|
-
authorized: false,
|
|
144
|
-
getPeerCertificate: () => ({ subject: { CN: "x" } }),
|
|
145
|
-
},
|
|
146
|
-
} as unknown as Request;
|
|
147
|
-
|
|
148
|
-
await expect(strategy.authenticate(req, route)).resolves.toBeNull();
|
|
149
|
-
});
|
|
150
|
-
});
|
package/tests/clickhouse.spec.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/// <reference types="vitest/globals" />
|
|
2
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
|
|
4
|
-
const commandMock = vi.fn();
|
|
5
|
-
const queryMock = vi.fn();
|
|
6
|
-
const insertMock = vi.fn();
|
|
7
|
-
const closeMock = vi.fn();
|
|
8
|
-
|
|
9
|
-
vi.mock("@clickhouse/client", () => ({
|
|
10
|
-
createClient: vi.fn(() => ({
|
|
11
|
-
command: commandMock,
|
|
12
|
-
query: queryMock,
|
|
13
|
-
insert: insertMock,
|
|
14
|
-
close: closeMock,
|
|
15
|
-
})),
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
import { createTypedClickhouse } from "../src/workers/db/clickhouse";
|
|
19
|
-
|
|
20
|
-
describe("Clickhouse wrapper", () => {
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
commandMock.mockReset();
|
|
23
|
-
queryMock.mockReset();
|
|
24
|
-
insertMock.mockReset();
|
|
25
|
-
closeMock.mockReset();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("executes createSQL for every configured table", async () => {
|
|
29
|
-
const clickhouse = createTypedClickhouse({
|
|
30
|
-
url: "http://localhost:8123",
|
|
31
|
-
tables: {
|
|
32
|
-
events: {
|
|
33
|
-
name: "events",
|
|
34
|
-
createSQL: "CREATE TABLE events (...)",
|
|
35
|
-
schema: () => ({ id: "", type: "" }),
|
|
36
|
-
},
|
|
37
|
-
metrics: {
|
|
38
|
-
name: "metrics",
|
|
39
|
-
createSQL: "CREATE TABLE metrics (...)",
|
|
40
|
-
schema: () => ({ id: "", value: 0 }),
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
await clickhouse.ensureTables();
|
|
46
|
-
|
|
47
|
-
expect(commandMock).toHaveBeenCalledTimes(2);
|
|
48
|
-
expect(commandMock).toHaveBeenNthCalledWith(1, {
|
|
49
|
-
query: "CREATE TABLE events (...)",
|
|
50
|
-
});
|
|
51
|
-
expect(commandMock).toHaveBeenNthCalledWith(2, {
|
|
52
|
-
query: "CREATE TABLE metrics (...)",
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("builds SELECT query with filters and returns parsed rows", async () => {
|
|
57
|
-
queryMock.mockResolvedValue({
|
|
58
|
-
json: async () => ({
|
|
59
|
-
data: [{ id: "1", type: "login", severity: 2 }],
|
|
60
|
-
}),
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
const clickhouse = createTypedClickhouse({
|
|
64
|
-
url: "http://localhost:8123",
|
|
65
|
-
tables: {
|
|
66
|
-
events: {
|
|
67
|
-
name: "events",
|
|
68
|
-
createSQL: "CREATE TABLE events (...)",
|
|
69
|
-
schema: () => ({ id: "", type: "", severity: 0 }),
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const rows = await clickhouse.events.select({ type: "login", severity: 2 });
|
|
75
|
-
|
|
76
|
-
expect(queryMock).toHaveBeenCalledWith({
|
|
77
|
-
query: "SELECT * FROM events WHERE type = 'login' AND severity = 2",
|
|
78
|
-
});
|
|
79
|
-
expect(rows).toEqual([{ id: "1", type: "login", severity: 2 }]);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("inserts rows using JSONEachRow format", async () => {
|
|
83
|
-
const clickhouse = createTypedClickhouse({
|
|
84
|
-
url: "http://localhost:8123",
|
|
85
|
-
tables: {
|
|
86
|
-
events: {
|
|
87
|
-
name: "events",
|
|
88
|
-
createSQL: "CREATE TABLE events (...)",
|
|
89
|
-
schema: () => ({ id: "", type: "" }),
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
await clickhouse.events.insert([{ id: "1", type: "login" }]);
|
|
95
|
-
|
|
96
|
-
expect(insertMock).toHaveBeenCalledWith({
|
|
97
|
-
table: "events",
|
|
98
|
-
values: [{ id: "1", type: "login" }],
|
|
99
|
-
format: "JSONEachRow",
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
});
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
/// <reference types="vitest/globals" />
|
|
2
|
-
import type { Request } from "express";
|
|
3
|
-
import z from "zod";
|
|
4
|
-
import {
|
|
5
|
-
createAuthResolver,
|
|
6
|
-
createMetaRouterWithAuth,
|
|
7
|
-
defaultHeaderAuthResolver,
|
|
8
|
-
} from "../src/workers/express-types";
|
|
9
|
-
|
|
10
|
-
function createMockResponse() {
|
|
11
|
-
return {
|
|
12
|
-
statusCode: 200,
|
|
13
|
-
body: undefined as unknown,
|
|
14
|
-
status(code: number) {
|
|
15
|
-
this.statusCode = code;
|
|
16
|
-
return this;
|
|
17
|
-
},
|
|
18
|
-
json(payload: unknown) {
|
|
19
|
-
this.body = payload;
|
|
20
|
-
return this;
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe("express-types", () => {
|
|
26
|
-
it("reads user from x-user-meta header when valid JSON is provided", () => {
|
|
27
|
-
const req = {
|
|
28
|
-
headers: {
|
|
29
|
-
"x-user-meta": JSON.stringify({ userId: "user-1", role: "admin" }),
|
|
30
|
-
},
|
|
31
|
-
} as unknown as Request;
|
|
32
|
-
|
|
33
|
-
expect(defaultHeaderAuthResolver(req)).toEqual({
|
|
34
|
-
userId: "user-1",
|
|
35
|
-
role: "admin",
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("returns null when x-user-meta is invalid JSON", () => {
|
|
40
|
-
const req = {
|
|
41
|
-
headers: {
|
|
42
|
-
"x-user-id": "user-1",
|
|
43
|
-
"x-user-meta": "{invalid-json",
|
|
44
|
-
},
|
|
45
|
-
} as unknown as Request;
|
|
46
|
-
|
|
47
|
-
expect(defaultHeaderAuthResolver(req)).toBeNull();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("falls back to x-user-id header when x-user-meta is not set", () => {
|
|
51
|
-
const req = {
|
|
52
|
-
headers: { "x-user-id": "user-2" },
|
|
53
|
-
} as unknown as Request;
|
|
54
|
-
|
|
55
|
-
expect(defaultHeaderAuthResolver(req)).toEqual({ userId: "user-2" });
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("uses auth strategies in order and then fallback resolver", async () => {
|
|
59
|
-
const authResolver = createAuthResolver(
|
|
60
|
-
[
|
|
61
|
-
{
|
|
62
|
-
name: "first",
|
|
63
|
-
canHandle: async () => false,
|
|
64
|
-
authenticate: async () => ({ userId: "ignored" }),
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
name: "second",
|
|
68
|
-
authenticate: async () => null,
|
|
69
|
-
},
|
|
70
|
-
],
|
|
71
|
-
{
|
|
72
|
-
fallbackResolver: async () => ({ userId: "fallback" }),
|
|
73
|
-
},
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
await expect(
|
|
77
|
-
authResolver({ headers: {} } as unknown as Request, {
|
|
78
|
-
method: "GET",
|
|
79
|
-
fullPath: "/",
|
|
80
|
-
requireAuth: true,
|
|
81
|
-
}),
|
|
82
|
-
).resolves.toEqual({ userId: "fallback" });
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("returns route metadata from createMetaRouterWithAuth", () => {
|
|
86
|
-
const { routes, addRoute } = createMetaRouterWithAuth();
|
|
87
|
-
|
|
88
|
-
addRoute(
|
|
89
|
-
{
|
|
90
|
-
method: "POST",
|
|
91
|
-
fullPath: "/items/:id",
|
|
92
|
-
requireAuth: true,
|
|
93
|
-
bodyValidator: z.object({ name: z.string() }),
|
|
94
|
-
paramsValidator: z.object({ id: z.string() }),
|
|
95
|
-
meta: { description: "Create item" },
|
|
96
|
-
},
|
|
97
|
-
(_req, res) => {
|
|
98
|
-
res.status(200).json({ ok: true });
|
|
99
|
-
},
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
expect(routes).toHaveLength(1);
|
|
103
|
-
expect(routes[0]).toMatchObject({
|
|
104
|
-
method: "POST",
|
|
105
|
-
fullPath: "/items/:id",
|
|
106
|
-
requireAuth: true,
|
|
107
|
-
meta: { description: "Create item" },
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("returns 401 for protected route when auth resolver returns null", async () => {
|
|
112
|
-
const { router, addRoute } = createMetaRouterWithAuth({
|
|
113
|
-
authResolver: async () => null,
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
addRoute(
|
|
117
|
-
{
|
|
118
|
-
method: "POST",
|
|
119
|
-
fullPath: "/secure",
|
|
120
|
-
requireAuth: true,
|
|
121
|
-
bodyValidator: z.object({ name: z.string() }),
|
|
122
|
-
},
|
|
123
|
-
(_req, res) => {
|
|
124
|
-
res.status(200).json({ ok: true });
|
|
125
|
-
},
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
const layer = (router as any).stack.find(
|
|
129
|
-
(l: any) => l.route?.path === "/secure",
|
|
130
|
-
);
|
|
131
|
-
const authMiddleware = layer.route.stack[0].handle;
|
|
132
|
-
|
|
133
|
-
const req = { headers: {}, body: { name: "a" } } as unknown as Request;
|
|
134
|
-
const res = createMockResponse();
|
|
135
|
-
const next = vi.fn();
|
|
136
|
-
|
|
137
|
-
await authMiddleware(req, res, next);
|
|
138
|
-
|
|
139
|
-
expect(next).not.toHaveBeenCalled();
|
|
140
|
-
expect(res.statusCode).toBe(401);
|
|
141
|
-
expect(res.body).toEqual({ isSuccess: false, message: "Unauthorized" });
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("parses JSON body string when object schema is used", () => {
|
|
145
|
-
const { router, addRoute } = createMetaRouterWithAuth();
|
|
146
|
-
|
|
147
|
-
addRoute(
|
|
148
|
-
{
|
|
149
|
-
method: "POST",
|
|
150
|
-
fullPath: "/body",
|
|
151
|
-
bodyValidator: z.object({ count: z.number() }),
|
|
152
|
-
},
|
|
153
|
-
(_req, res) => {
|
|
154
|
-
res.status(200).json({ ok: true });
|
|
155
|
-
},
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
const layer = (router as any).stack.find(
|
|
159
|
-
(l: any) => l.route?.path === "/body",
|
|
160
|
-
);
|
|
161
|
-
const bodyMiddleware = layer.route.stack[0].handle;
|
|
162
|
-
|
|
163
|
-
const req = { body: '{"count":1}' } as Request;
|
|
164
|
-
const res = createMockResponse();
|
|
165
|
-
const next = vi.fn();
|
|
166
|
-
|
|
167
|
-
bodyMiddleware(req, res as any, next);
|
|
168
|
-
|
|
169
|
-
expect(next).toHaveBeenCalledOnce();
|
|
170
|
-
expect(req.body).toEqual({ count: 1 });
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("returns 400 for invalid JSON body string", () => {
|
|
174
|
-
const { router, addRoute } = createMetaRouterWithAuth();
|
|
175
|
-
|
|
176
|
-
addRoute(
|
|
177
|
-
{
|
|
178
|
-
method: "POST",
|
|
179
|
-
fullPath: "/invalid-body",
|
|
180
|
-
bodyValidator: z.object({ count: z.number() }),
|
|
181
|
-
},
|
|
182
|
-
(_req, res) => {
|
|
183
|
-
res.status(200).json({ ok: true });
|
|
184
|
-
},
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
const layer = (router as any).stack.find(
|
|
188
|
-
(l: any) => l.route?.path === "/invalid-body",
|
|
189
|
-
);
|
|
190
|
-
const bodyMiddleware = layer.route.stack[0].handle;
|
|
191
|
-
|
|
192
|
-
const req = { body: "{bad" } as Request;
|
|
193
|
-
const res = createMockResponse();
|
|
194
|
-
const next = vi.fn();
|
|
195
|
-
|
|
196
|
-
bodyMiddleware(req, res as any, next);
|
|
197
|
-
|
|
198
|
-
expect(next).not.toHaveBeenCalled();
|
|
199
|
-
expect(res.statusCode).toBe(400);
|
|
200
|
-
expect((res.body as any).message).toBe("Invalid JSON in request body");
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it("returns 400 when params validation fails", () => {
|
|
204
|
-
const { router, addRoute } = createMetaRouterWithAuth();
|
|
205
|
-
|
|
206
|
-
addRoute(
|
|
207
|
-
{
|
|
208
|
-
method: "GET",
|
|
209
|
-
fullPath: "/items/:id",
|
|
210
|
-
paramsValidator: z.object({ id: z.uuid() }),
|
|
211
|
-
},
|
|
212
|
-
(_req, res) => {
|
|
213
|
-
res.status(200).json({ ok: true });
|
|
214
|
-
},
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
const layer = (router as any).stack.find(
|
|
218
|
-
(l: any) => l.route?.path === "/items/:id",
|
|
219
|
-
);
|
|
220
|
-
const paramsMiddleware = layer.route.stack[0].handle;
|
|
221
|
-
|
|
222
|
-
const req = { params: { id: "not-a-uuid" } } as unknown as Request;
|
|
223
|
-
const res = createMockResponse();
|
|
224
|
-
const next = vi.fn();
|
|
225
|
-
|
|
226
|
-
paramsMiddleware(req, res as any, next);
|
|
227
|
-
|
|
228
|
-
expect(next).not.toHaveBeenCalled();
|
|
229
|
-
expect(res.statusCode).toBe(400);
|
|
230
|
-
expect((res.body as any).message).toBe("Invalid URL parameters");
|
|
231
|
-
});
|
|
232
|
-
});
|