@agentick/gateway 0.0.1
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/LICENSE +21 -0
- package/README.md +477 -0
- package/dist/agent-registry.d.ts +51 -0
- package/dist/agent-registry.d.ts.map +1 -0
- package/dist/agent-registry.js +78 -0
- package/dist/agent-registry.js.map +1 -0
- package/dist/app-registry.d.ts +51 -0
- package/dist/app-registry.d.ts.map +1 -0
- package/dist/app-registry.js +78 -0
- package/dist/app-registry.js.map +1 -0
- package/dist/bin.d.ts +8 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +37 -0
- package/dist/bin.js.map +1 -0
- package/dist/gateway.d.ts +165 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +1339 -0
- package/dist/gateway.js.map +1 -0
- package/dist/http-transport.d.ts +65 -0
- package/dist/http-transport.d.ts.map +1 -0
- package/dist/http-transport.js +517 -0
- package/dist/http-transport.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +162 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +16 -0
- package/dist/protocol.js.map +1 -0
- package/dist/session-manager.d.ts +101 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +208 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/testing.d.ts +92 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +129 -0
- package/dist/testing.js.map +1 -0
- package/dist/transport-protocol.d.ts +162 -0
- package/dist/transport-protocol.d.ts.map +1 -0
- package/dist/transport-protocol.js +16 -0
- package/dist/transport-protocol.js.map +1 -0
- package/dist/transport.d.ts +115 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +56 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +314 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +37 -0
- package/dist/types.js.map +1 -0
- package/dist/websocket-server.d.ts +87 -0
- package/dist/websocket-server.d.ts.map +1 -0
- package/dist/websocket-server.js +245 -0
- package/dist/websocket-server.js.map +1 -0
- package/dist/ws-transport.d.ts +17 -0
- package/dist/ws-transport.d.ts.map +1 -0
- package/dist/ws-transport.js +174 -0
- package/dist/ws-transport.js.map +1 -0
- package/package.json +51 -0
- package/src/__tests__/custom-methods.spec.ts +220 -0
- package/src/__tests__/gateway-methods.spec.ts +262 -0
- package/src/__tests__/gateway.spec.ts +404 -0
- package/src/__tests__/guards.spec.ts +235 -0
- package/src/__tests__/protocol.spec.ts +58 -0
- package/src/__tests__/session-manager.spec.ts +220 -0
- package/src/__tests__/ws-transport.spec.ts +246 -0
- package/src/app-registry.ts +103 -0
- package/src/bin.ts +38 -0
- package/src/gateway.ts +1712 -0
- package/src/http-transport.ts +623 -0
- package/src/index.ts +94 -0
- package/src/session-manager.ts +272 -0
- package/src/testing.ts +236 -0
- package/src/transport-protocol.ts +249 -0
- package/src/transport.ts +191 -0
- package/src/types.ts +392 -0
- package/src/websocket-server.ts +303 -0
- package/src/ws-transport.ts +205 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway Method Dispatch Tests
|
|
3
|
+
*
|
|
4
|
+
* Integration tests for Gateway custom method dispatch, including:
|
|
5
|
+
* - Method initialization and path resolution
|
|
6
|
+
* - Role and custom guard middleware
|
|
7
|
+
* - Schema validation
|
|
8
|
+
* - Auth hydrateUser hook
|
|
9
|
+
* - Express middleware
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { Gateway, createGateway, type ExpressRequestHandler } from "../gateway.js";
|
|
15
|
+
import { method, type AuthResult, type AuthConfig, type GatewayConfig } from "../types.js";
|
|
16
|
+
import { Context, type UserContext } from "@agentick/kernel";
|
|
17
|
+
import { createMockApp } from "@agentick/core/testing";
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Gateway initialization tests
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
describe("Gateway method initialization", () => {
|
|
24
|
+
it("should initialize simple function methods", () => {
|
|
25
|
+
const gateway = createGateway({
|
|
26
|
+
apps: { test: createMockApp() as any },
|
|
27
|
+
defaultApp: "test",
|
|
28
|
+
methods: {
|
|
29
|
+
ping: async () => ({ pong: true }),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Gateway should be created without errors
|
|
34
|
+
expect(gateway).toBeInstanceOf(Gateway);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should initialize method() definition methods", () => {
|
|
38
|
+
const gateway = createGateway({
|
|
39
|
+
apps: { test: createMockApp() as any },
|
|
40
|
+
defaultApp: "test",
|
|
41
|
+
methods: {
|
|
42
|
+
create: method({
|
|
43
|
+
schema: z.object({ title: z.string() }),
|
|
44
|
+
handler: async (params) => ({ id: "1", title: params.title }),
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(gateway).toBeInstanceOf(Gateway);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should initialize nested namespace methods", () => {
|
|
53
|
+
const gateway = createGateway({
|
|
54
|
+
apps: { test: createMockApp() as any },
|
|
55
|
+
defaultApp: "test",
|
|
56
|
+
methods: {
|
|
57
|
+
tasks: {
|
|
58
|
+
list: async () => [],
|
|
59
|
+
create: method({
|
|
60
|
+
schema: z.object({ title: z.string() }),
|
|
61
|
+
handler: async (params) => ({ id: "1", title: params.title }),
|
|
62
|
+
}),
|
|
63
|
+
admin: {
|
|
64
|
+
archive: method({
|
|
65
|
+
roles: ["admin"],
|
|
66
|
+
handler: async () => ({ archived: true }),
|
|
67
|
+
}),
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(gateway).toBeInstanceOf(Gateway);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should handle deeply nested namespaces", () => {
|
|
77
|
+
const gateway = createGateway({
|
|
78
|
+
apps: { test: createMockApp() as any },
|
|
79
|
+
defaultApp: "test",
|
|
80
|
+
methods: {
|
|
81
|
+
level1: {
|
|
82
|
+
level2: {
|
|
83
|
+
level3: {
|
|
84
|
+
level4: {
|
|
85
|
+
deepMethod: async () => ({ deep: true }),
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(gateway).toBeInstanceOf(Gateway);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// Auth configuration tests
|
|
99
|
+
// ============================================================================
|
|
100
|
+
|
|
101
|
+
describe("Auth configuration", () => {
|
|
102
|
+
it("should support auth type none", () => {
|
|
103
|
+
const gateway = createGateway({
|
|
104
|
+
apps: { test: createMockApp() as any },
|
|
105
|
+
defaultApp: "test",
|
|
106
|
+
auth: { type: "none" },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(gateway).toBeInstanceOf(Gateway);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should support auth type token", () => {
|
|
113
|
+
const gateway = createGateway({
|
|
114
|
+
apps: { test: createMockApp() as any },
|
|
115
|
+
defaultApp: "test",
|
|
116
|
+
auth: { type: "token", token: "secret-token" },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(gateway).toBeInstanceOf(Gateway);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should support auth type custom with validate function", () => {
|
|
123
|
+
const gateway = createGateway({
|
|
124
|
+
apps: { test: createMockApp() as any },
|
|
125
|
+
defaultApp: "test",
|
|
126
|
+
auth: {
|
|
127
|
+
type: "custom",
|
|
128
|
+
validate: async (token) => ({
|
|
129
|
+
valid: token === "valid-token",
|
|
130
|
+
user: { id: "user-1" },
|
|
131
|
+
}),
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(gateway).toBeInstanceOf(Gateway);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should support hydrateUser hook on any auth type", () => {
|
|
139
|
+
const hydrateUser = vi.fn(
|
|
140
|
+
async (authResult: AuthResult): Promise<UserContext> => ({
|
|
141
|
+
id: authResult.user?.id ?? "unknown",
|
|
142
|
+
roles: ["user", "premium"],
|
|
143
|
+
email: "test@example.com",
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// With token auth
|
|
148
|
+
const gateway1 = createGateway({
|
|
149
|
+
apps: { test: createMockApp() as any },
|
|
150
|
+
defaultApp: "test",
|
|
151
|
+
auth: {
|
|
152
|
+
type: "token",
|
|
153
|
+
token: "secret",
|
|
154
|
+
hydrateUser,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
expect(gateway1).toBeInstanceOf(Gateway);
|
|
158
|
+
|
|
159
|
+
// With custom auth
|
|
160
|
+
const gateway2 = createGateway({
|
|
161
|
+
apps: { test: createMockApp() as any },
|
|
162
|
+
defaultApp: "test",
|
|
163
|
+
auth: {
|
|
164
|
+
type: "custom",
|
|
165
|
+
validate: async () => ({ valid: true, user: { id: "1" } }),
|
|
166
|
+
hydrateUser,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
expect(gateway2).toBeInstanceOf(Gateway);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// Gateway status and lifecycle tests
|
|
175
|
+
// ============================================================================
|
|
176
|
+
|
|
177
|
+
describe("Gateway status", () => {
|
|
178
|
+
it("should return correct status before start", () => {
|
|
179
|
+
const gateway = createGateway({
|
|
180
|
+
apps: { test: createMockApp() as any },
|
|
181
|
+
defaultApp: "test",
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(gateway.running).toBe(false);
|
|
185
|
+
expect(gateway.status.uptime).toBe(0);
|
|
186
|
+
expect(gateway.status.clients).toBe(0);
|
|
187
|
+
expect(gateway.status.sessions).toBe(0);
|
|
188
|
+
expect(gateway.status.apps).toContain("test");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should have a unique id when provided", () => {
|
|
192
|
+
const gateway1 = createGateway({
|
|
193
|
+
apps: { test: createMockApp() as any },
|
|
194
|
+
defaultApp: "test",
|
|
195
|
+
id: "gateway-1",
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const gateway2 = createGateway({
|
|
199
|
+
apps: { test: createMockApp() as any },
|
|
200
|
+
defaultApp: "test",
|
|
201
|
+
id: "gateway-2",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(gateway1.id).toBe("gateway-1");
|
|
205
|
+
expect(gateway2.id).toBe("gateway-2");
|
|
206
|
+
expect(gateway1.id).not.toBe(gateway2.id);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("should auto-generate id when not provided", () => {
|
|
210
|
+
const gateway = createGateway({
|
|
211
|
+
apps: { test: createMockApp() as any },
|
|
212
|
+
defaultApp: "test",
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(gateway.id).toBeTruthy();
|
|
216
|
+
expect(gateway.id).toMatch(/^gw-/);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should use provided id", () => {
|
|
220
|
+
const gateway = createGateway({
|
|
221
|
+
apps: { test: createMockApp() as any },
|
|
222
|
+
defaultApp: "test",
|
|
223
|
+
id: "custom-gateway-id",
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(gateway.id).toBe("custom-gateway-id");
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// ============================================================================
|
|
231
|
+
// Config validation tests
|
|
232
|
+
// ============================================================================
|
|
233
|
+
|
|
234
|
+
describe("Gateway config validation", () => {
|
|
235
|
+
it("should throw if no apps provided", () => {
|
|
236
|
+
expect(() =>
|
|
237
|
+
createGateway({
|
|
238
|
+
apps: {},
|
|
239
|
+
defaultApp: "test",
|
|
240
|
+
}),
|
|
241
|
+
).toThrow("At least one app is required");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("should throw if no defaultApp provided", () => {
|
|
245
|
+
expect(() =>
|
|
246
|
+
createGateway({
|
|
247
|
+
apps: { test: createMockApp() as any },
|
|
248
|
+
defaultApp: "",
|
|
249
|
+
} as any),
|
|
250
|
+
).toThrow("defaultApp is required");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("should use default port and host", () => {
|
|
254
|
+
const gateway = createGateway({
|
|
255
|
+
apps: { test: createMockApp() as any },
|
|
256
|
+
defaultApp: "test",
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Gateway should be created with defaults
|
|
260
|
+
expect(gateway).toBeInstanceOf(Gateway);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway Integration Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
6
|
+
import { Gateway, createGateway } from "../gateway.js";
|
|
7
|
+
import { createMockApp as createCoreMockApp, type MockApp } from "@agentick/core/testing";
|
|
8
|
+
import WebSocket from "ws";
|
|
9
|
+
|
|
10
|
+
describe("Gateway", () => {
|
|
11
|
+
const TEST_PORT = 19998;
|
|
12
|
+
const TEST_HOST = "127.0.0.1";
|
|
13
|
+
let gateway: Gateway;
|
|
14
|
+
let chatApp: MockApp;
|
|
15
|
+
let researchApp: MockApp;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
chatApp = createCoreMockApp();
|
|
19
|
+
researchApp = createCoreMockApp();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
if (gateway?.running) {
|
|
24
|
+
await gateway.stop();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("createGateway", () => {
|
|
29
|
+
it("creates gateway with config", () => {
|
|
30
|
+
gateway = createGateway({
|
|
31
|
+
port: TEST_PORT,
|
|
32
|
+
host: TEST_HOST,
|
|
33
|
+
apps: { chat: chatApp },
|
|
34
|
+
defaultApp: "chat",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(gateway).toBeInstanceOf(Gateway);
|
|
38
|
+
expect(gateway.id).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("throws if no apps provided", () => {
|
|
42
|
+
expect(() =>
|
|
43
|
+
createGateway({
|
|
44
|
+
apps: {},
|
|
45
|
+
defaultApp: "chat",
|
|
46
|
+
}),
|
|
47
|
+
).toThrow("At least one app is required");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("throws if default app not found", () => {
|
|
51
|
+
expect(() =>
|
|
52
|
+
createGateway({
|
|
53
|
+
apps: { chat: chatApp },
|
|
54
|
+
defaultApp: "research",
|
|
55
|
+
}),
|
|
56
|
+
).toThrow('Default app "research" not found');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("start/stop", () => {
|
|
61
|
+
it("starts and stops gateway", async () => {
|
|
62
|
+
gateway = createGateway({
|
|
63
|
+
port: TEST_PORT,
|
|
64
|
+
host: TEST_HOST,
|
|
65
|
+
apps: { chat: chatApp },
|
|
66
|
+
defaultApp: "chat",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await gateway.start();
|
|
70
|
+
expect(gateway.running).toBe(true);
|
|
71
|
+
|
|
72
|
+
await gateway.stop();
|
|
73
|
+
expect(gateway.running).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("emits started event", async () => {
|
|
77
|
+
gateway = createGateway({
|
|
78
|
+
port: TEST_PORT,
|
|
79
|
+
host: TEST_HOST,
|
|
80
|
+
apps: { chat: chatApp },
|
|
81
|
+
defaultApp: "chat",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const startedPromise = new Promise<{ port: number; host: string }>((resolve) => {
|
|
85
|
+
gateway.on("started", resolve);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await gateway.start();
|
|
89
|
+
const event = await startedPromise;
|
|
90
|
+
|
|
91
|
+
expect(event.port).toBe(TEST_PORT);
|
|
92
|
+
expect(event.host).toBe(TEST_HOST);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("throws if started twice", async () => {
|
|
96
|
+
gateway = createGateway({
|
|
97
|
+
port: TEST_PORT,
|
|
98
|
+
host: TEST_HOST,
|
|
99
|
+
apps: { chat: chatApp },
|
|
100
|
+
defaultApp: "chat",
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await gateway.start();
|
|
104
|
+
await expect(gateway.start()).rejects.toThrow("already running");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("status", () => {
|
|
109
|
+
it("reports gateway status", async () => {
|
|
110
|
+
gateway = createGateway({
|
|
111
|
+
port: TEST_PORT,
|
|
112
|
+
host: TEST_HOST,
|
|
113
|
+
apps: { chat: chatApp, research: researchApp },
|
|
114
|
+
defaultApp: "chat",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await gateway.start();
|
|
118
|
+
|
|
119
|
+
const status = gateway.status;
|
|
120
|
+
expect(status.id).toBeDefined();
|
|
121
|
+
expect(status.uptime).toBeGreaterThanOrEqual(0);
|
|
122
|
+
expect(status.clients).toBe(0);
|
|
123
|
+
expect(status.sessions).toBe(0);
|
|
124
|
+
expect(status.apps).toEqual(["chat", "research"]);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("client connection", () => {
|
|
129
|
+
it("accepts client and emits connected event", async () => {
|
|
130
|
+
gateway = createGateway({
|
|
131
|
+
port: TEST_PORT,
|
|
132
|
+
host: TEST_HOST,
|
|
133
|
+
apps: { chat: chatApp },
|
|
134
|
+
defaultApp: "chat",
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await gateway.start();
|
|
138
|
+
|
|
139
|
+
const connectedPromise = new Promise<{ clientId: string }>((resolve) => {
|
|
140
|
+
gateway.on("client:connected", resolve);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const client = new WebSocket(`ws://${TEST_HOST}:${TEST_PORT}`);
|
|
144
|
+
await new Promise<void>((r) => client.on("open", () => r()));
|
|
145
|
+
|
|
146
|
+
const event = await connectedPromise;
|
|
147
|
+
expect(event.clientId).toBeDefined();
|
|
148
|
+
|
|
149
|
+
client.close();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("handles client disconnect", async () => {
|
|
153
|
+
gateway = createGateway({
|
|
154
|
+
port: TEST_PORT,
|
|
155
|
+
host: TEST_HOST,
|
|
156
|
+
apps: { chat: chatApp },
|
|
157
|
+
defaultApp: "chat",
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await gateway.start();
|
|
161
|
+
|
|
162
|
+
const disconnectedPromise = new Promise<{ clientId: string }>((resolve) => {
|
|
163
|
+
gateway.on("client:disconnected", resolve);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const client = new WebSocket(`ws://${TEST_HOST}:${TEST_PORT}`);
|
|
167
|
+
await new Promise<void>((r) => client.on("open", () => r()));
|
|
168
|
+
|
|
169
|
+
// Connect
|
|
170
|
+
client.send(JSON.stringify({ type: "connect", clientId: "test" }));
|
|
171
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
172
|
+
|
|
173
|
+
client.close();
|
|
174
|
+
|
|
175
|
+
const event = await disconnectedPromise;
|
|
176
|
+
expect(event.clientId).toBeDefined();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("RPC methods", () => {
|
|
181
|
+
let client: WebSocket;
|
|
182
|
+
|
|
183
|
+
beforeEach(async () => {
|
|
184
|
+
gateway = createGateway({
|
|
185
|
+
port: TEST_PORT,
|
|
186
|
+
host: TEST_HOST,
|
|
187
|
+
apps: { chat: chatApp, research: researchApp },
|
|
188
|
+
defaultApp: "chat",
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await gateway.start();
|
|
192
|
+
|
|
193
|
+
client = new WebSocket(`ws://${TEST_HOST}:${TEST_PORT}`);
|
|
194
|
+
await new Promise<void>((r) => client.on("open", () => r()));
|
|
195
|
+
|
|
196
|
+
// Authenticate
|
|
197
|
+
client.send(JSON.stringify({ type: "connect", clientId: "test" }));
|
|
198
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
afterEach(() => {
|
|
202
|
+
client?.close();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("lists apps", async () => {
|
|
206
|
+
const responsePromise = new Promise<any>((resolve) => {
|
|
207
|
+
client.on("message", (data) => {
|
|
208
|
+
const msg = JSON.parse(data.toString());
|
|
209
|
+
if (msg.type === "res") resolve(msg);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
client.send(
|
|
214
|
+
JSON.stringify({
|
|
215
|
+
type: "req",
|
|
216
|
+
id: "req-1",
|
|
217
|
+
method: "apps",
|
|
218
|
+
params: {},
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const response = await responsePromise;
|
|
223
|
+
expect(response.ok).toBe(true);
|
|
224
|
+
expect(response.payload.apps).toHaveLength(2);
|
|
225
|
+
expect(response.payload.apps[0].id).toBeDefined();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("lists sessions", async () => {
|
|
229
|
+
const responsePromise = new Promise<any>((resolve) => {
|
|
230
|
+
client.on("message", (data) => {
|
|
231
|
+
const msg = JSON.parse(data.toString());
|
|
232
|
+
if (msg.type === "res") resolve(msg);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
client.send(
|
|
237
|
+
JSON.stringify({
|
|
238
|
+
type: "req",
|
|
239
|
+
id: "req-1",
|
|
240
|
+
method: "sessions",
|
|
241
|
+
params: {},
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const response = await responsePromise;
|
|
246
|
+
expect(response.ok).toBe(true);
|
|
247
|
+
expect(response.payload.sessions).toEqual([]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("returns gateway status", async () => {
|
|
251
|
+
const responsePromise = new Promise<any>((resolve) => {
|
|
252
|
+
client.on("message", (data) => {
|
|
253
|
+
const msg = JSON.parse(data.toString());
|
|
254
|
+
if (msg.type === "res") resolve(msg);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
client.send(
|
|
259
|
+
JSON.stringify({
|
|
260
|
+
type: "req",
|
|
261
|
+
id: "req-1",
|
|
262
|
+
method: "status",
|
|
263
|
+
params: {},
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const response = await responsePromise;
|
|
268
|
+
expect(response.ok).toBe(true);
|
|
269
|
+
expect(response.payload.gateway).toBeDefined();
|
|
270
|
+
expect(response.payload.gateway.apps).toContain("chat");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("sends message to session", async () => {
|
|
274
|
+
const messages: any[] = [];
|
|
275
|
+
client.on("message", (data) => {
|
|
276
|
+
messages.push(JSON.parse(data.toString()));
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
client.send(
|
|
280
|
+
JSON.stringify({
|
|
281
|
+
type: "req",
|
|
282
|
+
id: "req-1",
|
|
283
|
+
method: "send",
|
|
284
|
+
params: {
|
|
285
|
+
sessionId: "main",
|
|
286
|
+
message: "Hello!",
|
|
287
|
+
},
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Wait for response and events
|
|
292
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
293
|
+
|
|
294
|
+
const response = messages.find((m) => m.type === "res" && m.id === "req-1");
|
|
295
|
+
expect(response?.ok).toBe(true);
|
|
296
|
+
expect(response?.payload?.messageId).toBeDefined();
|
|
297
|
+
|
|
298
|
+
// Should have created a session and sent to it
|
|
299
|
+
expect(chatApp._sessions.size).toBeGreaterThan(0);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("handles unknown method", async () => {
|
|
303
|
+
const responsePromise = new Promise<any>((resolve) => {
|
|
304
|
+
client.on("message", (data) => {
|
|
305
|
+
const msg = JSON.parse(data.toString());
|
|
306
|
+
if (msg.type === "res") resolve(msg);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
client.send(
|
|
311
|
+
JSON.stringify({
|
|
312
|
+
type: "req",
|
|
313
|
+
id: "req-1",
|
|
314
|
+
method: "unknown_method" as any,
|
|
315
|
+
params: {},
|
|
316
|
+
}),
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const response = await responsePromise;
|
|
320
|
+
expect(response.ok).toBe(false);
|
|
321
|
+
expect(response.error.message).toContain("Unknown method");
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe("authentication", () => {
|
|
326
|
+
it("requires auth when configured", async () => {
|
|
327
|
+
gateway = createGateway({
|
|
328
|
+
port: TEST_PORT,
|
|
329
|
+
host: TEST_HOST,
|
|
330
|
+
apps: { chat: chatApp },
|
|
331
|
+
defaultApp: "chat",
|
|
332
|
+
auth: { type: "token", token: "secret123" },
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
await gateway.start();
|
|
336
|
+
|
|
337
|
+
const client = new WebSocket(`ws://${TEST_HOST}:${TEST_PORT}`);
|
|
338
|
+
await new Promise<void>((r) => client.on("open", () => r()));
|
|
339
|
+
|
|
340
|
+
const errorPromise = new Promise<any>((resolve) => {
|
|
341
|
+
client.on("message", (data) => {
|
|
342
|
+
const msg = JSON.parse(data.toString());
|
|
343
|
+
if (msg.type === "error") resolve(msg);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Try to connect without token
|
|
348
|
+
client.send(JSON.stringify({ type: "connect", clientId: "test" }));
|
|
349
|
+
|
|
350
|
+
const error = await errorPromise;
|
|
351
|
+
expect(error.code).toBe("AUTH_FAILED");
|
|
352
|
+
|
|
353
|
+
client.close();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("allows auth with valid token", async () => {
|
|
357
|
+
gateway = createGateway({
|
|
358
|
+
port: TEST_PORT,
|
|
359
|
+
host: TEST_HOST,
|
|
360
|
+
apps: { chat: chatApp },
|
|
361
|
+
defaultApp: "chat",
|
|
362
|
+
auth: { type: "token", token: "secret123" },
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
await gateway.start();
|
|
366
|
+
|
|
367
|
+
const client = new WebSocket(`ws://${TEST_HOST}:${TEST_PORT}`);
|
|
368
|
+
await new Promise<void>((r) => client.on("open", () => r()));
|
|
369
|
+
|
|
370
|
+
// Connect with valid token
|
|
371
|
+
client.send(
|
|
372
|
+
JSON.stringify({
|
|
373
|
+
type: "connect",
|
|
374
|
+
clientId: "test",
|
|
375
|
+
token: "secret123",
|
|
376
|
+
}),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
// Should be able to make requests now
|
|
380
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
381
|
+
|
|
382
|
+
const responsePromise = new Promise<any>((resolve) => {
|
|
383
|
+
client.on("message", (data) => {
|
|
384
|
+
const msg = JSON.parse(data.toString());
|
|
385
|
+
if (msg.type === "res") resolve(msg);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
client.send(
|
|
390
|
+
JSON.stringify({
|
|
391
|
+
type: "req",
|
|
392
|
+
id: "req-1",
|
|
393
|
+
method: "apps",
|
|
394
|
+
params: {},
|
|
395
|
+
}),
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const response = await responsePromise;
|
|
399
|
+
expect(response.ok).toBe(true);
|
|
400
|
+
|
|
401
|
+
client.close();
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
});
|