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,746 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { ApiTransport } from "../transport/api.js";
|
|
3
|
+
|
|
4
|
+
// Helper function to mock fetch response
|
|
5
|
+
function mockFetchResponse(data: any, ok = true, status = 200) {
|
|
6
|
+
return {
|
|
7
|
+
ok,
|
|
8
|
+
status,
|
|
9
|
+
statusText: ok ? "OK" : "Error",
|
|
10
|
+
json: () => Promise.resolve(data),
|
|
11
|
+
text: () => Promise.resolve(JSON.stringify(data)),
|
|
12
|
+
headers: new Headers({ "content-type": "application/json" }),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("ApiTransport", () => {
|
|
17
|
+
let originalFetch: typeof global.fetch;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
originalFetch = global.fetch;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
global.fetch = originalFetch;
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("constructor", () => {
|
|
29
|
+
it("should strip trailing slash from baseUrl", () => {
|
|
30
|
+
const transport = new ApiTransport({
|
|
31
|
+
baseUrl: "https://auth.example.com/",
|
|
32
|
+
verifySsl: true,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Verify by making a request and checking the URL
|
|
36
|
+
const mockFetch = vi.fn().mockResolvedValue(mockFetchResponse({ cfgNum: 1 }));
|
|
37
|
+
global.fetch = mockFetch as any;
|
|
38
|
+
|
|
39
|
+
transport.configInfo();
|
|
40
|
+
|
|
41
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
42
|
+
"https://auth.example.com/api/v1/config/latest",
|
|
43
|
+
expect.any(Object),
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("authentication", () => {
|
|
49
|
+
it("should add Basic auth header when basicAuth is configured", async () => {
|
|
50
|
+
const transport = new ApiTransport({
|
|
51
|
+
baseUrl: "https://auth.example.com",
|
|
52
|
+
basicAuth: { username: "admin", password: "secret" },
|
|
53
|
+
verifySsl: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const mockFetch = vi
|
|
57
|
+
.fn()
|
|
58
|
+
.mockResolvedValue(mockFetchResponse({ cfgNum: 1, cfgAuthor: "admin", cfgDate: 123456 }));
|
|
59
|
+
global.fetch = mockFetch as any;
|
|
60
|
+
|
|
61
|
+
await transport.configInfo();
|
|
62
|
+
|
|
63
|
+
const expectedAuth = Buffer.from("admin:secret").toString("base64");
|
|
64
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
65
|
+
expect.any(String),
|
|
66
|
+
expect.objectContaining({
|
|
67
|
+
headers: expect.objectContaining({
|
|
68
|
+
Authorization: `Basic ${expectedAuth}`,
|
|
69
|
+
}),
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should not add auth header when basicAuth is not configured", async () => {
|
|
75
|
+
const transport = new ApiTransport({
|
|
76
|
+
baseUrl: "https://auth.example.com",
|
|
77
|
+
verifySsl: true,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const mockFetch = vi
|
|
81
|
+
.fn()
|
|
82
|
+
.mockResolvedValue(mockFetchResponse({ cfgNum: 1, cfgAuthor: "admin", cfgDate: 123456 }));
|
|
83
|
+
global.fetch = mockFetch as any;
|
|
84
|
+
|
|
85
|
+
await transport.configInfo();
|
|
86
|
+
|
|
87
|
+
const callArgs = mockFetch.mock.calls[0][1] as RequestInit;
|
|
88
|
+
const headers = callArgs.headers as Record<string, string>;
|
|
89
|
+
expect(headers["Authorization"]).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("error handling", () => {
|
|
94
|
+
it("should throw error on HTTP error response", async () => {
|
|
95
|
+
const transport = new ApiTransport({
|
|
96
|
+
baseUrl: "https://auth.example.com",
|
|
97
|
+
verifySsl: true,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const mockFetch = vi
|
|
101
|
+
.fn()
|
|
102
|
+
.mockResolvedValue(mockFetchResponse({ error: "Not found" }, false, 404));
|
|
103
|
+
global.fetch = mockFetch as any;
|
|
104
|
+
|
|
105
|
+
await expect(transport.configInfo()).rejects.toThrow("API request failed");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("configInfo", () => {
|
|
110
|
+
it("should fetch config metadata", async () => {
|
|
111
|
+
const transport = new ApiTransport({
|
|
112
|
+
baseUrl: "https://auth.example.com",
|
|
113
|
+
verifySsl: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const configData = {
|
|
117
|
+
cfgNum: 5,
|
|
118
|
+
cfgAuthor: "admin",
|
|
119
|
+
cfgDate: 1234567890,
|
|
120
|
+
cfgLog: "Test update",
|
|
121
|
+
otherField: "ignored",
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const mockFetch = vi.fn().mockResolvedValue(mockFetchResponse(configData));
|
|
125
|
+
global.fetch = mockFetch as any;
|
|
126
|
+
|
|
127
|
+
const result = await transport.configInfo();
|
|
128
|
+
|
|
129
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
130
|
+
"https://auth.example.com/api/v1/config/latest",
|
|
131
|
+
expect.any(Object),
|
|
132
|
+
);
|
|
133
|
+
expect(result).toEqual({
|
|
134
|
+
cfgNum: 5,
|
|
135
|
+
cfgAuthor: "admin",
|
|
136
|
+
cfgDate: 1234567890,
|
|
137
|
+
cfgLog: "Test update",
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("configGet", () => {
|
|
143
|
+
it("should return only requested keys", async () => {
|
|
144
|
+
const transport = new ApiTransport({
|
|
145
|
+
baseUrl: "https://auth.example.com",
|
|
146
|
+
verifySsl: true,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const configData = {
|
|
150
|
+
cfgNum: 5,
|
|
151
|
+
portal: "https://portal.example.com",
|
|
152
|
+
domain: "example.com",
|
|
153
|
+
timeout: 3600,
|
|
154
|
+
extraField: "not requested",
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const mockFetch = vi.fn().mockResolvedValue(mockFetchResponse(configData));
|
|
158
|
+
global.fetch = mockFetch as any;
|
|
159
|
+
|
|
160
|
+
const result = await transport.configGet(["portal", "domain"]);
|
|
161
|
+
|
|
162
|
+
expect(result).toEqual({
|
|
163
|
+
portal: "https://portal.example.com",
|
|
164
|
+
domain: "example.com",
|
|
165
|
+
});
|
|
166
|
+
expect(result).not.toHaveProperty("timeout");
|
|
167
|
+
expect(result).not.toHaveProperty("extraField");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("configSet", () => {
|
|
172
|
+
it("should merge pairs and PUT updated config", async () => {
|
|
173
|
+
const transport = new ApiTransport({
|
|
174
|
+
baseUrl: "https://auth.example.com",
|
|
175
|
+
verifySsl: true,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const currentConfig = {
|
|
179
|
+
cfgNum: 5,
|
|
180
|
+
portal: "https://portal.example.com",
|
|
181
|
+
domain: "example.com",
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const mockFetch = vi
|
|
185
|
+
.fn()
|
|
186
|
+
.mockResolvedValueOnce(mockFetchResponse(currentConfig))
|
|
187
|
+
.mockResolvedValueOnce(mockFetchResponse({ success: true }));
|
|
188
|
+
global.fetch = mockFetch as any;
|
|
189
|
+
|
|
190
|
+
await transport.configSet({ domain: "newdomain.com", timeout: 7200 });
|
|
191
|
+
|
|
192
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
193
|
+
expect(mockFetch.mock.calls[0][0]).toBe("https://auth.example.com/api/v1/config/latest");
|
|
194
|
+
expect(mockFetch.mock.calls[1][0]).toBe("https://auth.example.com/api/v1/config");
|
|
195
|
+
expect(mockFetch.mock.calls[1][1].method).toBe("PUT");
|
|
196
|
+
|
|
197
|
+
const body = JSON.parse(mockFetch.mock.calls[1][1].body);
|
|
198
|
+
expect(body).toEqual({
|
|
199
|
+
cfgNum: 5,
|
|
200
|
+
portal: "https://portal.example.com",
|
|
201
|
+
domain: "newdomain.com",
|
|
202
|
+
timeout: 7200,
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("should add cfgLog when log parameter is provided", async () => {
|
|
207
|
+
const transport = new ApiTransport({
|
|
208
|
+
baseUrl: "https://auth.example.com",
|
|
209
|
+
verifySsl: true,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const currentConfig = {
|
|
213
|
+
cfgNum: 5,
|
|
214
|
+
portal: "https://portal.example.com",
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const mockFetch = vi
|
|
218
|
+
.fn()
|
|
219
|
+
.mockResolvedValueOnce(mockFetchResponse(currentConfig))
|
|
220
|
+
.mockResolvedValueOnce(mockFetchResponse({ success: true }));
|
|
221
|
+
global.fetch = mockFetch as any;
|
|
222
|
+
|
|
223
|
+
await transport.configSet({ domain: "example.com" }, "Added domain");
|
|
224
|
+
|
|
225
|
+
const body = JSON.parse(mockFetch.mock.calls[1][1].body);
|
|
226
|
+
expect(body.cfgLog).toBe("Added domain");
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("configAddKey", () => {
|
|
231
|
+
it("should add subkey to nested object", async () => {
|
|
232
|
+
const transport = new ApiTransport({
|
|
233
|
+
baseUrl: "https://auth.example.com",
|
|
234
|
+
verifySsl: true,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const currentConfig = {
|
|
238
|
+
cfgNum: 5,
|
|
239
|
+
locationRules: {
|
|
240
|
+
default: "accept",
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const mockFetch = vi
|
|
245
|
+
.fn()
|
|
246
|
+
.mockResolvedValueOnce(mockFetchResponse(currentConfig))
|
|
247
|
+
.mockResolvedValueOnce(mockFetchResponse({ success: true }));
|
|
248
|
+
global.fetch = mockFetch as any;
|
|
249
|
+
|
|
250
|
+
await transport.configAddKey("locationRules", "admin.example.com", '$uid eq "admin"');
|
|
251
|
+
|
|
252
|
+
const body = JSON.parse(mockFetch.mock.calls[1][1].body);
|
|
253
|
+
expect(body.locationRules).toEqual({
|
|
254
|
+
default: "accept",
|
|
255
|
+
"admin.example.com": '$uid eq "admin"',
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("should initialize key if it does not exist", async () => {
|
|
260
|
+
const transport = new ApiTransport({
|
|
261
|
+
baseUrl: "https://auth.example.com",
|
|
262
|
+
verifySsl: true,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const currentConfig = {
|
|
266
|
+
cfgNum: 5,
|
|
267
|
+
portal: "https://portal.example.com",
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const mockFetch = vi
|
|
271
|
+
.fn()
|
|
272
|
+
.mockResolvedValueOnce(mockFetchResponse(currentConfig))
|
|
273
|
+
.mockResolvedValueOnce(mockFetchResponse({ success: true }));
|
|
274
|
+
global.fetch = mockFetch as any;
|
|
275
|
+
|
|
276
|
+
await transport.configAddKey("locationRules", "default", "accept");
|
|
277
|
+
|
|
278
|
+
const body = JSON.parse(mockFetch.mock.calls[1][1].body);
|
|
279
|
+
expect(body.locationRules).toEqual({
|
|
280
|
+
default: "accept",
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("configDelKey", () => {
|
|
286
|
+
it("should remove subkey from nested object", async () => {
|
|
287
|
+
const transport = new ApiTransport({
|
|
288
|
+
baseUrl: "https://auth.example.com",
|
|
289
|
+
verifySsl: true,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const currentConfig = {
|
|
293
|
+
cfgNum: 5,
|
|
294
|
+
locationRules: {
|
|
295
|
+
default: "accept",
|
|
296
|
+
"admin.example.com": '$uid eq "admin"',
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const mockFetch = vi
|
|
301
|
+
.fn()
|
|
302
|
+
.mockResolvedValueOnce(mockFetchResponse(currentConfig))
|
|
303
|
+
.mockResolvedValueOnce(mockFetchResponse({ success: true }));
|
|
304
|
+
global.fetch = mockFetch as any;
|
|
305
|
+
|
|
306
|
+
await transport.configDelKey("locationRules", "admin.example.com");
|
|
307
|
+
|
|
308
|
+
const body = JSON.parse(mockFetch.mock.calls[1][1].body);
|
|
309
|
+
expect(body.locationRules).toEqual({
|
|
310
|
+
default: "accept",
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("should not fail when key does not exist", async () => {
|
|
315
|
+
const transport = new ApiTransport({
|
|
316
|
+
baseUrl: "https://auth.example.com",
|
|
317
|
+
verifySsl: true,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const currentConfig = {
|
|
321
|
+
cfgNum: 5,
|
|
322
|
+
locationRules: {
|
|
323
|
+
default: "accept",
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const mockFetch = vi.fn().mockResolvedValueOnce(mockFetchResponse(currentConfig));
|
|
328
|
+
global.fetch = mockFetch as any;
|
|
329
|
+
|
|
330
|
+
await transport.configDelKey("locationRules", "nonexistent");
|
|
331
|
+
|
|
332
|
+
// Should only fetch, not PUT
|
|
333
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe("configSave", () => {
|
|
338
|
+
it("should return config as JSON string", async () => {
|
|
339
|
+
const transport = new ApiTransport({
|
|
340
|
+
baseUrl: "https://auth.example.com",
|
|
341
|
+
verifySsl: true,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const configData = {
|
|
345
|
+
cfgNum: 5,
|
|
346
|
+
portal: "https://portal.example.com",
|
|
347
|
+
domain: "example.com",
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const mockFetch = vi.fn().mockResolvedValue(mockFetchResponse(configData));
|
|
351
|
+
global.fetch = mockFetch as any;
|
|
352
|
+
|
|
353
|
+
const result = await transport.configSave();
|
|
354
|
+
|
|
355
|
+
expect(result).toBe(JSON.stringify(configData, null, 2));
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe("configRestore", () => {
|
|
360
|
+
it("should PUT parsed JSON config", async () => {
|
|
361
|
+
const transport = new ApiTransport({
|
|
362
|
+
baseUrl: "https://auth.example.com",
|
|
363
|
+
verifySsl: true,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const configData = {
|
|
367
|
+
cfgNum: 5,
|
|
368
|
+
portal: "https://portal.example.com",
|
|
369
|
+
domain: "example.com",
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const mockFetch = vi.fn().mockResolvedValue(mockFetchResponse({ success: true }));
|
|
373
|
+
global.fetch = mockFetch as any;
|
|
374
|
+
|
|
375
|
+
await transport.configRestore(JSON.stringify(configData));
|
|
376
|
+
|
|
377
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
378
|
+
"https://auth.example.com/api/v1/config",
|
|
379
|
+
expect.objectContaining({
|
|
380
|
+
method: "PUT",
|
|
381
|
+
body: JSON.stringify(configData),
|
|
382
|
+
}),
|
|
383
|
+
);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe("configMerge", () => {
|
|
388
|
+
it("should deep merge JSON snippet with current config", async () => {
|
|
389
|
+
const transport = new ApiTransport({
|
|
390
|
+
baseUrl: "https://auth.example.com",
|
|
391
|
+
verifySsl: true,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const currentConfig = {
|
|
395
|
+
cfgNum: 5,
|
|
396
|
+
portal: "https://portal.example.com",
|
|
397
|
+
locationRules: {
|
|
398
|
+
default: "accept",
|
|
399
|
+
},
|
|
400
|
+
authParams: {
|
|
401
|
+
timeout: 3600,
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const snippet = {
|
|
406
|
+
locationRules: {
|
|
407
|
+
"admin.example.com": '$uid eq "admin"',
|
|
408
|
+
},
|
|
409
|
+
authParams: {
|
|
410
|
+
maxRetries: 3,
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const mockFetch = vi
|
|
415
|
+
.fn()
|
|
416
|
+
.mockResolvedValueOnce(mockFetchResponse(currentConfig))
|
|
417
|
+
.mockResolvedValueOnce(mockFetchResponse({ success: true }));
|
|
418
|
+
global.fetch = mockFetch as any;
|
|
419
|
+
|
|
420
|
+
await transport.configMerge(JSON.stringify(snippet));
|
|
421
|
+
|
|
422
|
+
const body = JSON.parse(mockFetch.mock.calls[1][1].body);
|
|
423
|
+
expect(body.locationRules).toEqual({
|
|
424
|
+
default: "accept",
|
|
425
|
+
"admin.example.com": '$uid eq "admin"',
|
|
426
|
+
});
|
|
427
|
+
expect(body.authParams).toEqual({
|
|
428
|
+
timeout: 3600,
|
|
429
|
+
maxRetries: 3,
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe("configRollback", () => {
|
|
435
|
+
it("should fetch previous config and PUT it", async () => {
|
|
436
|
+
const transport = new ApiTransport({
|
|
437
|
+
baseUrl: "https://auth.example.com",
|
|
438
|
+
verifySsl: true,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const currentConfig = {
|
|
442
|
+
cfgNum: 5,
|
|
443
|
+
portal: "https://portal.example.com",
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const previousConfig = {
|
|
447
|
+
cfgNum: 4,
|
|
448
|
+
portal: "https://old-portal.example.com",
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const mockFetch = vi
|
|
452
|
+
.fn()
|
|
453
|
+
.mockResolvedValueOnce(mockFetchResponse(currentConfig))
|
|
454
|
+
.mockResolvedValueOnce(mockFetchResponse(previousConfig))
|
|
455
|
+
.mockResolvedValueOnce(mockFetchResponse({ success: true }));
|
|
456
|
+
global.fetch = mockFetch as any;
|
|
457
|
+
|
|
458
|
+
await transport.configRollback();
|
|
459
|
+
|
|
460
|
+
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
461
|
+
expect(mockFetch.mock.calls[1][0]).toBe("https://auth.example.com/api/v1/config/4");
|
|
462
|
+
expect(mockFetch.mock.calls[2][0]).toBe("https://auth.example.com/api/v1/config");
|
|
463
|
+
|
|
464
|
+
const body = JSON.parse(mockFetch.mock.calls[2][1].body);
|
|
465
|
+
expect(body).toEqual(previousConfig);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("should throw error when at cfgNum 1", async () => {
|
|
469
|
+
const transport = new ApiTransport({
|
|
470
|
+
baseUrl: "https://auth.example.com",
|
|
471
|
+
verifySsl: true,
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const currentConfig = {
|
|
475
|
+
cfgNum: 1,
|
|
476
|
+
portal: "https://portal.example.com",
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const mockFetch = vi.fn().mockResolvedValue(mockFetchResponse(currentConfig));
|
|
480
|
+
global.fetch = mockFetch as any;
|
|
481
|
+
|
|
482
|
+
await expect(transport.configRollback()).rejects.toThrow(
|
|
483
|
+
"Cannot rollback: already at first config",
|
|
484
|
+
);
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
describe("configUpdateCache", () => {
|
|
489
|
+
it("should be a no-op", async () => {
|
|
490
|
+
const transport = new ApiTransport({
|
|
491
|
+
baseUrl: "https://auth.example.com",
|
|
492
|
+
verifySsl: true,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const mockFetch = vi.fn();
|
|
496
|
+
global.fetch = mockFetch as any;
|
|
497
|
+
|
|
498
|
+
await transport.configUpdateCache();
|
|
499
|
+
|
|
500
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
describe("sessionGet", () => {
|
|
505
|
+
it("should fetch session from global backend by default", async () => {
|
|
506
|
+
const transport = new ApiTransport({
|
|
507
|
+
baseUrl: "https://auth.example.com",
|
|
508
|
+
verifySsl: true,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const sessionData = {
|
|
512
|
+
_session_id: "abc123",
|
|
513
|
+
uid: "user1",
|
|
514
|
+
_startTime: 1234567890,
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const mockFetch = vi.fn().mockResolvedValue(mockFetchResponse(sessionData));
|
|
518
|
+
global.fetch = mockFetch as any;
|
|
519
|
+
|
|
520
|
+
const result = await transport.sessionGet("abc123");
|
|
521
|
+
|
|
522
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
523
|
+
"https://auth.example.com/api/v1/sessions/global/abc123",
|
|
524
|
+
expect.any(Object),
|
|
525
|
+
);
|
|
526
|
+
expect(result).toEqual(sessionData);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("should fetch session from specified backend", async () => {
|
|
530
|
+
const transport = new ApiTransport({
|
|
531
|
+
baseUrl: "https://auth.example.com",
|
|
532
|
+
verifySsl: true,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
const sessionData = {
|
|
536
|
+
_session_id: "xyz789",
|
|
537
|
+
sub: "user@example.com",
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const mockFetch = vi.fn().mockResolvedValue(mockFetchResponse(sessionData));
|
|
541
|
+
global.fetch = mockFetch as any;
|
|
542
|
+
|
|
543
|
+
await transport.sessionGet("xyz789", { backend: "oidc" });
|
|
544
|
+
|
|
545
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
546
|
+
"https://auth.example.com/api/v1/sessions/oidc/xyz789",
|
|
547
|
+
expect.any(Object),
|
|
548
|
+
);
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
describe("sessionSearch", () => {
|
|
553
|
+
it("should build query string with where clause", async () => {
|
|
554
|
+
const transport = new ApiTransport({
|
|
555
|
+
baseUrl: "https://auth.example.com",
|
|
556
|
+
verifySsl: true,
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const sessionsData = {
|
|
560
|
+
session1: { uid: "user1", _startTime: 1234567890 },
|
|
561
|
+
session2: { uid: "user2", _startTime: 1234567891 },
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const mockFetch = vi.fn().mockResolvedValue(mockFetchResponse(sessionsData));
|
|
565
|
+
global.fetch = mockFetch as any;
|
|
566
|
+
|
|
567
|
+
const result = await transport.sessionSearch({
|
|
568
|
+
where: { uid: "user1", ipAddr: "192.168.1.1" },
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
const expectedUrl =
|
|
572
|
+
"https://auth.example.com/api/v1/sessions/global?where=uid%3Duser1%20AND%20ipAddr%3D192.168.1.1";
|
|
573
|
+
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, expect.any(Object));
|
|
574
|
+
|
|
575
|
+
expect(result).toHaveLength(2);
|
|
576
|
+
expect(result[0]).toHaveProperty("id", "session1");
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it("should handle select parameter", async () => {
|
|
580
|
+
const transport = new ApiTransport({
|
|
581
|
+
baseUrl: "https://auth.example.com",
|
|
582
|
+
verifySsl: true,
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
const mockFetch = vi.fn().mockResolvedValue(mockFetchResponse({}));
|
|
586
|
+
global.fetch = mockFetch as any;
|
|
587
|
+
|
|
588
|
+
await transport.sessionSearch({
|
|
589
|
+
select: ["uid", "_startTime"],
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
expect(mockFetch.mock.calls[0][0]).toContain("select=uid,_startTime");
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it("should handle count parameter", async () => {
|
|
596
|
+
const transport = new ApiTransport({
|
|
597
|
+
baseUrl: "https://auth.example.com",
|
|
598
|
+
verifySsl: true,
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
const mockFetch = vi.fn().mockResolvedValue(mockFetchResponse({}));
|
|
602
|
+
global.fetch = mockFetch as any;
|
|
603
|
+
|
|
604
|
+
await transport.sessionSearch({
|
|
605
|
+
count: true,
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
expect(mockFetch.mock.calls[0][0]).toContain("count=1");
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
describe("sessionDelete", () => {
|
|
613
|
+
it("should DELETE each session ID", async () => {
|
|
614
|
+
const transport = new ApiTransport({
|
|
615
|
+
baseUrl: "https://auth.example.com",
|
|
616
|
+
verifySsl: true,
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
const mockFetch = vi.fn().mockResolvedValue(mockFetchResponse({ success: true }));
|
|
620
|
+
global.fetch = mockFetch as any;
|
|
621
|
+
|
|
622
|
+
await transport.sessionDelete(["session1", "session2", "session3"]);
|
|
623
|
+
|
|
624
|
+
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
625
|
+
expect(mockFetch.mock.calls[0][0]).toBe(
|
|
626
|
+
"https://auth.example.com/api/v1/sessions/global/session1",
|
|
627
|
+
);
|
|
628
|
+
expect(mockFetch.mock.calls[1][0]).toBe(
|
|
629
|
+
"https://auth.example.com/api/v1/sessions/global/session2",
|
|
630
|
+
);
|
|
631
|
+
expect(mockFetch.mock.calls[2][0]).toBe(
|
|
632
|
+
"https://auth.example.com/api/v1/sessions/global/session3",
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
mockFetch.mock.calls.forEach((call) => {
|
|
636
|
+
expect(call[1].method).toBe("DELETE");
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it("should use specified backend", async () => {
|
|
641
|
+
const transport = new ApiTransport({
|
|
642
|
+
baseUrl: "https://auth.example.com",
|
|
643
|
+
verifySsl: true,
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const mockFetch = vi.fn().mockResolvedValue(mockFetchResponse({ success: true }));
|
|
647
|
+
global.fetch = mockFetch as any;
|
|
648
|
+
|
|
649
|
+
await transport.sessionDelete(["session1"], { backend: "oidc" });
|
|
650
|
+
|
|
651
|
+
expect(mockFetch.mock.calls[0][0]).toBe(
|
|
652
|
+
"https://auth.example.com/api/v1/sessions/oidc/session1",
|
|
653
|
+
);
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
describe("deepMerge", () => {
|
|
658
|
+
it("should properly merge nested objects", async () => {
|
|
659
|
+
const transport = new ApiTransport({
|
|
660
|
+
baseUrl: "https://auth.example.com",
|
|
661
|
+
verifySsl: true,
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
const currentConfig = {
|
|
665
|
+
level1: {
|
|
666
|
+
level2: {
|
|
667
|
+
a: 1,
|
|
668
|
+
b: 2,
|
|
669
|
+
},
|
|
670
|
+
c: 3,
|
|
671
|
+
},
|
|
672
|
+
d: 4,
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
const snippet = {
|
|
676
|
+
level1: {
|
|
677
|
+
level2: {
|
|
678
|
+
b: 20,
|
|
679
|
+
e: 5,
|
|
680
|
+
},
|
|
681
|
+
f: 6,
|
|
682
|
+
},
|
|
683
|
+
g: 7,
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
const mockFetch = vi
|
|
687
|
+
.fn()
|
|
688
|
+
.mockResolvedValueOnce(mockFetchResponse(currentConfig))
|
|
689
|
+
.mockResolvedValueOnce(mockFetchResponse({ success: true }));
|
|
690
|
+
global.fetch = mockFetch as any;
|
|
691
|
+
|
|
692
|
+
await transport.configMerge(JSON.stringify(snippet));
|
|
693
|
+
|
|
694
|
+
const body = JSON.parse(mockFetch.mock.calls[1][1].body);
|
|
695
|
+
|
|
696
|
+
expect(body.level1.level2).toEqual({
|
|
697
|
+
a: 1,
|
|
698
|
+
b: 20,
|
|
699
|
+
e: 5,
|
|
700
|
+
});
|
|
701
|
+
expect(body.level1.c).toBe(3);
|
|
702
|
+
expect(body.level1.f).toBe(6);
|
|
703
|
+
expect(body.d).toBe(4);
|
|
704
|
+
expect(body.g).toBe(7);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it("should handle array replacement (not merge)", async () => {
|
|
708
|
+
const transport = new ApiTransport({
|
|
709
|
+
baseUrl: "https://auth.example.com",
|
|
710
|
+
verifySsl: true,
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
const currentConfig = {
|
|
714
|
+
arrayField: [1, 2, 3],
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
const snippet = {
|
|
718
|
+
arrayField: [4, 5],
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
const mockFetch = vi
|
|
722
|
+
.fn()
|
|
723
|
+
.mockResolvedValueOnce(mockFetchResponse(currentConfig))
|
|
724
|
+
.mockResolvedValueOnce(mockFetchResponse({ success: true }));
|
|
725
|
+
global.fetch = mockFetch as any;
|
|
726
|
+
|
|
727
|
+
await transport.configMerge(JSON.stringify(snippet));
|
|
728
|
+
|
|
729
|
+
const body = JSON.parse(mockFetch.mock.calls[1][1].body);
|
|
730
|
+
expect(body.arrayField).toEqual([4, 5]);
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
describe("execScript", () => {
|
|
735
|
+
it("should throw for execScript", async () => {
|
|
736
|
+
const transport = new ApiTransport({
|
|
737
|
+
baseUrl: "https://auth.example.com",
|
|
738
|
+
verifySsl: true,
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
await expect(transport.execScript("rotateOidcKeys", [])).rejects.toThrow(
|
|
742
|
+
"execScript is not supported via API. Use SSH or K8s mode.",
|
|
743
|
+
);
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
});
|