llng-mcp 0.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/.github/workflows/ci.yml +77 -0
- package/.prettierrc +7 -0
- package/LICENSE +661 -0
- package/README.md +502 -0
- package/dist/__tests__/api-transport.test.d.ts +1 -0
- package/dist/__tests__/api-transport.test.js +577 -0
- package/dist/__tests__/api-transport.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +472 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/integration/api-mode.test.d.ts +1 -0
- package/dist/__tests__/integration/api-mode.test.js +199 -0
- package/dist/__tests__/integration/api-mode.test.js.map +1 -0
- package/dist/__tests__/integration/oidc-rp.test.d.ts +1 -0
- package/dist/__tests__/integration/oidc-rp.test.js +120 -0
- package/dist/__tests__/integration/oidc-rp.test.js.map +1 -0
- package/dist/__tests__/integration/ssh-mode.test.d.ts +1 -0
- package/dist/__tests__/integration/ssh-mode.test.js +101 -0
- package/dist/__tests__/integration/ssh-mode.test.js.map +1 -0
- package/dist/__tests__/k8s-transport.test.d.ts +1 -0
- package/dist/__tests__/k8s-transport.test.js +254 -0
- package/dist/__tests__/k8s-transport.test.js.map +1 -0
- package/dist/__tests__/oidc-tools.test.d.ts +1 -0
- package/dist/__tests__/oidc-tools.test.js +457 -0
- package/dist/__tests__/oidc-tools.test.js.map +1 -0
- package/dist/__tests__/registry.test.d.ts +1 -0
- package/dist/__tests__/registry.test.js +96 -0
- package/dist/__tests__/registry.test.js.map +1 -0
- package/dist/__tests__/ssh-transport.test.d.ts +1 -0
- package/dist/__tests__/ssh-transport.test.js +618 -0
- package/dist/__tests__/ssh-transport.test.js.map +1 -0
- package/dist/__tests__/tools.test.d.ts +1 -0
- package/dist/__tests__/tools.test.js +525 -0
- package/dist/__tests__/tools.test.js.map +1 -0
- package/dist/config.d.ts +65 -0
- package/dist/config.js +506 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/resources/documentation.d.ts +5 -0
- package/dist/resources/documentation.js +56 -0
- package/dist/resources/documentation.js.map +1 -0
- package/dist/tools/cli-utilities.d.ts +3 -0
- package/dist/tools/cli-utilities.js +187 -0
- package/dist/tools/cli-utilities.js.map +1 -0
- package/dist/tools/config.d.ts +6 -0
- package/dist/tools/config.js +326 -0
- package/dist/tools/config.js.map +1 -0
- package/dist/tools/consents.d.ts +3 -0
- package/dist/tools/consents.js +39 -0
- package/dist/tools/consents.js.map +1 -0
- package/dist/tools/instances.d.ts +3 -0
- package/dist/tools/instances.js +14 -0
- package/dist/tools/instances.js.map +1 -0
- package/dist/tools/oidc-rp.d.ts +6 -0
- package/dist/tools/oidc-rp.js +246 -0
- package/dist/tools/oidc-rp.js.map +1 -0
- package/dist/tools/oidc.d.ts +3 -0
- package/dist/tools/oidc.js +343 -0
- package/dist/tools/oidc.js.map +1 -0
- package/dist/tools/secondfactors.d.ts +3 -0
- package/dist/tools/secondfactors.js +62 -0
- package/dist/tools/secondfactors.js.map +1 -0
- package/dist/tools/sessions.d.ts +6 -0
- package/dist/tools/sessions.js +300 -0
- package/dist/tools/sessions.js.map +1 -0
- package/dist/transport/api.d.ts +35 -0
- package/dist/transport/api.js +327 -0
- package/dist/transport/api.js.map +1 -0
- package/dist/transport/interface.d.ts +50 -0
- package/dist/transport/interface.js +2 -0
- package/dist/transport/interface.js.map +1 -0
- package/dist/transport/k8s.d.ts +41 -0
- package/dist/transport/k8s.js +303 -0
- package/dist/transport/k8s.js.map +1 -0
- package/dist/transport/registry.d.ts +20 -0
- package/dist/transport/registry.js +91 -0
- package/dist/transport/registry.js.map +1 -0
- package/dist/transport/ssh.d.ts +37 -0
- package/dist/transport/ssh.js +353 -0
- package/dist/transport/ssh.js.map +1 -0
- package/docker-compose.test.yml +16 -0
- package/eslint.config.js +21 -0
- package/package.json +38 -0
- package/src/__tests__/api-transport.test.ts +746 -0
- package/src/__tests__/config.test.ts +587 -0
- package/src/__tests__/integration/api-mode.test.ts +229 -0
- package/src/__tests__/integration/oidc-rp.test.ts +138 -0
- package/src/__tests__/integration/ssh-mode.test.ts +113 -0
- package/src/__tests__/k8s-transport.test.ts +342 -0
- package/src/__tests__/oidc-tools.test.ts +554 -0
- package/src/__tests__/registry.test.ts +110 -0
- package/src/__tests__/ssh-transport.test.ts +805 -0
- package/src/__tests__/tools.test.ts +735 -0
- package/src/config.ts +605 -0
- package/src/index.ts +48 -0
- package/src/resources/documentation.ts +65 -0
- package/src/tools/cli-utilities.ts +207 -0
- package/src/tools/config.ts +382 -0
- package/src/tools/consents.ts +50 -0
- package/src/tools/instances.ts +21 -0
- package/src/tools/oidc-rp.ts +299 -0
- package/src/tools/oidc.ts +434 -0
- package/src/tools/secondfactors.ts +78 -0
- package/src/tools/sessions.ts +342 -0
- package/src/transport/api.ts +429 -0
- package/src/transport/interface.ts +58 -0
- package/src/transport/k8s.ts +367 -0
- package/src/transport/registry.ts +105 -0
- package/src/transport/ssh.ts +430 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +8 -0
- package/vitest.integration.config.ts +9 -0
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { registerConfigTools } from "../tools/config.js";
|
|
4
|
+
import { registerSessionTools } from "../tools/sessions.js";
|
|
5
|
+
import { registerSecondFactorTools } from "../tools/secondfactors.js";
|
|
6
|
+
import { registerConsentTools } from "../tools/consents.js";
|
|
7
|
+
import { registerOidcTools } from "../tools/oidc.js";
|
|
8
|
+
import { registerOidcRpTools } from "../tools/oidc-rp.js";
|
|
9
|
+
import { registerCliUtilityTools } from "../tools/cli-utilities.js";
|
|
10
|
+
import { TransportRegistry } from "../transport/registry.js";
|
|
11
|
+
|
|
12
|
+
describe("Tool Registration", () => {
|
|
13
|
+
function createMockServer() {
|
|
14
|
+
const toolNames: string[] = [];
|
|
15
|
+
const mockServer = {
|
|
16
|
+
tool: vi.fn((name: string, _desc: string, _schema: any, _handler: any) => {
|
|
17
|
+
toolNames.push(name);
|
|
18
|
+
}),
|
|
19
|
+
} as unknown as McpServer;
|
|
20
|
+
return { mockServer, toolNames };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createMockRegistry() {
|
|
24
|
+
const mockTransport = {
|
|
25
|
+
configInfo: vi
|
|
26
|
+
.fn()
|
|
27
|
+
.mockResolvedValue({ cfgNum: 1, cfgAuthor: "test", cfgDate: "2025-01-30" }),
|
|
28
|
+
configGet: vi.fn().mockResolvedValue({}),
|
|
29
|
+
configSet: vi.fn().mockResolvedValue(undefined),
|
|
30
|
+
configAddKey: vi.fn().mockResolvedValue(undefined),
|
|
31
|
+
configDelKey: vi.fn().mockResolvedValue(undefined),
|
|
32
|
+
configSave: vi.fn().mockResolvedValue("{}"),
|
|
33
|
+
configRestore: vi.fn().mockResolvedValue(undefined),
|
|
34
|
+
configMerge: vi.fn().mockResolvedValue(undefined),
|
|
35
|
+
configRollback: vi.fn().mockResolvedValue(undefined),
|
|
36
|
+
configUpdateCache: vi.fn().mockResolvedValue(undefined),
|
|
37
|
+
sessionGet: vi.fn().mockResolvedValue({}),
|
|
38
|
+
sessionSearch: vi.fn().mockResolvedValue([]),
|
|
39
|
+
sessionDelete: vi.fn().mockResolvedValue(undefined),
|
|
40
|
+
sessionSetKey: vi.fn().mockResolvedValue(undefined),
|
|
41
|
+
sessionDelKey: vi.fn().mockResolvedValue(undefined),
|
|
42
|
+
sessionBackup: vi.fn().mockResolvedValue("{}"),
|
|
43
|
+
secondFactorsGet: vi.fn().mockResolvedValue([]),
|
|
44
|
+
secondFactorsDelete: vi.fn().mockResolvedValue(undefined),
|
|
45
|
+
secondFactorsDelType: vi.fn().mockResolvedValue(undefined),
|
|
46
|
+
consentsGet: vi.fn().mockResolvedValue([]),
|
|
47
|
+
consentsDelete: vi.fn().mockResolvedValue(undefined),
|
|
48
|
+
configTestEmail: vi.fn().mockResolvedValue(undefined),
|
|
49
|
+
execScript: vi.fn().mockResolvedValue("script output"),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const registry = {
|
|
53
|
+
getTransport: vi.fn().mockReturnValue(mockTransport),
|
|
54
|
+
getOidcConfig: vi.fn().mockReturnValue({
|
|
55
|
+
issuer: "https://auth.example.com",
|
|
56
|
+
clientId: "test-client",
|
|
57
|
+
redirectUri: "http://localhost:3000/callback",
|
|
58
|
+
scope: "openid profile",
|
|
59
|
+
}),
|
|
60
|
+
listInstances: vi.fn().mockReturnValue([{ name: "default", mode: "ssh", isDefault: true }]),
|
|
61
|
+
} as unknown as TransportRegistry;
|
|
62
|
+
|
|
63
|
+
return { registry, mockTransport };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("Config Tools", () => {
|
|
67
|
+
it("should register 11 config tools", () => {
|
|
68
|
+
const { mockServer, toolNames } = createMockServer();
|
|
69
|
+
const { registry } = createMockRegistry();
|
|
70
|
+
|
|
71
|
+
registerConfigTools(mockServer, registry);
|
|
72
|
+
|
|
73
|
+
expect(toolNames).toHaveLength(11);
|
|
74
|
+
expect(toolNames).toEqual([
|
|
75
|
+
"llng_config_info",
|
|
76
|
+
"llng_config_get",
|
|
77
|
+
"llng_config_set",
|
|
78
|
+
"llng_config_addKey",
|
|
79
|
+
"llng_config_delKey",
|
|
80
|
+
"llng_config_export",
|
|
81
|
+
"llng_config_import",
|
|
82
|
+
"llng_config_merge",
|
|
83
|
+
"llng_config_rollback",
|
|
84
|
+
"llng_config_update_cache",
|
|
85
|
+
"llng_config_test_email",
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should handle transport errors with isError flag", async () => {
|
|
90
|
+
const { mockServer } = createMockServer();
|
|
91
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
92
|
+
|
|
93
|
+
// Override one method to throw
|
|
94
|
+
mockTransport.configInfo = vi.fn().mockRejectedValue(new Error("Connection failed"));
|
|
95
|
+
|
|
96
|
+
registerConfigTools(mockServer, registry);
|
|
97
|
+
|
|
98
|
+
// Get the handler from the mock
|
|
99
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
100
|
+
(call: any) => call[0] === "llng_config_info",
|
|
101
|
+
);
|
|
102
|
+
expect(toolCall).toBeDefined();
|
|
103
|
+
|
|
104
|
+
const handler = toolCall[3];
|
|
105
|
+
const result = await handler({});
|
|
106
|
+
|
|
107
|
+
expect(result.isError).toBe(true);
|
|
108
|
+
expect(result.content[0].text).toContain("Error: Connection failed");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should successfully call transport methods", async () => {
|
|
112
|
+
const { mockServer } = createMockServer();
|
|
113
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
114
|
+
|
|
115
|
+
const expectedInfo = {
|
|
116
|
+
cfgNum: 5,
|
|
117
|
+
cfgAuthor: "admin",
|
|
118
|
+
cfgDate: 1234567890,
|
|
119
|
+
cfgLog: "Test update",
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
mockTransport.configInfo = vi.fn().mockResolvedValue(expectedInfo);
|
|
123
|
+
|
|
124
|
+
registerConfigTools(mockServer, registry);
|
|
125
|
+
|
|
126
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
127
|
+
(call: any) => call[0] === "llng_config_info",
|
|
128
|
+
);
|
|
129
|
+
const handler = toolCall[3];
|
|
130
|
+
const result = await handler({});
|
|
131
|
+
|
|
132
|
+
expect(result.isError).toBeUndefined();
|
|
133
|
+
expect(result.content[0].text).toBe(JSON.stringify(expectedInfo, null, 2));
|
|
134
|
+
expect(mockTransport.configInfo).toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("Session Tools", () => {
|
|
139
|
+
it("should register 6 session tools", () => {
|
|
140
|
+
const { mockServer, toolNames } = createMockServer();
|
|
141
|
+
const { registry } = createMockRegistry();
|
|
142
|
+
|
|
143
|
+
registerSessionTools(mockServer, registry);
|
|
144
|
+
|
|
145
|
+
expect(toolNames).toHaveLength(6);
|
|
146
|
+
expect(toolNames).toEqual([
|
|
147
|
+
"llng_session_get",
|
|
148
|
+
"llng_session_search",
|
|
149
|
+
"llng_session_delete",
|
|
150
|
+
"llng_session_setKey",
|
|
151
|
+
"llng_session_delKey",
|
|
152
|
+
"llng_session_backup",
|
|
153
|
+
]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should handle transport errors with isError flag", async () => {
|
|
157
|
+
const { mockServer } = createMockServer();
|
|
158
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
159
|
+
|
|
160
|
+
mockTransport.sessionGet = vi.fn().mockRejectedValue(new Error("Session not found"));
|
|
161
|
+
|
|
162
|
+
registerSessionTools(mockServer, registry);
|
|
163
|
+
|
|
164
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
165
|
+
(call: any) => call[0] === "llng_session_get",
|
|
166
|
+
);
|
|
167
|
+
const handler = toolCall[3];
|
|
168
|
+
const result = await handler({ id: "session123" });
|
|
169
|
+
|
|
170
|
+
expect(result.isError).toBe(true);
|
|
171
|
+
expect(result.content[0].text).toContain("Session not found");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should pass correct parameters to transport", async () => {
|
|
175
|
+
const { mockServer } = createMockServer();
|
|
176
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
177
|
+
|
|
178
|
+
const sessionData = { _session_id: "session123", uid: "user1" };
|
|
179
|
+
mockTransport.sessionGet = vi.fn().mockResolvedValue(sessionData);
|
|
180
|
+
|
|
181
|
+
registerSessionTools(mockServer, registry);
|
|
182
|
+
|
|
183
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
184
|
+
(call: any) => call[0] === "llng_session_get",
|
|
185
|
+
);
|
|
186
|
+
const handler = toolCall[3];
|
|
187
|
+
const result = await handler({ id: "session123", backend: "oidc" });
|
|
188
|
+
|
|
189
|
+
expect(mockTransport.sessionGet).toHaveBeenCalledWith("session123", {
|
|
190
|
+
backend: "oidc",
|
|
191
|
+
refreshTokens: undefined,
|
|
192
|
+
persistent: undefined,
|
|
193
|
+
hash: undefined,
|
|
194
|
+
});
|
|
195
|
+
expect(result.content[0].text).toBe(JSON.stringify(sessionData, null, 2));
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("2FA Tools", () => {
|
|
200
|
+
it("should register 3 second factor tools", () => {
|
|
201
|
+
const { mockServer, toolNames } = createMockServer();
|
|
202
|
+
const { registry } = createMockRegistry();
|
|
203
|
+
|
|
204
|
+
registerSecondFactorTools(mockServer, registry);
|
|
205
|
+
|
|
206
|
+
expect(toolNames).toHaveLength(3);
|
|
207
|
+
expect(toolNames).toEqual(["llng_2fa_list", "llng_2fa_delete", "llng_2fa_delType"]);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("should handle transport errors with isError flag", async () => {
|
|
211
|
+
const { mockServer } = createMockServer();
|
|
212
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
213
|
+
|
|
214
|
+
mockTransport.secondFactorsGet = vi.fn().mockRejectedValue(new Error("User not found"));
|
|
215
|
+
|
|
216
|
+
registerSecondFactorTools(mockServer, registry);
|
|
217
|
+
|
|
218
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
219
|
+
(call: any) => call[0] === "llng_2fa_list",
|
|
220
|
+
);
|
|
221
|
+
const handler = toolCall[3];
|
|
222
|
+
const result = await handler({ user: "john" });
|
|
223
|
+
|
|
224
|
+
expect(result.isError).toBe(true);
|
|
225
|
+
expect(result.content[0].text).toContain("User not found");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should return device list", async () => {
|
|
229
|
+
const { mockServer } = createMockServer();
|
|
230
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
231
|
+
|
|
232
|
+
const devices = [
|
|
233
|
+
{ id: "1", type: "TOTP", name: "Authenticator" },
|
|
234
|
+
{ id: "2", type: "U2F", name: "Security Key" },
|
|
235
|
+
];
|
|
236
|
+
mockTransport.secondFactorsGet = vi.fn().mockResolvedValue(devices);
|
|
237
|
+
|
|
238
|
+
registerSecondFactorTools(mockServer, registry);
|
|
239
|
+
|
|
240
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
241
|
+
(call: any) => call[0] === "llng_2fa_list",
|
|
242
|
+
);
|
|
243
|
+
const handler = toolCall[3];
|
|
244
|
+
const result = await handler({ user: "john" });
|
|
245
|
+
|
|
246
|
+
expect(result.content[0].text).toBe(JSON.stringify(devices, null, 2));
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("Consent Tools", () => {
|
|
251
|
+
it("should register 2 consent tools", () => {
|
|
252
|
+
const { mockServer, toolNames } = createMockServer();
|
|
253
|
+
const { registry } = createMockRegistry();
|
|
254
|
+
|
|
255
|
+
registerConsentTools(mockServer, registry);
|
|
256
|
+
|
|
257
|
+
expect(toolNames).toHaveLength(2);
|
|
258
|
+
expect(toolNames).toEqual(["llng_consent_list", "llng_consent_delete"]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("should handle transport errors with isError flag", async () => {
|
|
262
|
+
const { mockServer } = createMockServer();
|
|
263
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
264
|
+
|
|
265
|
+
mockTransport.consentsGet = vi.fn().mockRejectedValue(new Error("Database error"));
|
|
266
|
+
|
|
267
|
+
registerConsentTools(mockServer, registry);
|
|
268
|
+
|
|
269
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
270
|
+
(call: any) => call[0] === "llng_consent_list",
|
|
271
|
+
);
|
|
272
|
+
const handler = toolCall[3];
|
|
273
|
+
const result = await handler({ user: "john" });
|
|
274
|
+
|
|
275
|
+
expect(result.isError).toBe(true);
|
|
276
|
+
expect(result.content[0].text).toContain("Database error");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("should return consent list", async () => {
|
|
280
|
+
const { mockServer } = createMockServer();
|
|
281
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
282
|
+
|
|
283
|
+
const consents = [
|
|
284
|
+
{ id: "1", rp: "app1.example.com", scope: "openid profile" },
|
|
285
|
+
{ id: "2", rp: "app2.example.com", scope: "openid email" },
|
|
286
|
+
];
|
|
287
|
+
mockTransport.consentsGet = vi.fn().mockResolvedValue(consents);
|
|
288
|
+
|
|
289
|
+
registerConsentTools(mockServer, registry);
|
|
290
|
+
|
|
291
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
292
|
+
(call: any) => call[0] === "llng_consent_list",
|
|
293
|
+
);
|
|
294
|
+
const handler = toolCall[3];
|
|
295
|
+
const result = await handler({ user: "john" });
|
|
296
|
+
|
|
297
|
+
expect(result.content[0].text).toBe(JSON.stringify(consents, null, 2));
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe("OIDC Tools", () => {
|
|
302
|
+
it("should register 8 OIDC tools", () => {
|
|
303
|
+
const { mockServer, toolNames } = createMockServer();
|
|
304
|
+
const { registry } = createMockRegistry();
|
|
305
|
+
|
|
306
|
+
registerOidcTools(mockServer, registry);
|
|
307
|
+
|
|
308
|
+
expect(toolNames).toHaveLength(8);
|
|
309
|
+
expect(toolNames).toEqual([
|
|
310
|
+
"llng_oidc_metadata",
|
|
311
|
+
"llng_oidc_authorize",
|
|
312
|
+
"llng_oidc_tokens",
|
|
313
|
+
"llng_oidc_userinfo",
|
|
314
|
+
"llng_oidc_introspect",
|
|
315
|
+
"llng_oidc_refresh",
|
|
316
|
+
"llng_oidc_whoami",
|
|
317
|
+
"llng_oidc_check_auth",
|
|
318
|
+
]);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("should handle undefined config with error", async () => {
|
|
322
|
+
const { mockServer } = createMockServer();
|
|
323
|
+
const { registry } = createMockRegistry();
|
|
324
|
+
(registry.getOidcConfig as any).mockReturnValue(undefined);
|
|
325
|
+
|
|
326
|
+
registerOidcTools(mockServer, registry);
|
|
327
|
+
|
|
328
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
329
|
+
(call: any) => call[0] === "llng_oidc_metadata",
|
|
330
|
+
);
|
|
331
|
+
const handler = toolCall[3];
|
|
332
|
+
const result = await handler({});
|
|
333
|
+
|
|
334
|
+
expect(result.isError).toBe(true);
|
|
335
|
+
expect(result.content[0].text).toBe("Error: OIDC not configured");
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe("OIDC RP Tools", () => {
|
|
340
|
+
it("should register 5 OIDC RP tools", () => {
|
|
341
|
+
const { mockServer, toolNames } = createMockServer();
|
|
342
|
+
const { registry } = createMockRegistry();
|
|
343
|
+
|
|
344
|
+
registerOidcRpTools(mockServer, registry);
|
|
345
|
+
|
|
346
|
+
expect(toolNames).toHaveLength(5);
|
|
347
|
+
expect(toolNames).toEqual([
|
|
348
|
+
"llng_oidc_issuer_enable",
|
|
349
|
+
"llng_oidc_rp_list",
|
|
350
|
+
"llng_oidc_rp_get",
|
|
351
|
+
"llng_oidc_rp_add",
|
|
352
|
+
"llng_oidc_rp_delete",
|
|
353
|
+
]);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("should return empty list when no RPs configured", async () => {
|
|
357
|
+
const { mockServer } = createMockServer();
|
|
358
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
359
|
+
|
|
360
|
+
mockTransport.configGet = vi.fn().mockResolvedValue({ oidcRPMetaDataOptions: null });
|
|
361
|
+
|
|
362
|
+
registerOidcRpTools(mockServer, registry);
|
|
363
|
+
|
|
364
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
365
|
+
(call: any) => call[0] === "llng_oidc_rp_list",
|
|
366
|
+
);
|
|
367
|
+
const handler = toolCall[3];
|
|
368
|
+
const result = await handler({});
|
|
369
|
+
|
|
370
|
+
expect(result.content[0].text).toBe(JSON.stringify([], null, 2));
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("should fetch top-level keys and extract confKey for rp_get", async () => {
|
|
374
|
+
const { mockServer } = createMockServer();
|
|
375
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
376
|
+
|
|
377
|
+
mockTransport.configGet = vi.fn().mockResolvedValue({
|
|
378
|
+
oidcRPMetaDataOptions: { myApp: { oidcRPMetaDataOptionsClientID: "client1" } },
|
|
379
|
+
oidcRPMetaDataExportedVars: { myApp: { name: "cn" } },
|
|
380
|
+
oidcRPMetaDataMacros: {},
|
|
381
|
+
oidcRPMetaDataScopeRules: {},
|
|
382
|
+
oidcRPMetaDataOptionsExtraClaims: {},
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
registerOidcRpTools(mockServer, registry);
|
|
386
|
+
|
|
387
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
388
|
+
(call: any) => call[0] === "llng_oidc_rp_get",
|
|
389
|
+
);
|
|
390
|
+
const handler = toolCall[3];
|
|
391
|
+
const result = await handler({ confKey: "myApp" });
|
|
392
|
+
|
|
393
|
+
expect(result.content[0].text).toContain("client1");
|
|
394
|
+
// Verify it fetches top-level keys, not slash-separated
|
|
395
|
+
expect(mockTransport.configGet).toHaveBeenCalledWith([
|
|
396
|
+
"oidcRPMetaDataOptions",
|
|
397
|
+
"oidcRPMetaDataExportedVars",
|
|
398
|
+
"oidcRPMetaDataMacros",
|
|
399
|
+
"oidcRPMetaDataScopeRules",
|
|
400
|
+
"oidcRPMetaDataOptionsExtraClaims",
|
|
401
|
+
]);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("should handle transport errors with isError flag", async () => {
|
|
405
|
+
const { mockServer } = createMockServer();
|
|
406
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
407
|
+
|
|
408
|
+
mockTransport.configGet = vi.fn().mockRejectedValue(new Error("Transport error"));
|
|
409
|
+
|
|
410
|
+
registerOidcRpTools(mockServer, registry);
|
|
411
|
+
|
|
412
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
413
|
+
(call: any) => call[0] === "llng_oidc_rp_list",
|
|
414
|
+
);
|
|
415
|
+
const handler = toolCall[3];
|
|
416
|
+
const result = await handler({});
|
|
417
|
+
|
|
418
|
+
expect(result.isError).toBe(true);
|
|
419
|
+
expect(result.content[0].text).toContain("Transport error");
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("should enable OIDC issuer when not activated", async () => {
|
|
423
|
+
const { mockServer } = createMockServer();
|
|
424
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
425
|
+
|
|
426
|
+
mockTransport.configGet = vi.fn().mockResolvedValue({
|
|
427
|
+
issuerDBOpenIDConnectActivation: 0,
|
|
428
|
+
oidcServicePrivateKeySig: "",
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
registerOidcRpTools(mockServer, registry);
|
|
432
|
+
|
|
433
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
434
|
+
(call: any) => call[0] === "llng_oidc_issuer_enable",
|
|
435
|
+
);
|
|
436
|
+
const handler = toolCall[3];
|
|
437
|
+
const result = await handler({});
|
|
438
|
+
|
|
439
|
+
expect(mockTransport.configSet).toHaveBeenCalledWith({ issuerDBOpenIDConnectActivation: 1 });
|
|
440
|
+
expect(mockTransport.execScript).toHaveBeenCalledWith("rotateOidcKeys", []);
|
|
441
|
+
expect(result.content[0].text).toContain("enabled successfully");
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("should refuse rp_add when issuer is not enabled", async () => {
|
|
445
|
+
const { mockServer } = createMockServer();
|
|
446
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
447
|
+
|
|
448
|
+
mockTransport.configGet = vi.fn().mockResolvedValue({
|
|
449
|
+
issuerDBOpenIDConnectActivation: 0,
|
|
450
|
+
oidcServicePrivateKeySig: "",
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
registerOidcRpTools(mockServer, registry);
|
|
454
|
+
|
|
455
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
456
|
+
(call: any) => call[0] === "llng_oidc_rp_add",
|
|
457
|
+
);
|
|
458
|
+
const handler = toolCall[3];
|
|
459
|
+
const result = await handler({
|
|
460
|
+
confKey: "myApp",
|
|
461
|
+
clientId: "client1",
|
|
462
|
+
redirectUris: "http://localhost/callback",
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
expect(result.isError).toBe(true);
|
|
466
|
+
expect(result.content[0].text).toContain("llng_oidc_issuer_enable");
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("should add default IDTokenSignAlg when issuer is enabled", async () => {
|
|
470
|
+
const { mockServer } = createMockServer();
|
|
471
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
472
|
+
|
|
473
|
+
mockTransport.configGet = vi.fn().mockResolvedValue({
|
|
474
|
+
issuerDBOpenIDConnectActivation: 1,
|
|
475
|
+
oidcServicePrivateKeySig:
|
|
476
|
+
"-----BEGIN RSA PRIVATE KEY-----\nfake\n-----END RSA PRIVATE KEY-----",
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
registerOidcRpTools(mockServer, registry);
|
|
480
|
+
|
|
481
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
482
|
+
(call: any) => call[0] === "llng_oidc_rp_add",
|
|
483
|
+
);
|
|
484
|
+
const handler = toolCall[3];
|
|
485
|
+
const result = await handler({
|
|
486
|
+
confKey: "myApp",
|
|
487
|
+
clientId: "client1",
|
|
488
|
+
redirectUris: "http://localhost/callback",
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
expect(result.isError).toBeUndefined();
|
|
492
|
+
expect(result.content[0].text).toContain("added successfully");
|
|
493
|
+
|
|
494
|
+
// Verify configMerge was called with IDTokenSignAlg default
|
|
495
|
+
const mergeCall = mockTransport.configMerge.mock.calls[0][0];
|
|
496
|
+
const mergeData = JSON.parse(mergeCall);
|
|
497
|
+
expect(mergeData.oidcRPMetaDataOptions.myApp.oidcRPMetaDataOptionsIDTokenSignAlg).toBe(
|
|
498
|
+
"RS256",
|
|
499
|
+
);
|
|
500
|
+
expect(mergeData.oidcRPMetaDataOptions.myApp.oidcRPMetaDataOptionsAccessTokenClaims).toBe(1);
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
describe("CLI Utility Tools", () => {
|
|
505
|
+
it("should register 7 CLI utility tools", () => {
|
|
506
|
+
const { mockServer, toolNames } = createMockServer();
|
|
507
|
+
const { registry } = createMockRegistry();
|
|
508
|
+
|
|
509
|
+
registerCliUtilityTools(mockServer, registry);
|
|
510
|
+
|
|
511
|
+
expect(toolNames).toHaveLength(7);
|
|
512
|
+
expect(toolNames).toEqual([
|
|
513
|
+
"llng_download_saml_metadata",
|
|
514
|
+
"llng_import_metadata",
|
|
515
|
+
"llng_delete_session",
|
|
516
|
+
"llng_user_attributes",
|
|
517
|
+
"llng_purge_central_cache",
|
|
518
|
+
"llng_purge_local_cache",
|
|
519
|
+
"llng_rotate_oidc_keys",
|
|
520
|
+
]);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("should call execScript with correct arguments", async () => {
|
|
524
|
+
const { mockServer } = createMockServer();
|
|
525
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
526
|
+
|
|
527
|
+
registerCliUtilityTools(mockServer, registry);
|
|
528
|
+
|
|
529
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
530
|
+
(call: any) => call[0] === "llng_user_attributes",
|
|
531
|
+
);
|
|
532
|
+
const handler = toolCall[3];
|
|
533
|
+
const result = await handler({ username: "john", field: "mail" });
|
|
534
|
+
|
|
535
|
+
expect(mockTransport.execScript).toHaveBeenCalledWith("llngUserAttributes", [
|
|
536
|
+
"--username",
|
|
537
|
+
"john",
|
|
538
|
+
"--field",
|
|
539
|
+
"mail",
|
|
540
|
+
]);
|
|
541
|
+
expect(result.content[0].text).toBe("script output");
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("should handle execScript errors with isError flag", async () => {
|
|
545
|
+
const { mockServer } = createMockServer();
|
|
546
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
547
|
+
|
|
548
|
+
mockTransport.execScript = vi.fn().mockRejectedValue(new Error("Not supported via API"));
|
|
549
|
+
|
|
550
|
+
registerCliUtilityTools(mockServer, registry);
|
|
551
|
+
|
|
552
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
553
|
+
(call: any) => call[0] === "llng_rotate_oidc_keys",
|
|
554
|
+
);
|
|
555
|
+
const handler = toolCall[3];
|
|
556
|
+
const result = await handler({});
|
|
557
|
+
|
|
558
|
+
expect(result.isError).toBe(true);
|
|
559
|
+
expect(result.content[0].text).toContain("Not supported via API");
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it("should handle boolean flags correctly", async () => {
|
|
563
|
+
const { mockServer } = createMockServer();
|
|
564
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
565
|
+
|
|
566
|
+
registerCliUtilityTools(mockServer, registry);
|
|
567
|
+
|
|
568
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
569
|
+
(call: any) => call[0] === "llng_purge_central_cache",
|
|
570
|
+
);
|
|
571
|
+
const handler = toolCall[3];
|
|
572
|
+
await handler({ debug: true, force: true, json: true });
|
|
573
|
+
|
|
574
|
+
expect(mockTransport.execScript).toHaveBeenCalledWith("purgeCentralCache", [
|
|
575
|
+
"--debug",
|
|
576
|
+
"--force",
|
|
577
|
+
"--json",
|
|
578
|
+
]);
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
describe("Error Handling", () => {
|
|
583
|
+
it("should wrap non-Error objects in error responses", async () => {
|
|
584
|
+
const { mockServer } = createMockServer();
|
|
585
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
586
|
+
|
|
587
|
+
// Reject with a string instead of Error object
|
|
588
|
+
mockTransport.configInfo = vi.fn().mockRejectedValue("Something went wrong");
|
|
589
|
+
|
|
590
|
+
registerConfigTools(mockServer, registry);
|
|
591
|
+
|
|
592
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
593
|
+
(call: any) => call[0] === "llng_config_info",
|
|
594
|
+
);
|
|
595
|
+
const handler = toolCall[3];
|
|
596
|
+
const result = await handler({});
|
|
597
|
+
|
|
598
|
+
expect(result.isError).toBe(true);
|
|
599
|
+
expect(result.content[0].text).toContain("Error: Something went wrong");
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it("should handle multiple tool errors independently", async () => {
|
|
603
|
+
const { mockServer } = createMockServer();
|
|
604
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
605
|
+
|
|
606
|
+
mockTransport.configInfo = vi.fn().mockRejectedValue(new Error("Config error"));
|
|
607
|
+
mockTransport.sessionGet = vi.fn().mockRejectedValue(new Error("Session error"));
|
|
608
|
+
|
|
609
|
+
registerConfigTools(mockServer, registry);
|
|
610
|
+
registerSessionTools(mockServer, registry);
|
|
611
|
+
|
|
612
|
+
const configToolCall = (mockServer.tool as any).mock.calls.find(
|
|
613
|
+
(call: any) => call[0] === "llng_config_info",
|
|
614
|
+
);
|
|
615
|
+
const sessionToolCall = (mockServer.tool as any).mock.calls.find(
|
|
616
|
+
(call: any) => call[0] === "llng_session_get",
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
const configResult = await configToolCall[3]({});
|
|
620
|
+
const sessionResult = await sessionToolCall[3]({ id: "test" });
|
|
621
|
+
|
|
622
|
+
expect(configResult.isError).toBe(true);
|
|
623
|
+
expect(configResult.content[0].text).toContain("Config error");
|
|
624
|
+
|
|
625
|
+
expect(sessionResult.isError).toBe(true);
|
|
626
|
+
expect(sessionResult.content[0].text).toContain("Session error");
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
describe("Success Responses", () => {
|
|
631
|
+
it("should return success messages for void operations", async () => {
|
|
632
|
+
const { mockServer } = createMockServer();
|
|
633
|
+
const { registry } = createMockRegistry();
|
|
634
|
+
|
|
635
|
+
registerConfigTools(mockServer, registry);
|
|
636
|
+
|
|
637
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
638
|
+
(call: any) => call[0] === "llng_config_set",
|
|
639
|
+
);
|
|
640
|
+
const handler = toolCall[3];
|
|
641
|
+
const result = await handler({ keys: { domain: "example.com" } });
|
|
642
|
+
|
|
643
|
+
expect(result.isError).toBeUndefined();
|
|
644
|
+
expect(result.content[0].text).toBe("Config values updated successfully");
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it("should include IDs in success messages", async () => {
|
|
648
|
+
const { mockServer } = createMockServer();
|
|
649
|
+
const { registry } = createMockRegistry();
|
|
650
|
+
|
|
651
|
+
registerSessionTools(mockServer, registry);
|
|
652
|
+
|
|
653
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
654
|
+
(call: any) => call[0] === "llng_session_delete",
|
|
655
|
+
);
|
|
656
|
+
const handler = toolCall[3];
|
|
657
|
+
const result = await handler({ ids: ["session1", "session2", "session3"] });
|
|
658
|
+
|
|
659
|
+
expect(result.isError).toBeUndefined();
|
|
660
|
+
expect(result.content[0].text).toBe("Successfully deleted 3 session(s)");
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
describe("Parameter Handling", () => {
|
|
665
|
+
it("should pass optional parameters correctly", async () => {
|
|
666
|
+
const { mockServer } = createMockServer();
|
|
667
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
668
|
+
|
|
669
|
+
registerConfigTools(mockServer, registry);
|
|
670
|
+
|
|
671
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
672
|
+
(call: any) => call[0] === "llng_config_set",
|
|
673
|
+
);
|
|
674
|
+
const handler = toolCall[3];
|
|
675
|
+
|
|
676
|
+
await handler({ keys: { domain: "example.com" }, log: "Updated domain" });
|
|
677
|
+
|
|
678
|
+
expect(mockTransport.configSet).toHaveBeenCalledWith(
|
|
679
|
+
{ domain: "example.com" },
|
|
680
|
+
"Updated domain",
|
|
681
|
+
);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it("should handle array parameters", async () => {
|
|
685
|
+
const { mockServer } = createMockServer();
|
|
686
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
687
|
+
|
|
688
|
+
registerSessionTools(mockServer, registry);
|
|
689
|
+
|
|
690
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
691
|
+
(call: any) => call[0] === "llng_session_delete",
|
|
692
|
+
);
|
|
693
|
+
const handler = toolCall[3];
|
|
694
|
+
|
|
695
|
+
await handler({ ids: ["id1", "id2"], backend: "oidc" });
|
|
696
|
+
|
|
697
|
+
expect(mockTransport.sessionDelete).toHaveBeenCalledWith(["id1", "id2"], {
|
|
698
|
+
backend: "oidc",
|
|
699
|
+
refreshTokens: undefined,
|
|
700
|
+
persistent: undefined,
|
|
701
|
+
hash: undefined,
|
|
702
|
+
where: undefined,
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it("should handle complex filter objects", async () => {
|
|
707
|
+
const { mockServer } = createMockServer();
|
|
708
|
+
const { registry, mockTransport } = createMockRegistry();
|
|
709
|
+
|
|
710
|
+
registerSessionTools(mockServer, registry);
|
|
711
|
+
|
|
712
|
+
const toolCall = (mockServer.tool as any).mock.calls.find(
|
|
713
|
+
(call: any) => call[0] === "llng_session_search",
|
|
714
|
+
);
|
|
715
|
+
const handler = toolCall[3];
|
|
716
|
+
|
|
717
|
+
const filter = {
|
|
718
|
+
where: { uid: "user1", ipAddr: "192.168.1.1" },
|
|
719
|
+
select: ["uid", "_startTime"],
|
|
720
|
+
backend: "global",
|
|
721
|
+
count: false,
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
await handler(filter);
|
|
725
|
+
|
|
726
|
+
expect(mockTransport.sessionSearch).toHaveBeenCalledWith({
|
|
727
|
+
...filter,
|
|
728
|
+
refreshTokens: undefined,
|
|
729
|
+
persistent: undefined,
|
|
730
|
+
hash: undefined,
|
|
731
|
+
idOnly: undefined,
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
});
|