@j3r3mcdev/oast-server 1.1.6 → 1.1.8
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/.github/workflows/ci.yml +29 -29
- package/.github/workflows/publish.yml +31 -31
- package/README.md +192 -192
- package/dist/core/router.d.ts +1 -0
- package/dist/core/router.js +3 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +16 -1
- package/jest.config.js +14 -14
- package/package.json +45 -45
- package/sadmin list shadows +9 -9
- package/src/api/controllers/__tests__/tasks.controller.test.ts +74 -74
- package/src/api/controllers/events.controller.ts +10 -10
- package/src/api/controllers/health.controller.ts +7 -7
- package/src/api/controllers/tasks.controller.ts +41 -41
- package/src/api/dto/__tests__/create-task.dto.test.ts +41 -41
- package/src/api/dto/__tests__/filter-tasks.dto.test.ts +35 -35
- package/src/api/dto/create-task.dto.ts +33 -33
- package/src/api/dto/filter-tasks.dto.ts +33 -33
- package/src/api/services/__tests__/events.service.test.ts +41 -41
- package/src/api/services/__tests__/tasks.service.test.ts +41 -41
- package/src/api/services/events.service.ts +17 -17
- package/src/api/services/tasks.service.ts +79 -79
- package/src/api/sse/events.stream.ts +90 -90
- package/src/bootstrap.ts +89 -89
- package/src/core/__tests__/core-router.test.ts +30 -30
- package/src/core/__tests__/core-server.test.ts +44 -44
- package/src/core/__tests__/event.normalizer.test.ts +56 -56
- package/src/core/__tests__/event.router.test.ts +89 -89
- package/src/core/__tests__/logger.test.ts +32 -32
- package/src/core/__tests__/storage-manager.test.ts +74 -74
- package/src/core/event.normalizer.ts +147 -147
- package/src/core/event.router.ts +13 -13
- package/src/core/http/__tests__/adapter-node.test.ts +52 -52
- package/src/core/http/__tests__/body-parser-multipart.test.ts +41 -41
- package/src/core/http/__tests__/body-parser-raw.test.ts +28 -28
- package/src/core/http/__tests__/body-parser-text.test.ts +28 -28
- package/src/core/http/__tests__/compile-path.test.ts +39 -39
- package/src/core/http/__tests__/middleware-pipeline.test.ts +51 -51
- package/src/core/http/__tests__/request.test.ts +34 -34
- package/src/core/http/__tests__/response.test.ts +35 -35
- package/src/core/http/__tests__/router-match.test.ts +171 -171
- package/src/core/http/adapter-node.ts +51 -51
- package/src/core/http/buildRequest.ts +18 -18
- package/src/core/http/compile-path.ts +32 -32
- package/src/core/http/errors.ts +37 -37
- package/src/core/http/http-server.ts +52 -52
- package/src/core/http/middleware.ts +160 -160
- package/src/core/http/request.ts +55 -55
- package/src/core/http/response.ts +93 -93
- package/src/core/http/router.ts +138 -138
- package/src/core/id-generator.ts +8 -8
- package/src/core/logger.ts +113 -113
- package/src/core/router.ts +44 -44
- package/src/core/server.ts +85 -85
- package/src/core/storage.ts +64 -64
- package/src/index.ts +14 -14
- package/src/listeners/api/__tests__/api.controller.test.ts +116 -116
- package/src/listeners/api/__tests__/api.extractor.test.ts +46 -46
- package/src/listeners/api/__tests__/api.listener.test.ts +82 -82
- package/src/listeners/api/__tests__/api.routes.test.ts +155 -155
- package/src/listeners/api/__tests__/api.sse.test.ts +105 -105
- package/src/listeners/api/api.controllers.ts +67 -67
- package/src/listeners/api/api.extractor.ts +43 -43
- package/src/listeners/api/api.listener.ts +50 -50
- package/src/listeners/api/api.routes.ts +76 -76
- package/src/listeners/api/api.sse.ts +38 -38
- package/src/listeners/dns/__tests__/dns.test.ts +118 -118
- package/src/listeners/dns/dns.extractor.ts +14 -14
- package/src/listeners/dns/dns.listener.ts +61 -61
- package/src/listeners/http/__tests__/http.extractor.test.ts +59 -59
- package/src/listeners/http/__tests__/http.listener.test.ts +133 -133
- package/src/listeners/http/http.extractor.ts +15 -15
- package/src/listeners/http/http.listener.ts +110 -110
- package/src/listeners/listener.interface.ts +4 -4
- package/src/listeners/smtp/__tests__/smtp.extractor.test.ts +69 -69
- package/src/listeners/smtp/__tests__/smtp.listener.test.ts +150 -150
- package/src/listeners/smtp/smtp.extractor.ts +18 -18
- package/src/listeners/smtp/smtp.listener.ts +60 -60
- package/src/listeners/ssrf/__tests__/ssrf.extractor.test.ts +41 -41
- package/src/listeners/ssrf/__tests__/ssrf.listener.test.ts +87 -87
- package/src/listeners/ssrf/ssrf.extractor.ts +14 -14
- package/src/listeners/ssrf/ssrf.listener.ts +37 -37
- package/src/listeners/tcp/tcp.extractor.ts +16 -16
- package/src/listeners/tcp/tcp.listener.ts +61 -61
- package/src/listeners/webhook/__tests__/webhook.extractor.test.ts +35 -35
- package/src/listeners/webhook/__tests__/webhook.listener.test.ts +122 -122
- package/src/listeners/webhook/webhook.extractor.ts +12 -12
- package/src/listeners/webhook/webhook.listener.ts +58 -58
- package/src/listeners/websocket/__tests__/websocket.extractor.test.ts +33 -33
- package/src/listeners/websocket/__tests__/websocket.listener.test.ts +90 -90
- package/src/listeners/websocket/websocket.extractor.ts +11 -11
- package/src/listeners/websocket/websocket.listener.ts +40 -40
- package/src/storage-adapters/adapters/__tests__/memory.storage.test.ts +75 -75
- package/src/storage-adapters/adapters/memory.storage.ts +64 -64
- package/src/storage-adapters/storage.interface.ts +26 -26
- package/src/types/event.types.ts +147 -147
- package/tsconfig.json +20 -21
|
@@ -1,155 +1,155 @@
|
|
|
1
|
-
import { handleApiRequest } from "../api.routes";
|
|
2
|
-
import { StorageManager } from "../../../core/storage";
|
|
3
|
-
import { ApiSse } from "../api.sse";
|
|
4
|
-
import { ApiController } from "../api.controllers";
|
|
5
|
-
import { Logger } from "../../../core/logger";
|
|
6
|
-
import { IncomingMessage, ServerResponse } from "http";
|
|
7
|
-
import { Socket } from "net";
|
|
8
|
-
import { describe, it, expect, beforeEach, jest } from "@jest/globals";
|
|
9
|
-
|
|
10
|
-
// Helpers pour mocker req/res
|
|
11
|
-
function mockReq(method: string, url: string): IncomingMessage {
|
|
12
|
-
const socket = new Socket();
|
|
13
|
-
const req = new IncomingMessage(socket);
|
|
14
|
-
req.method = method;
|
|
15
|
-
req.url = url;
|
|
16
|
-
req.headers = { host: "localhost" };
|
|
17
|
-
return req;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function mockRes(): ServerResponse {
|
|
21
|
-
const socket = new Socket();
|
|
22
|
-
const req = new IncomingMessage(socket);
|
|
23
|
-
const res = new ServerResponse(req);
|
|
24
|
-
|
|
25
|
-
jest.spyOn(res, "writeHead");
|
|
26
|
-
jest.spyOn(res, "end");
|
|
27
|
-
|
|
28
|
-
return res;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe("handleApiRequest", () => {
|
|
32
|
-
let storage: StorageManager;
|
|
33
|
-
let sse: ApiSse;
|
|
34
|
-
let logger: Logger;
|
|
35
|
-
|
|
36
|
-
beforeEach(() => {
|
|
37
|
-
storage = new StorageManager();
|
|
38
|
-
sse = new ApiSse(new Logger({ context: "SSETest" }));
|
|
39
|
-
logger = new Logger({ context: "ApiTest" });
|
|
40
|
-
|
|
41
|
-
jest.spyOn(ApiController, "listEvents").mockResolvedValue(undefined);
|
|
42
|
-
jest.spyOn(ApiController, "getEvent").mockResolvedValue(undefined);
|
|
43
|
-
jest.spyOn(ApiController, "deleteAll").mockResolvedValue(undefined);
|
|
44
|
-
jest.spyOn(ApiController, "deleteOne").mockResolvedValue(undefined);
|
|
45
|
-
jest.spyOn(ApiController, "stats").mockResolvedValue(undefined);
|
|
46
|
-
|
|
47
|
-
jest.spyOn(sse, "handle").mockReturnValue(undefined as any);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("route GET /events → ApiController.listEvents", async () => {
|
|
51
|
-
const req = mockReq("GET", "/events");
|
|
52
|
-
const res = mockRes();
|
|
53
|
-
|
|
54
|
-
await expect(
|
|
55
|
-
handleApiRequest(req, res, storage, sse, logger),
|
|
56
|
-
).resolves.toBeUndefined();
|
|
57
|
-
|
|
58
|
-
expect(ApiController.listEvents).toHaveBeenCalled();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("route GET /events/:id → ApiController.getEvent", async () => {
|
|
62
|
-
const req = mockReq("GET", "/events/123");
|
|
63
|
-
const res = mockRes();
|
|
64
|
-
|
|
65
|
-
await expect(
|
|
66
|
-
handleApiRequest(req, res, storage, sse, logger),
|
|
67
|
-
).resolves.toBeUndefined();
|
|
68
|
-
|
|
69
|
-
expect(ApiController.getEvent).toHaveBeenCalledWith("123", res, storage);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("route DELETE /events → ApiController.deleteAll", async () => {
|
|
73
|
-
const req = mockReq("DELETE", "/events");
|
|
74
|
-
const res = mockRes();
|
|
75
|
-
|
|
76
|
-
await expect(
|
|
77
|
-
handleApiRequest(req, res, storage, sse, logger),
|
|
78
|
-
).resolves.toBeUndefined();
|
|
79
|
-
|
|
80
|
-
expect(ApiController.deleteAll).toHaveBeenCalled();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("route DELETE /events/:id → ApiController.deleteOne", async () => {
|
|
84
|
-
const req = mockReq("DELETE", "/events/abc");
|
|
85
|
-
const res = mockRes();
|
|
86
|
-
|
|
87
|
-
await expect(
|
|
88
|
-
handleApiRequest(req, res, storage, sse, logger),
|
|
89
|
-
).resolves.toBeUndefined();
|
|
90
|
-
|
|
91
|
-
expect(ApiController.deleteOne).toHaveBeenCalledWith("abc", res, storage);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("route GET /stats → ApiController.stats", async () => {
|
|
95
|
-
const req = mockReq("GET", "/stats");
|
|
96
|
-
const res = mockRes();
|
|
97
|
-
|
|
98
|
-
await expect(
|
|
99
|
-
handleApiRequest(req, res, storage, sse, logger),
|
|
100
|
-
).resolves.toBeUndefined();
|
|
101
|
-
|
|
102
|
-
expect(ApiController.stats).toHaveBeenCalled();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("route GET /events/stream → SSE", async () => {
|
|
106
|
-
const req = mockReq("GET", "/events/stream");
|
|
107
|
-
const res = mockRes();
|
|
108
|
-
|
|
109
|
-
await expect(
|
|
110
|
-
handleApiRequest(req, res, storage, sse, logger),
|
|
111
|
-
).resolves.toBeUndefined();
|
|
112
|
-
|
|
113
|
-
expect(sse.handle).toHaveBeenCalledWith(res);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("404 → renvoie Not found", async () => {
|
|
117
|
-
const req = mockReq("GET", "/unknown");
|
|
118
|
-
const res = mockRes();
|
|
119
|
-
|
|
120
|
-
await expect(
|
|
121
|
-
handleApiRequest(req, res, storage, sse, logger),
|
|
122
|
-
).resolves.toBeUndefined();
|
|
123
|
-
|
|
124
|
-
expect(res.writeHead).toHaveBeenCalledWith(404, {
|
|
125
|
-
"Content-Type": "application/json",
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
const raw = String((res.end as jest.Mock).mock.calls[0][0]);
|
|
129
|
-
const body = JSON.parse(raw);
|
|
130
|
-
expect(body.success).toBe(false);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("500 → renvoie Internal Server Error", async () => {
|
|
134
|
-
const req = mockReq("GET", "/events");
|
|
135
|
-
|
|
136
|
-
jest
|
|
137
|
-
.spyOn(ApiController, "listEvents")
|
|
138
|
-
.mockRejectedValue(new Error("Boom"));
|
|
139
|
-
|
|
140
|
-
const res = mockRes();
|
|
141
|
-
|
|
142
|
-
await expect(
|
|
143
|
-
handleApiRequest(req, res, storage, sse, logger),
|
|
144
|
-
).resolves.toBeUndefined();
|
|
145
|
-
|
|
146
|
-
expect(res.writeHead).toHaveBeenCalledWith(500, {
|
|
147
|
-
"Content-Type": "application/json",
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
const raw = String((res.end as jest.Mock).mock.calls[0][0]);
|
|
151
|
-
const body = JSON.parse(raw);
|
|
152
|
-
expect(body.success).toBe(false);
|
|
153
|
-
expect(body.error).toBe("Boom");
|
|
154
|
-
});
|
|
155
|
-
});
|
|
1
|
+
import { handleApiRequest } from "../api.routes";
|
|
2
|
+
import { StorageManager } from "../../../core/storage";
|
|
3
|
+
import { ApiSse } from "../api.sse";
|
|
4
|
+
import { ApiController } from "../api.controllers";
|
|
5
|
+
import { Logger } from "../../../core/logger";
|
|
6
|
+
import { IncomingMessage, ServerResponse } from "http";
|
|
7
|
+
import { Socket } from "net";
|
|
8
|
+
import { describe, it, expect, beforeEach, jest } from "@jest/globals";
|
|
9
|
+
|
|
10
|
+
// Helpers pour mocker req/res
|
|
11
|
+
function mockReq(method: string, url: string): IncomingMessage {
|
|
12
|
+
const socket = new Socket();
|
|
13
|
+
const req = new IncomingMessage(socket);
|
|
14
|
+
req.method = method;
|
|
15
|
+
req.url = url;
|
|
16
|
+
req.headers = { host: "localhost" };
|
|
17
|
+
return req;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function mockRes(): ServerResponse {
|
|
21
|
+
const socket = new Socket();
|
|
22
|
+
const req = new IncomingMessage(socket);
|
|
23
|
+
const res = new ServerResponse(req);
|
|
24
|
+
|
|
25
|
+
jest.spyOn(res, "writeHead");
|
|
26
|
+
jest.spyOn(res, "end");
|
|
27
|
+
|
|
28
|
+
return res;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("handleApiRequest", () => {
|
|
32
|
+
let storage: StorageManager;
|
|
33
|
+
let sse: ApiSse;
|
|
34
|
+
let logger: Logger;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
storage = new StorageManager();
|
|
38
|
+
sse = new ApiSse(new Logger({ context: "SSETest" }));
|
|
39
|
+
logger = new Logger({ context: "ApiTest" });
|
|
40
|
+
|
|
41
|
+
jest.spyOn(ApiController, "listEvents").mockResolvedValue(undefined);
|
|
42
|
+
jest.spyOn(ApiController, "getEvent").mockResolvedValue(undefined);
|
|
43
|
+
jest.spyOn(ApiController, "deleteAll").mockResolvedValue(undefined);
|
|
44
|
+
jest.spyOn(ApiController, "deleteOne").mockResolvedValue(undefined);
|
|
45
|
+
jest.spyOn(ApiController, "stats").mockResolvedValue(undefined);
|
|
46
|
+
|
|
47
|
+
jest.spyOn(sse, "handle").mockReturnValue(undefined as any);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("route GET /events → ApiController.listEvents", async () => {
|
|
51
|
+
const req = mockReq("GET", "/events");
|
|
52
|
+
const res = mockRes();
|
|
53
|
+
|
|
54
|
+
await expect(
|
|
55
|
+
handleApiRequest(req, res, storage, sse, logger),
|
|
56
|
+
).resolves.toBeUndefined();
|
|
57
|
+
|
|
58
|
+
expect(ApiController.listEvents).toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("route GET /events/:id → ApiController.getEvent", async () => {
|
|
62
|
+
const req = mockReq("GET", "/events/123");
|
|
63
|
+
const res = mockRes();
|
|
64
|
+
|
|
65
|
+
await expect(
|
|
66
|
+
handleApiRequest(req, res, storage, sse, logger),
|
|
67
|
+
).resolves.toBeUndefined();
|
|
68
|
+
|
|
69
|
+
expect(ApiController.getEvent).toHaveBeenCalledWith("123", res, storage);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("route DELETE /events → ApiController.deleteAll", async () => {
|
|
73
|
+
const req = mockReq("DELETE", "/events");
|
|
74
|
+
const res = mockRes();
|
|
75
|
+
|
|
76
|
+
await expect(
|
|
77
|
+
handleApiRequest(req, res, storage, sse, logger),
|
|
78
|
+
).resolves.toBeUndefined();
|
|
79
|
+
|
|
80
|
+
expect(ApiController.deleteAll).toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("route DELETE /events/:id → ApiController.deleteOne", async () => {
|
|
84
|
+
const req = mockReq("DELETE", "/events/abc");
|
|
85
|
+
const res = mockRes();
|
|
86
|
+
|
|
87
|
+
await expect(
|
|
88
|
+
handleApiRequest(req, res, storage, sse, logger),
|
|
89
|
+
).resolves.toBeUndefined();
|
|
90
|
+
|
|
91
|
+
expect(ApiController.deleteOne).toHaveBeenCalledWith("abc", res, storage);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("route GET /stats → ApiController.stats", async () => {
|
|
95
|
+
const req = mockReq("GET", "/stats");
|
|
96
|
+
const res = mockRes();
|
|
97
|
+
|
|
98
|
+
await expect(
|
|
99
|
+
handleApiRequest(req, res, storage, sse, logger),
|
|
100
|
+
).resolves.toBeUndefined();
|
|
101
|
+
|
|
102
|
+
expect(ApiController.stats).toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("route GET /events/stream → SSE", async () => {
|
|
106
|
+
const req = mockReq("GET", "/events/stream");
|
|
107
|
+
const res = mockRes();
|
|
108
|
+
|
|
109
|
+
await expect(
|
|
110
|
+
handleApiRequest(req, res, storage, sse, logger),
|
|
111
|
+
).resolves.toBeUndefined();
|
|
112
|
+
|
|
113
|
+
expect(sse.handle).toHaveBeenCalledWith(res);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("404 → renvoie Not found", async () => {
|
|
117
|
+
const req = mockReq("GET", "/unknown");
|
|
118
|
+
const res = mockRes();
|
|
119
|
+
|
|
120
|
+
await expect(
|
|
121
|
+
handleApiRequest(req, res, storage, sse, logger),
|
|
122
|
+
).resolves.toBeUndefined();
|
|
123
|
+
|
|
124
|
+
expect(res.writeHead).toHaveBeenCalledWith(404, {
|
|
125
|
+
"Content-Type": "application/json",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const raw = String((res.end as jest.Mock).mock.calls[0][0]);
|
|
129
|
+
const body = JSON.parse(raw);
|
|
130
|
+
expect(body.success).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("500 → renvoie Internal Server Error", async () => {
|
|
134
|
+
const req = mockReq("GET", "/events");
|
|
135
|
+
|
|
136
|
+
jest
|
|
137
|
+
.spyOn(ApiController, "listEvents")
|
|
138
|
+
.mockRejectedValue(new Error("Boom"));
|
|
139
|
+
|
|
140
|
+
const res = mockRes();
|
|
141
|
+
|
|
142
|
+
await expect(
|
|
143
|
+
handleApiRequest(req, res, storage, sse, logger),
|
|
144
|
+
).resolves.toBeUndefined();
|
|
145
|
+
|
|
146
|
+
expect(res.writeHead).toHaveBeenCalledWith(500, {
|
|
147
|
+
"Content-Type": "application/json",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const raw = String((res.end as jest.Mock).mock.calls[0][0]);
|
|
151
|
+
const body = JSON.parse(raw);
|
|
152
|
+
expect(body.success).toBe(false);
|
|
153
|
+
expect(body.error).toBe("Boom");
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
import { ApiSse } from "../api.sse";
|
|
2
|
-
import { Logger } from "../../../core/logger";
|
|
3
|
-
import { ServerResponse, IncomingMessage } from "http";
|
|
4
|
-
import { Socket } from "net";
|
|
5
|
-
import { describe, it, expect, jest } from "@jest/globals";
|
|
6
|
-
|
|
7
|
-
function mockRes(): ServerResponse {
|
|
8
|
-
const socket = new Socket();
|
|
9
|
-
const req = new IncomingMessage(socket);
|
|
10
|
-
const res = new ServerResponse(req);
|
|
11
|
-
return res;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
describe("ApiSse", () => {
|
|
15
|
-
it("connecte un client SSE", () => {
|
|
16
|
-
const sse = new ApiSse(new Logger({ context: "Test" }));
|
|
17
|
-
const res = mockRes();
|
|
18
|
-
|
|
19
|
-
// On espionne writeHead et write
|
|
20
|
-
const writeHeadSpy = jest.spyOn(res, "writeHead");
|
|
21
|
-
const writeSpy = jest.spyOn(res, "write");
|
|
22
|
-
|
|
23
|
-
sse.handle(res);
|
|
24
|
-
|
|
25
|
-
expect(writeHeadSpy).toHaveBeenCalledWith(200, {
|
|
26
|
-
"Content-Type": "text/event-stream",
|
|
27
|
-
"Cache-Control": "no-cache",
|
|
28
|
-
Connection: "keep-alive",
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
expect(writeSpy).toHaveBeenCalled();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("broadcast un event à tous les clients", () => {
|
|
35
|
-
const sse = new ApiSse(new Logger({ context: "Test" }));
|
|
36
|
-
|
|
37
|
-
const res1 = mockRes();
|
|
38
|
-
const res2 = mockRes();
|
|
39
|
-
|
|
40
|
-
const spy1 = jest.spyOn(res1, "write");
|
|
41
|
-
const spy2 = jest.spyOn(res2, "write");
|
|
42
|
-
|
|
43
|
-
sse.handle(res1);
|
|
44
|
-
sse.handle(res2);
|
|
45
|
-
|
|
46
|
-
sse.broadcast({ id: "123", type: "http" });
|
|
47
|
-
|
|
48
|
-
expect(spy1).toHaveBeenCalled();
|
|
49
|
-
expect(spy2).toHaveBeenCalled();
|
|
50
|
-
|
|
51
|
-
const payload = spy1.mock.calls[spy1.mock.calls.length - 1][0];
|
|
52
|
-
expect(payload).toContain("event: event");
|
|
53
|
-
expect(payload).toContain('"id":"123"');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("supprime un client à la fermeture", () => {
|
|
57
|
-
const sse = new ApiSse(new Logger({ context: "Test" }));
|
|
58
|
-
const res = mockRes();
|
|
59
|
-
|
|
60
|
-
sse.handle(res);
|
|
61
|
-
|
|
62
|
-
expect((sse as any).clients.size).toBe(1);
|
|
63
|
-
|
|
64
|
-
res.emit("close");
|
|
65
|
-
|
|
66
|
-
expect((sse as any).clients.size).toBe(0);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("envoie un event SSE avec le bon format", () => {
|
|
70
|
-
const sse = new ApiSse(new Logger({ context: "Test" }));
|
|
71
|
-
const res = mockRes();
|
|
72
|
-
|
|
73
|
-
const spy = jest.spyOn(res, "write");
|
|
74
|
-
|
|
75
|
-
sse.handle(res);
|
|
76
|
-
sse.broadcast({ id: "abc", type: "dns" });
|
|
77
|
-
|
|
78
|
-
const payload = spy.mock.calls[spy.mock.calls.length - 1][0];
|
|
79
|
-
|
|
80
|
-
expect(payload).toContain("event: event");
|
|
81
|
-
expect(payload).toContain("data:");
|
|
82
|
-
expect(payload).toContain('"id":"abc"');
|
|
83
|
-
expect(payload.endsWith("\n\n")).toBe(true);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("broadcast ne plante pas s'il n'y a aucun client", () => {
|
|
87
|
-
const sse = new ApiSse(new Logger({ context: "Test" }));
|
|
88
|
-
|
|
89
|
-
expect(() => {
|
|
90
|
-
sse.broadcast({ id: "x", type: "http" });
|
|
91
|
-
}).not.toThrow();
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("ne duplique pas un client déjà enregistré", () => {
|
|
95
|
-
const sse = new ApiSse(new Logger({ context: "Test" }));
|
|
96
|
-
const res = mockRes();
|
|
97
|
-
|
|
98
|
-
sse.handle(res);
|
|
99
|
-
|
|
100
|
-
// simulate duplicate registration attempt
|
|
101
|
-
(sse as any).clients.add(res);
|
|
102
|
-
|
|
103
|
-
expect((sse as any).clients.size).toBe(1);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
1
|
+
import { ApiSse } from "../api.sse";
|
|
2
|
+
import { Logger } from "../../../core/logger";
|
|
3
|
+
import { ServerResponse, IncomingMessage } from "http";
|
|
4
|
+
import { Socket } from "net";
|
|
5
|
+
import { describe, it, expect, jest } from "@jest/globals";
|
|
6
|
+
|
|
7
|
+
function mockRes(): ServerResponse {
|
|
8
|
+
const socket = new Socket();
|
|
9
|
+
const req = new IncomingMessage(socket);
|
|
10
|
+
const res = new ServerResponse(req);
|
|
11
|
+
return res;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("ApiSse", () => {
|
|
15
|
+
it("connecte un client SSE", () => {
|
|
16
|
+
const sse = new ApiSse(new Logger({ context: "Test" }));
|
|
17
|
+
const res = mockRes();
|
|
18
|
+
|
|
19
|
+
// On espionne writeHead et write
|
|
20
|
+
const writeHeadSpy = jest.spyOn(res, "writeHead");
|
|
21
|
+
const writeSpy = jest.spyOn(res, "write");
|
|
22
|
+
|
|
23
|
+
sse.handle(res);
|
|
24
|
+
|
|
25
|
+
expect(writeHeadSpy).toHaveBeenCalledWith(200, {
|
|
26
|
+
"Content-Type": "text/event-stream",
|
|
27
|
+
"Cache-Control": "no-cache",
|
|
28
|
+
Connection: "keep-alive",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(writeSpy).toHaveBeenCalled();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("broadcast un event à tous les clients", () => {
|
|
35
|
+
const sse = new ApiSse(new Logger({ context: "Test" }));
|
|
36
|
+
|
|
37
|
+
const res1 = mockRes();
|
|
38
|
+
const res2 = mockRes();
|
|
39
|
+
|
|
40
|
+
const spy1 = jest.spyOn(res1, "write");
|
|
41
|
+
const spy2 = jest.spyOn(res2, "write");
|
|
42
|
+
|
|
43
|
+
sse.handle(res1);
|
|
44
|
+
sse.handle(res2);
|
|
45
|
+
|
|
46
|
+
sse.broadcast({ id: "123", type: "http" });
|
|
47
|
+
|
|
48
|
+
expect(spy1).toHaveBeenCalled();
|
|
49
|
+
expect(spy2).toHaveBeenCalled();
|
|
50
|
+
|
|
51
|
+
const payload = spy1.mock.calls[spy1.mock.calls.length - 1][0];
|
|
52
|
+
expect(payload).toContain("event: event");
|
|
53
|
+
expect(payload).toContain('"id":"123"');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("supprime un client à la fermeture", () => {
|
|
57
|
+
const sse = new ApiSse(new Logger({ context: "Test" }));
|
|
58
|
+
const res = mockRes();
|
|
59
|
+
|
|
60
|
+
sse.handle(res);
|
|
61
|
+
|
|
62
|
+
expect((sse as any).clients.size).toBe(1);
|
|
63
|
+
|
|
64
|
+
res.emit("close");
|
|
65
|
+
|
|
66
|
+
expect((sse as any).clients.size).toBe(0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("envoie un event SSE avec le bon format", () => {
|
|
70
|
+
const sse = new ApiSse(new Logger({ context: "Test" }));
|
|
71
|
+
const res = mockRes();
|
|
72
|
+
|
|
73
|
+
const spy = jest.spyOn(res, "write");
|
|
74
|
+
|
|
75
|
+
sse.handle(res);
|
|
76
|
+
sse.broadcast({ id: "abc", type: "dns" });
|
|
77
|
+
|
|
78
|
+
const payload = spy.mock.calls[spy.mock.calls.length - 1][0];
|
|
79
|
+
|
|
80
|
+
expect(payload).toContain("event: event");
|
|
81
|
+
expect(payload).toContain("data:");
|
|
82
|
+
expect(payload).toContain('"id":"abc"');
|
|
83
|
+
expect(payload.endsWith("\n\n")).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("broadcast ne plante pas s'il n'y a aucun client", () => {
|
|
87
|
+
const sse = new ApiSse(new Logger({ context: "Test" }));
|
|
88
|
+
|
|
89
|
+
expect(() => {
|
|
90
|
+
sse.broadcast({ id: "x", type: "http" });
|
|
91
|
+
}).not.toThrow();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("ne duplique pas un client déjà enregistré", () => {
|
|
95
|
+
const sse = new ApiSse(new Logger({ context: "Test" }));
|
|
96
|
+
const res = mockRes();
|
|
97
|
+
|
|
98
|
+
sse.handle(res);
|
|
99
|
+
|
|
100
|
+
// simulate duplicate registration attempt
|
|
101
|
+
(sse as any).clients.add(res);
|
|
102
|
+
|
|
103
|
+
expect((sse as any).clients.size).toBe(1);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
import { ServerResponse } from "http";
|
|
2
|
-
import { StorageManager } from "../../core/storage";
|
|
3
|
-
|
|
4
|
-
export class ApiController {
|
|
5
|
-
static async listEvents(
|
|
6
|
-
url: URL,
|
|
7
|
-
res: ServerResponse,
|
|
8
|
-
storage: StorageManager,
|
|
9
|
-
): Promise<void> {
|
|
10
|
-
const type = url.searchParams.get("type") ?? undefined;
|
|
11
|
-
const page = Number(url.searchParams.get("page") ?? 1);
|
|
12
|
-
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
13
|
-
|
|
14
|
-
const events = await storage.listEvents({ type, page, limit });
|
|
15
|
-
|
|
16
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
17
|
-
res.end(JSON.stringify({ success: true, events }));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
static async getEvent(
|
|
21
|
-
id: string,
|
|
22
|
-
res: ServerResponse,
|
|
23
|
-
storage: StorageManager,
|
|
24
|
-
): Promise<void> {
|
|
25
|
-
const event = await storage.getEvent(id);
|
|
26
|
-
|
|
27
|
-
if (!event) {
|
|
28
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
29
|
-
res.end(JSON.stringify({ success: false, error: "Not found" }));
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
34
|
-
res.end(JSON.stringify({ success: true, event }));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
static async deleteAll(
|
|
38
|
-
res: ServerResponse,
|
|
39
|
-
storage: StorageManager,
|
|
40
|
-
): Promise<void> {
|
|
41
|
-
await storage.clearEvents();
|
|
42
|
-
|
|
43
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
44
|
-
res.end(JSON.stringify({ success: true }));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
static async deleteOne(
|
|
48
|
-
id: string,
|
|
49
|
-
res: ServerResponse,
|
|
50
|
-
storage: StorageManager,
|
|
51
|
-
): Promise<void> {
|
|
52
|
-
const ok = await storage.deleteEvent(id);
|
|
53
|
-
|
|
54
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
55
|
-
res.end(JSON.stringify({ success: ok }));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
static async stats(
|
|
59
|
-
res: ServerResponse,
|
|
60
|
-
storage: StorageManager,
|
|
61
|
-
): Promise<void> {
|
|
62
|
-
const stats = await storage.getStats();
|
|
63
|
-
|
|
64
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
65
|
-
res.end(JSON.stringify({ success: true, stats }));
|
|
66
|
-
}
|
|
67
|
-
}
|
|
1
|
+
import { ServerResponse } from "http";
|
|
2
|
+
import { StorageManager } from "../../core/storage";
|
|
3
|
+
|
|
4
|
+
export class ApiController {
|
|
5
|
+
static async listEvents(
|
|
6
|
+
url: URL,
|
|
7
|
+
res: ServerResponse,
|
|
8
|
+
storage: StorageManager,
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
const type = url.searchParams.get("type") ?? undefined;
|
|
11
|
+
const page = Number(url.searchParams.get("page") ?? 1);
|
|
12
|
+
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
13
|
+
|
|
14
|
+
const events = await storage.listEvents({ type, page, limit });
|
|
15
|
+
|
|
16
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
17
|
+
res.end(JSON.stringify({ success: true, events }));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static async getEvent(
|
|
21
|
+
id: string,
|
|
22
|
+
res: ServerResponse,
|
|
23
|
+
storage: StorageManager,
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
const event = await storage.getEvent(id);
|
|
26
|
+
|
|
27
|
+
if (!event) {
|
|
28
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
29
|
+
res.end(JSON.stringify({ success: false, error: "Not found" }));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
34
|
+
res.end(JSON.stringify({ success: true, event }));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static async deleteAll(
|
|
38
|
+
res: ServerResponse,
|
|
39
|
+
storage: StorageManager,
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
await storage.clearEvents();
|
|
42
|
+
|
|
43
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
44
|
+
res.end(JSON.stringify({ success: true }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static async deleteOne(
|
|
48
|
+
id: string,
|
|
49
|
+
res: ServerResponse,
|
|
50
|
+
storage: StorageManager,
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
const ok = await storage.deleteEvent(id);
|
|
53
|
+
|
|
54
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
55
|
+
res.end(JSON.stringify({ success: ok }));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static async stats(
|
|
59
|
+
res: ServerResponse,
|
|
60
|
+
storage: StorageManager,
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
const stats = await storage.getStats();
|
|
63
|
+
|
|
64
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
65
|
+
res.end(JSON.stringify({ success: true, stats }));
|
|
66
|
+
}
|
|
67
|
+
}
|