@kumori/aurora-backend-handler 1.0.99 → 1.0.111
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/.gitlab-ci.yml +21 -21
- package/CHANGELOG.md +10 -0
- package/api/account-api-service.ts +31 -23
- package/api/deploy-service-helper.ts +84 -68
- package/api/environment-api-service.ts +15 -10
- package/api/marketplace-api-service.ts +31 -80
- package/api/resources-api-service.ts +20 -20
- package/api/service-api-service.ts +18 -14
- package/api/tenant-api-service.ts +130 -242
- package/api/user-api-service.ts +24 -9
- package/backend-handler.ts +374 -348
- package/developer.md +998 -0
- package/event-helper.ts +13 -3
- package/event-names.ts +3 -0
- package/helpers/account-helper.ts +12 -13
- package/helpers/environment-helper.ts +25 -29
- package/helpers/marketplace-helper.ts +109 -0
- package/helpers/resource-helper.ts +19 -1
- package/helpers/revision-helper.ts +68 -18
- package/helpers/service-helper.ts +49 -33
- package/helpers/tenant-helper.ts +0 -5
- package/helpers/token-helper.ts +74 -2
- package/helpers/user-helper.ts +72 -32
- package/jest.config.ts +1 -0
- package/package.json +3 -3
- package/test/backend-handler.test.ts +992 -730
- package/test/deploy-service-helper.test.ts +707 -273
- package/test/event-helper.test.ts +489 -104
- package/test/event-names.test.ts +186 -0
- package/websocket-manager.ts +394 -69
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
ServiceSpecForm,
|
|
9
9
|
} from "../api/deploy-service-helper";
|
|
10
10
|
|
|
11
|
+
jest.mock("@kumori/kumori-dsl-generator", () => ({
|
|
12
|
+
buildServiceDeploymentModule: jest.fn().mockResolvedValue(new Blob(["bundleDSL"])),
|
|
13
|
+
}));
|
|
14
|
+
|
|
11
15
|
jest.mock("@kumori/kumori-module-generator", () => ({
|
|
12
16
|
buildServiceDeploymentModuleWithLocalComponent: jest
|
|
13
17
|
.fn()
|
|
@@ -22,11 +26,14 @@ jest.mock("../websocket-manager", () => ({
|
|
|
22
26
|
getReferenceDomain: jest.fn().mockReturnValue("test-domain.com"),
|
|
23
27
|
}));
|
|
24
28
|
|
|
29
|
+
// ── withDefaultValue ─────────────────────────────────────────────────────────
|
|
30
|
+
|
|
25
31
|
describe("withDefaultValue", () => {
|
|
26
|
-
it("retorna el valor si
|
|
32
|
+
it("retorna el valor si no es null ni undefined", () => {
|
|
27
33
|
expect(withDefaultValue("valor", "default")).toBe("valor");
|
|
28
34
|
expect(withDefaultValue(0, 10)).toBe(0);
|
|
29
35
|
expect(withDefaultValue(false, true)).toBe(false);
|
|
36
|
+
expect(withDefaultValue("", "default")).toBe("");
|
|
30
37
|
});
|
|
31
38
|
|
|
32
39
|
it("retorna el valor por defecto si el valor es null o undefined", () => {
|
|
@@ -35,135 +42,224 @@ describe("withDefaultValue", () => {
|
|
|
35
42
|
});
|
|
36
43
|
});
|
|
37
44
|
|
|
45
|
+
// ── handleParametersToGenerateData ───────────────────────────────────────────
|
|
46
|
+
|
|
38
47
|
describe("handleParametersToGenerateData", () => {
|
|
39
|
-
it("maneja
|
|
40
|
-
const
|
|
41
|
-
{ name: "
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
it("maneja tipo string", () => {
|
|
49
|
+
const result = handleParametersToGenerateData(
|
|
50
|
+
[{ name: "p1", value: "v1", type: "string" }],
|
|
51
|
+
[],
|
|
52
|
+
);
|
|
53
|
+
expect(result.parameters).toContainEqual({ name: "p1", type: "string", defaultValue: "v1" });
|
|
54
|
+
expect(result.environment).toContainEqual({ varName: "p1", param: "p1" });
|
|
55
|
+
});
|
|
46
56
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
});
|
|
54
|
-
expect(result.parameters[1].defaultValue).toBe(42);
|
|
55
|
-
expect(result.parameters[2].defaultValue).toBe(true);
|
|
57
|
+
it("maneja tipo number", () => {
|
|
58
|
+
const result = handleParametersToGenerateData(
|
|
59
|
+
[{ name: "p1", value: "42", type: "number" }],
|
|
60
|
+
[],
|
|
61
|
+
);
|
|
62
|
+
expect(result.parameters[0].defaultValue).toBe(42);
|
|
63
|
+
expect(result.environment).toContainEqual({ varName: "p1", param: "p1" });
|
|
56
64
|
});
|
|
57
65
|
|
|
58
|
-
it("maneja
|
|
59
|
-
const
|
|
60
|
-
|
|
66
|
+
it("maneja tipo boolean con string 'true'", () => {
|
|
67
|
+
const result = handleParametersToGenerateData(
|
|
68
|
+
[{ name: "p1", value: "true", type: "boolean" }],
|
|
69
|
+
[],
|
|
70
|
+
);
|
|
71
|
+
expect(result.parameters[0].defaultValue).toBe(true);
|
|
72
|
+
});
|
|
61
73
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
});
|
|
74
|
+
it("maneja tipo bool con string 'false'", () => {
|
|
75
|
+
const result = handleParametersToGenerateData(
|
|
76
|
+
[{ name: "p1", value: "false", type: "bool" }],
|
|
77
|
+
[],
|
|
78
|
+
);
|
|
79
|
+
expect(result.parameters[0].defaultValue).toBe(false);
|
|
69
80
|
});
|
|
70
81
|
|
|
71
|
-
it("maneja
|
|
72
|
-
const
|
|
73
|
-
{ name: "
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
it("maneja tipo bool con valor boolean true", () => {
|
|
83
|
+
const result = handleParametersToGenerateData(
|
|
84
|
+
[{ name: "p1", value: true as any, type: "bool" }],
|
|
85
|
+
[],
|
|
86
|
+
);
|
|
87
|
+
expect(result.parameters[0].defaultValue).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("maneja tipo secret", () => {
|
|
91
|
+
const result = handleParametersToGenerateData(
|
|
92
|
+
[{ name: "sec1", value: "secretValue", type: "secret" }],
|
|
93
|
+
[],
|
|
94
|
+
);
|
|
95
|
+
expect(result.environment).toContainEqual({ varName: "sec1", secret: "sec1" });
|
|
96
|
+
expect(result.resources).toContainEqual({ secret: { name: "secretValue" } });
|
|
97
|
+
expect(result.parameters).toHaveLength(0);
|
|
98
|
+
});
|
|
76
99
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
100
|
+
it("maneja tipo clientChannel", () => {
|
|
101
|
+
const result = handleParametersToGenerateData(
|
|
102
|
+
[{ name: "test", value: "ClientChannel1aaa", type: "clientChannel" }],
|
|
103
|
+
[],
|
|
104
|
+
);
|
|
105
|
+
expect(result.environment).toContainEqual({
|
|
106
|
+
varName: "test",
|
|
107
|
+
value: "0.ClientChannel1aaa",
|
|
80
108
|
});
|
|
109
|
+
expect(result.parameters).toHaveLength(0);
|
|
110
|
+
expect(result.resources).toHaveLength(0);
|
|
81
111
|
});
|
|
82
112
|
|
|
83
|
-
it("maneja
|
|
84
|
-
const
|
|
85
|
-
{
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
113
|
+
it("maneja tipo file", () => {
|
|
114
|
+
const result = handleParametersToGenerateData(
|
|
115
|
+
[{ name: "myconfig", value: "/path/to/file", type: "file" }],
|
|
116
|
+
[],
|
|
117
|
+
);
|
|
118
|
+
expect(result.parameters[0].name).toBe("CONFIG_FILE_0");
|
|
119
|
+
expect(result.parameters[0].type).toBe("string");
|
|
120
|
+
expect(result.parameters[0].defaultValue).toBe("/path/to/file");
|
|
121
|
+
expect(result.fileSystem).toContainEqual({ path: "myconfig", param: "CONFIG_FILE_0" });
|
|
122
|
+
});
|
|
93
123
|
|
|
124
|
+
it("maneja tipo fileContent con content", () => {
|
|
125
|
+
const result = handleParametersToGenerateData(
|
|
126
|
+
[{ name: "f1", value: "/custom/path", type: "fileContent", content: "contenido" }],
|
|
127
|
+
[],
|
|
128
|
+
);
|
|
94
129
|
expect(result.parameters[0].defaultValue).toBe("contenido");
|
|
95
130
|
expect(result.fileSystem[0].path).toBe("/custom/path");
|
|
131
|
+
expect(result.fileSystem[0].param).toBe("CONFIG_FILE_0");
|
|
96
132
|
});
|
|
97
133
|
|
|
98
|
-
it("maneja
|
|
99
|
-
const
|
|
100
|
-
|
|
134
|
+
it("maneja tipo fileContent sin content (usa value)", () => {
|
|
135
|
+
const result = handleParametersToGenerateData(
|
|
136
|
+
[{ name: "f1", value: "/some/path", type: "fileContent" }],
|
|
137
|
+
[],
|
|
138
|
+
);
|
|
139
|
+
expect(result.parameters[0].defaultValue).toBe("/some/path");
|
|
140
|
+
expect(result.fileSystem[0].path).toBe("/some/path");
|
|
141
|
+
});
|
|
101
142
|
|
|
143
|
+
it("maneja tipo volume sin content (usa value)", () => {
|
|
144
|
+
const result = handleParametersToGenerateData(
|
|
145
|
+
[{ name: "vol1", value: "/data", type: "volume" }],
|
|
146
|
+
[],
|
|
147
|
+
);
|
|
102
148
|
expect(result.resources).toContainEqual({ volume: { name: "vol1" } });
|
|
103
|
-
expect(result.fileSystem).toContainEqual({
|
|
104
|
-
path: "/data",
|
|
105
|
-
resourceVolume: "vol1",
|
|
106
|
-
});
|
|
149
|
+
expect(result.fileSystem).toContainEqual({ path: "/data", resourceVolume: "vol1" });
|
|
107
150
|
});
|
|
108
151
|
|
|
109
|
-
it("maneja
|
|
110
|
-
const
|
|
111
|
-
{ name: "
|
|
112
|
-
|
|
113
|
-
|
|
152
|
+
it("maneja tipo volume con content", () => {
|
|
153
|
+
const result = handleParametersToGenerateData(
|
|
154
|
+
[{ name: "vol1", value: "/data", type: "volume", content: "/mount/path" }],
|
|
155
|
+
[],
|
|
156
|
+
);
|
|
157
|
+
expect(result.resources).toContainEqual({ volume: { name: "vol1" } });
|
|
158
|
+
expect(result.fileSystem).toContainEqual({ path: "/mount/path", resourceVolume: "vol1" });
|
|
159
|
+
});
|
|
114
160
|
|
|
161
|
+
it("maneja tipo serviceUrl", () => {
|
|
162
|
+
const result = handleParametersToGenerateData(
|
|
163
|
+
[{ name: "url1", value: "https://api.example.com", type: "serviceUrl" }],
|
|
164
|
+
[],
|
|
165
|
+
);
|
|
115
166
|
expect(result.parameters[0].defaultValue).toBe("https://api.example.com");
|
|
167
|
+
expect(result.environment).toContainEqual({ varName: "url1", param: "url1" });
|
|
116
168
|
});
|
|
117
169
|
|
|
118
|
-
it("maneja tipos desconocidos", () => {
|
|
119
|
-
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
170
|
+
it("maneja tipos desconocidos con warn", () => {
|
|
171
|
+
const consoleSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
172
|
+
const result = handleParametersToGenerateData(
|
|
173
|
+
[{ name: "p1", value: "v1", type: "unknown" }],
|
|
174
|
+
[],
|
|
175
|
+
);
|
|
124
176
|
expect(consoleSpy).toHaveBeenCalledWith("Unknown parameter type: unknown");
|
|
177
|
+
expect(result.parameters[0].type).toBe("string");
|
|
125
178
|
consoleSpy.mockRestore();
|
|
126
179
|
});
|
|
127
180
|
|
|
128
|
-
it("maneja
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
181
|
+
it("maneja tipos desconocidos con value undefined", () => {
|
|
182
|
+
const consoleSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
183
|
+
handleParametersToGenerateData(
|
|
184
|
+
[{ name: "p1", value: undefined as any, type: "unknown" }],
|
|
185
|
+
[],
|
|
186
|
+
);
|
|
187
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
188
|
+
consoleSpy.mockRestore();
|
|
189
|
+
});
|
|
133
190
|
|
|
191
|
+
it("maneja formResources de tipo secret", () => {
|
|
192
|
+
const result = handleParametersToGenerateData([], [
|
|
193
|
+
{ name: "res1", type: "secret", value: "secretValue" },
|
|
194
|
+
]);
|
|
134
195
|
expect(result.resources).toContainEqual({ secret: { name: "res1" } });
|
|
196
|
+
expect(result.environment).toContainEqual({ varName: "res1", secret: "res1" });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("maneja formResources de tipo secret con key (montado en path)", () => {
|
|
200
|
+
const result = handleParametersToGenerateData([], [
|
|
201
|
+
{ name: "mySuperSecretSecret", type: "secret", key: "/mysecret" },
|
|
202
|
+
]);
|
|
203
|
+
expect(result.resources).toContainEqual({ secret: { name: "mySuperSecretSecret" } });
|
|
204
|
+
expect(result.fileSystem).toContainEqual({ path: "/mysecret", resourceSecret: "mySuperSecretSecret" });
|
|
205
|
+
expect(result.environment).not.toContainEqual({ varName: "mySuperSecretSecret", secret: "mySuperSecretSecret" });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("maneja formResources de tipo volume", () => {
|
|
209
|
+
const result = handleParametersToGenerateData([], [
|
|
210
|
+
{ name: "vol1", type: "volume", key: "/mnt/data" },
|
|
211
|
+
]);
|
|
212
|
+
expect(result.resources).toContainEqual({ volume: { name: "vol1" } });
|
|
213
|
+
expect(result.fileSystem).toContainEqual({ path: "/mnt/data", resourceVolume: "vol1" });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("maneja formResources de tipo volume sin key", () => {
|
|
217
|
+
const result = handleParametersToGenerateData([], [
|
|
218
|
+
{ name: "vol1", type: "volume" },
|
|
219
|
+
]);
|
|
220
|
+
expect(result.fileSystem[0].path).toBe("");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("maneja formResources de tipo desconocido con warn", () => {
|
|
224
|
+
const consoleSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
225
|
+
handleParametersToGenerateData([], [{ name: "r1", type: "unknown" }]);
|
|
226
|
+
expect(consoleSpy).toHaveBeenCalledWith("Unknown resource type: unknown");
|
|
227
|
+
consoleSpy.mockRestore();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("maneja múltiples parámetros a la vez", () => {
|
|
231
|
+
const result = handleParametersToGenerateData(
|
|
232
|
+
[
|
|
233
|
+
{ name: "s1", value: "v1", type: "string" },
|
|
234
|
+
{ name: "n1", value: "10", type: "number" },
|
|
235
|
+
{ name: "b1", value: "true", type: "boolean" },
|
|
236
|
+
],
|
|
237
|
+
[],
|
|
238
|
+
);
|
|
239
|
+
expect(result.parameters).toHaveLength(3);
|
|
240
|
+
expect(result.environment).toHaveLength(3);
|
|
135
241
|
});
|
|
136
242
|
});
|
|
137
243
|
|
|
244
|
+
// ── transformServiceToForm ────────────────────────────────────────────────────
|
|
245
|
+
|
|
138
246
|
describe("transformServiceToForm", () => {
|
|
139
247
|
const createMockService = (overrides = {}): Service => ({
|
|
140
|
-
id: "
|
|
248
|
+
id: "s1",
|
|
141
249
|
tenant: "tenant1",
|
|
142
250
|
account: "account1",
|
|
143
251
|
environment: "env1",
|
|
144
|
-
name: "
|
|
252
|
+
name: "TestService",
|
|
145
253
|
logo: "",
|
|
146
254
|
description: "",
|
|
147
255
|
revisions: [],
|
|
148
|
-
status: {
|
|
149
|
-
|
|
150
|
-
timestamp: "2024-01-01",
|
|
151
|
-
args: [],
|
|
152
|
-
code: "200",
|
|
153
|
-
},
|
|
154
|
-
role: [{ name: "servicio1", instances: [] }],
|
|
256
|
+
status: { message: "running", timestamp: "2024-01-01", args: [], code: "200" },
|
|
257
|
+
role: [{ name: "s1", instances: [] }],
|
|
155
258
|
links: [],
|
|
156
259
|
resources: [],
|
|
157
260
|
parameters: [],
|
|
158
261
|
usage: {
|
|
159
|
-
current: {
|
|
160
|
-
cpu: 2,
|
|
161
|
-
memory: 512,
|
|
162
|
-
storage: 100,
|
|
163
|
-
volatileStorage: 10,
|
|
164
|
-
nonReplicatedStorage: 10,
|
|
165
|
-
persistentStorage: 20,
|
|
166
|
-
},
|
|
262
|
+
current: { cpu: 2, memory: 512, storage: 100, volatileStorage: 10, nonReplicatedStorage: 10, persistentStorage: 20 },
|
|
167
263
|
limit: {
|
|
168
264
|
cpu: { max: 4, min: 1 },
|
|
169
265
|
memory: { max: 1024, min: 256 },
|
|
@@ -174,14 +270,11 @@ describe("transformServiceToForm", () => {
|
|
|
174
270
|
},
|
|
175
271
|
cost: 100,
|
|
176
272
|
},
|
|
177
|
-
|
|
178
|
-
maxReplicas: 3,
|
|
179
|
-
lastDeployed: "",
|
|
180
|
-
project: "project1",
|
|
273
|
+
project: "proj1",
|
|
181
274
|
registry: "docker.io",
|
|
182
|
-
imageName: "
|
|
183
|
-
entrypoint: "/
|
|
184
|
-
cmd: "
|
|
275
|
+
imageName: "myImage",
|
|
276
|
+
entrypoint: "/start",
|
|
277
|
+
cmd: "run",
|
|
185
278
|
serverChannels: [],
|
|
186
279
|
clientChannels: [],
|
|
187
280
|
duplexChannels: [],
|
|
@@ -189,231 +282,427 @@ describe("transformServiceToForm", () => {
|
|
|
189
282
|
...overrides,
|
|
190
283
|
});
|
|
191
284
|
|
|
192
|
-
it("transforma
|
|
193
|
-
const
|
|
194
|
-
clientChannels: [{ name: "clientChannel1", from: "", to: "" }],
|
|
195
|
-
serverChannels: [
|
|
196
|
-
{
|
|
197
|
-
name: "serverChannel1",
|
|
198
|
-
from: "",
|
|
199
|
-
to: "",
|
|
200
|
-
port: 8080,
|
|
201
|
-
isPublic: true,
|
|
202
|
-
protocol: "http",
|
|
203
|
-
portNum: 80,
|
|
204
|
-
},
|
|
205
|
-
],
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
const form = transformServiceToForm(service);
|
|
209
|
-
|
|
285
|
+
it("transforma servicio básico correctamente", () => {
|
|
286
|
+
const form = transformServiceToForm(createMockService());
|
|
210
287
|
expect(form.tenantId).toBe("tenant1");
|
|
211
288
|
expect(form.accountId).toBe("account1");
|
|
212
|
-
expect(form.
|
|
289
|
+
expect(form.environmentId).toBe("env1");
|
|
290
|
+
expect(form.serviceId).toBe("TestService");
|
|
213
291
|
expect(form.cpuRequirements).toBe(4);
|
|
214
292
|
expect(form.memoryRequirements).toBe(1024);
|
|
215
293
|
expect(form.registryUrl).toBe("docker.io");
|
|
216
|
-
expect(form.imageTag).toBe("
|
|
217
|
-
expect(form.
|
|
294
|
+
expect(form.imageTag).toBe("myImage");
|
|
295
|
+
expect(form.defaultExecutable).toEqual({ cmd: "run", entryPoint: "/start" });
|
|
296
|
+
expect(form.clientChannels).toEqual([]);
|
|
297
|
+
expect(form.channels).toEqual([]);
|
|
298
|
+
expect(form.clientChannelsExtra).toEqual([]);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("transforma serverChannels con protocolo http a HTTPS", () => {
|
|
302
|
+
const service = createMockService({
|
|
303
|
+
serverChannels: [{ name: "web", port: 8080, isPublic: true, protocol: "http", portNum: 80 }],
|
|
304
|
+
});
|
|
305
|
+
const form = transformServiceToForm(service);
|
|
218
306
|
expect(form.channels[0].protocol).toBe("HTTPS");
|
|
307
|
+
expect(form.channels[0].containerPort).toBe(8080);
|
|
308
|
+
expect(form.channels[0].isPublic).toBe(true);
|
|
309
|
+
expect(form.channels[0].publicPort).toBe("80");
|
|
219
310
|
});
|
|
220
311
|
|
|
221
|
-
it("
|
|
312
|
+
it("transforma serverChannels con protocolo tcp a TCP", () => {
|
|
222
313
|
const service = createMockService({
|
|
223
|
-
serverChannels: [
|
|
224
|
-
{
|
|
225
|
-
name: "tcpChannel",
|
|
226
|
-
from: "",
|
|
227
|
-
to: "",
|
|
228
|
-
protocol: "tcp",
|
|
229
|
-
},
|
|
230
|
-
],
|
|
314
|
+
serverChannels: [{ name: "tcp1", protocol: "tcp" }],
|
|
231
315
|
});
|
|
316
|
+
const form = transformServiceToForm(service);
|
|
317
|
+
expect(form.channels[0].protocol).toBe("TCP");
|
|
318
|
+
});
|
|
232
319
|
|
|
320
|
+
it("transforma serverChannels con protocolo desconocido a TCP", () => {
|
|
321
|
+
const service = createMockService({
|
|
322
|
+
serverChannels: [{ name: "other", protocol: "grpc" }],
|
|
323
|
+
});
|
|
233
324
|
const form = transformServiceToForm(service);
|
|
234
325
|
expect(form.channels[0].protocol).toBe("TCP");
|
|
235
326
|
});
|
|
236
327
|
|
|
237
|
-
it("
|
|
328
|
+
it("transforma clientChannels y duplexChannels", () => {
|
|
329
|
+
const service = createMockService({
|
|
330
|
+
clientChannels: [{ name: "clientCh" }],
|
|
331
|
+
duplexChannels: [{ name: "duplexCh" }],
|
|
332
|
+
});
|
|
333
|
+
const form = transformServiceToForm(service);
|
|
334
|
+
expect(form.clientChannels).toEqual([{ name: "clientCh" }]);
|
|
335
|
+
expect(form.clientChannelsExtra).toEqual([{ name: "duplexCh" }]);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("transforma resources de tipo volume al fileSystem", () => {
|
|
238
339
|
const service = createMockService({
|
|
239
|
-
|
|
240
|
-
{
|
|
241
|
-
|
|
242
|
-
instances: [],
|
|
243
|
-
scalling: {
|
|
244
|
-
cpu: { up: "80", down: "20" },
|
|
245
|
-
memory: { up: "80", down: "20" },
|
|
246
|
-
instances: { max: 5, min: 1 },
|
|
247
|
-
histeresys: "10",
|
|
248
|
-
},
|
|
249
|
-
},
|
|
340
|
+
resources: [
|
|
341
|
+
{ name: "vol1", type: "volume", key: "/data" },
|
|
342
|
+
{ name: "sec1", type: "secret", value: "mySecret" },
|
|
250
343
|
],
|
|
251
344
|
});
|
|
345
|
+
const form = transformServiceToForm(service);
|
|
346
|
+
expect(form.fileSystem).toContainEqual({ path: "/data", resourceVolume: "vol1" });
|
|
347
|
+
expect(form.config.resources).toHaveLength(2);
|
|
348
|
+
});
|
|
252
349
|
|
|
350
|
+
it("transforma parameters", () => {
|
|
351
|
+
const service = createMockService({
|
|
352
|
+
parameters: [{ name: "env", value: "prod", type: "string" }],
|
|
353
|
+
});
|
|
354
|
+
const form = transformServiceToForm(service);
|
|
355
|
+
expect(form.config.parameters).toContainEqual({ name: "env", value: "prod", type: "string" });
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("transforma scaling configuration", () => {
|
|
359
|
+
const service = createMockService({
|
|
360
|
+
role: [{
|
|
361
|
+
name: "s1",
|
|
362
|
+
instances: [],
|
|
363
|
+
scalling: { cpu: { up: "80", down: "20" }, memory: { up: "80", down: "20" }, instances: { max: 5, min: 1 }, histeresys: "10" },
|
|
364
|
+
}],
|
|
365
|
+
});
|
|
253
366
|
const form = transformServiceToForm(service);
|
|
254
367
|
expect(form.scaling).toBeDefined();
|
|
255
368
|
expect(form.scaling?.instances.max).toBe(5);
|
|
256
369
|
});
|
|
370
|
+
|
|
371
|
+
it("transforma certificateResource, withMtls y caResource en channels", () => {
|
|
372
|
+
const service = createMockService({
|
|
373
|
+
serverChannels: [{ name: "web", protocol: "http", certificateResource: "mycert", withMtls: true, caResource: "myca" }],
|
|
374
|
+
});
|
|
375
|
+
const form = transformServiceToForm(service);
|
|
376
|
+
expect(form.channels[0].certificateResource).toBe("mycert");
|
|
377
|
+
expect(form.channels[0].withMtls).toBe(true);
|
|
378
|
+
expect(form.channels[0].caResource).toBe("myca");
|
|
379
|
+
});
|
|
257
380
|
});
|
|
258
381
|
|
|
382
|
+
// ── generateServiceSpec ───────────────────────────────────────────────────────
|
|
383
|
+
|
|
259
384
|
describe("generateServiceSpec", () => {
|
|
260
|
-
const createMockForm = (overrides = {}): ServiceSpecForm => ({
|
|
385
|
+
const createMockForm = (overrides: Partial<ServiceSpecForm> = {}): ServiceSpecForm => ({
|
|
261
386
|
tenantId: "tenant1",
|
|
262
387
|
accountId: "account1",
|
|
263
388
|
environmentId: "env1",
|
|
264
|
-
serviceId: "
|
|
389
|
+
serviceId: "svc1",
|
|
265
390
|
cpuRequirements: 2,
|
|
266
391
|
memoryRequirements: 512,
|
|
267
392
|
registryUrl: "docker.io",
|
|
268
|
-
imageTag: "
|
|
393
|
+
imageTag: "myImage",
|
|
269
394
|
clientChannels: [],
|
|
270
395
|
channels: [],
|
|
271
396
|
defaultExecutable: {},
|
|
272
|
-
config: {
|
|
273
|
-
parameters: [],
|
|
274
|
-
resources: [],
|
|
275
|
-
},
|
|
397
|
+
config: { parameters: [], resources: [] },
|
|
276
398
|
...overrides,
|
|
277
399
|
});
|
|
278
400
|
|
|
279
|
-
it("genera
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
401
|
+
it("genera spec básico sin channels ni recursos", async () => {
|
|
402
|
+
const spec = await generateServiceSpec(createMockForm());
|
|
403
|
+
expect(spec.type).toBe("service");
|
|
404
|
+
expect(spec.subtype).toBe("service_with_local_component");
|
|
405
|
+
expect(spec.tenantId).toBe("tenant1");
|
|
406
|
+
expect(spec.deploymentConfig.scale.detail["svc1"]).toBe(1);
|
|
407
|
+
expect(spec.local_components["svc1"]).toBeDefined();
|
|
408
|
+
});
|
|
285
409
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
expect(
|
|
289
|
-
expect(
|
|
290
|
-
"
|
|
291
|
-
|
|
410
|
+
it("genera spec con clientChannels", async () => {
|
|
411
|
+
const spec = await generateServiceSpec(createMockForm({ clientChannels: [{ name: "ch1" }] }));
|
|
412
|
+
expect(spec.local_components["svc1"].channels.client).toContain("ch1");
|
|
413
|
+
expect(spec.topology).toContainEqual(
|
|
414
|
+
expect.objectContaining({ clientRole: "svc1", clientChannel: "ch1", serverRole: "self" }),
|
|
415
|
+
);
|
|
292
416
|
});
|
|
293
417
|
|
|
294
|
-
it("genera inbound
|
|
418
|
+
it("genera inbound role para canal HTTPS público sin certificateResource (cert por defecto)", async () => {
|
|
295
419
|
const form = createMockForm({
|
|
296
|
-
channels: [
|
|
297
|
-
{
|
|
298
|
-
channelName: "https1",
|
|
299
|
-
containerPort: 443,
|
|
300
|
-
isPublic: true,
|
|
301
|
-
protocol: "HTTPS",
|
|
302
|
-
domain: "example.com",
|
|
303
|
-
},
|
|
304
|
-
],
|
|
420
|
+
channels: [{ channelName: "web", containerPort: 443, isPublic: true, protocol: "HTTPS", domain: "example.com" }],
|
|
305
421
|
});
|
|
422
|
+
const spec = await generateServiceSpec(form);
|
|
423
|
+
const inbound = spec.roles.find((r) => r.name === "web_inbound");
|
|
424
|
+
expect(inbound).toBeDefined();
|
|
425
|
+
expect(inbound?.artifact.config.parameters).toContainEqual({ name: "type", value: "https", type: "string" });
|
|
426
|
+
expect(spec.config.resources).toContainEqual({ certificate: { name: "main_inbound_servercert" } });
|
|
427
|
+
const certDeploy = spec.deploymentConfig.resources.find((r: any) => r.certificate?.name === "main_inbound_servercert");
|
|
428
|
+
expect(certDeploy).toBeDefined();
|
|
429
|
+
});
|
|
306
430
|
|
|
307
|
-
|
|
431
|
+
it("genera inbound role para canal HTTPS público con certificateResource personalizado", async () => {
|
|
432
|
+
const form = createMockForm({
|
|
433
|
+
channels: [{ channelName: "web", containerPort: 443, isPublic: true, protocol: "HTTPS", domain: "example.com", certificateResource: "my-cert" }],
|
|
434
|
+
});
|
|
435
|
+
const spec = await generateServiceSpec(form);
|
|
436
|
+
const certConfig = spec.config.resources.find((r: any) => r.certificate?.name === "web_cert");
|
|
437
|
+
expect(certConfig).toBeDefined();
|
|
438
|
+
const certDeploy = spec.deploymentConfig.resources.find((r: any) => r.certificate?.name === "web_cert");
|
|
439
|
+
expect(certDeploy).toBeDefined();
|
|
440
|
+
expect((certDeploy as any).certificate.resource).toBe("my-cert");
|
|
441
|
+
});
|
|
308
442
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
value: "https",
|
|
316
|
-
type: "string",
|
|
443
|
+
it("genera inbound role para canal HTTPS con mTLS y caResource", async () => {
|
|
444
|
+
const form = createMockForm({
|
|
445
|
+
channels: [{
|
|
446
|
+
channelName: "web", containerPort: 443, isPublic: true, protocol: "HTTPS",
|
|
447
|
+
domain: "example.com", certificateResource: "my-cert", withMtls: true, caResource: "my-ca",
|
|
448
|
+
}],
|
|
317
449
|
});
|
|
450
|
+
const spec = await generateServiceSpec(form);
|
|
451
|
+
const caConfig = spec.config.resources.find((r: any) => r.ca?.name === "web_ca");
|
|
452
|
+
expect(caConfig).toBeDefined();
|
|
453
|
+
const caDeploy = spec.deploymentConfig.resources.find((r: any) => r.ca?.name === "web_ca");
|
|
454
|
+
expect(caDeploy).toBeDefined();
|
|
318
455
|
});
|
|
319
456
|
|
|
320
|
-
it("genera inbound
|
|
457
|
+
it("genera inbound role para canal TCP público", async () => {
|
|
321
458
|
const form = createMockForm({
|
|
322
|
-
channels: [
|
|
323
|
-
{
|
|
324
|
-
channelName: "tcp1",
|
|
325
|
-
containerPort: 9000,
|
|
326
|
-
isPublic: true,
|
|
327
|
-
protocol: "TCP",
|
|
328
|
-
publicPort: "9000",
|
|
329
|
-
},
|
|
330
|
-
],
|
|
459
|
+
channels: [{ channelName: "tcp1", containerPort: 9000, isPublic: true, protocol: "TCP", publicPort: "9000" }],
|
|
331
460
|
});
|
|
461
|
+
const spec = await generateServiceSpec(form);
|
|
462
|
+
const inbound = spec.roles.find((r) => r.name === "tcp1_inbound");
|
|
463
|
+
expect(inbound).toBeDefined();
|
|
464
|
+
expect(inbound?.artifact.config.parameters).toContainEqual({ name: "type", value: "tcp", type: "string" });
|
|
465
|
+
const portDeploy = spec.deploymentConfig.resources.find((r: any) => r.port?.name === "tcp1_port");
|
|
466
|
+
expect(portDeploy).toBeDefined();
|
|
467
|
+
});
|
|
332
468
|
|
|
333
|
-
|
|
469
|
+
it("canal con containerPort 0 no añade port en channels.server", async () => {
|
|
470
|
+
const form = createMockForm({
|
|
471
|
+
channels: [{ channelName: "web", containerPort: 0, isPublic: false, protocol: "HTTPS" }],
|
|
472
|
+
});
|
|
473
|
+
const spec = await generateServiceSpec(form);
|
|
474
|
+
const ch = spec.channels.server.find((s: any) => s.name === "web");
|
|
475
|
+
expect(ch).toEqual({ name: "web" });
|
|
476
|
+
});
|
|
334
477
|
|
|
335
|
-
|
|
336
|
-
|
|
478
|
+
it("genera topology para canal público", async () => {
|
|
479
|
+
const form = createMockForm({
|
|
480
|
+
channels: [{ channelName: "web", containerPort: 443, isPublic: true, protocol: "HTTPS" }],
|
|
481
|
+
});
|
|
482
|
+
const spec = await generateServiceSpec(form);
|
|
483
|
+
expect(spec.topology).toContainEqual(
|
|
484
|
+
expect.objectContaining({ clientRole: "web_inbound", clientChannel: "inbound" }),
|
|
485
|
+
);
|
|
486
|
+
expect(spec.topology).toContainEqual(
|
|
487
|
+
expect.objectContaining({ clientRole: "self", clientChannel: "web" }),
|
|
337
488
|
);
|
|
338
|
-
expect(inboundRole).toBeDefined();
|
|
339
489
|
});
|
|
340
490
|
|
|
341
|
-
it("maneja
|
|
342
|
-
const
|
|
491
|
+
it("maneja recursos de tipo secret en config", async () => {
|
|
492
|
+
const form = createMockForm({
|
|
493
|
+
config: { parameters: [], resources: [{ name: "sec1", type: "secret", value: "my-secret" }] },
|
|
494
|
+
});
|
|
495
|
+
const spec = await generateServiceSpec(form);
|
|
496
|
+
expect(spec.config.resources).toContainEqual({ secret: { name: "sec1" } });
|
|
497
|
+
const secDeploy = spec.deploymentConfig.resources.find((r: any) => r.secret?.name === "sec1");
|
|
498
|
+
expect(secDeploy).toBeDefined();
|
|
499
|
+
});
|
|
343
500
|
|
|
501
|
+
it("maneja volumen sin size (referencia externa)", async () => {
|
|
344
502
|
const form = createMockForm({
|
|
345
|
-
config: {
|
|
346
|
-
parameters: [],
|
|
347
|
-
resources: [
|
|
348
|
-
{ name: "vol1", type: "volume", size: 5, kind: "volatile" },
|
|
349
|
-
{ name: "vol2", type: "volume", size: 10, kind: "persistent" },
|
|
350
|
-
{ name: "vol3", type: "volume", size: 15, kind: "nonreplicated" },
|
|
351
|
-
],
|
|
352
|
-
},
|
|
503
|
+
config: { parameters: [], resources: [{ name: "vol1", type: "volume", value: "existing-vol" }] },
|
|
353
504
|
});
|
|
505
|
+
const spec = await generateServiceSpec(form);
|
|
506
|
+
const volDeploy = spec.deploymentConfig.resources.find((r: any) => r.volume?.name === "vol1");
|
|
507
|
+
expect(volDeploy).toBeDefined();
|
|
508
|
+
expect((volDeploy as any).volume.resource).toBe("existing-vol");
|
|
509
|
+
});
|
|
354
510
|
|
|
355
|
-
|
|
511
|
+
it("maneja volúmenes con type volatile", async () => {
|
|
512
|
+
const form = createMockForm({
|
|
513
|
+
config: { parameters: [], resources: [{ name: "vol1", type: "volume", size: 5, kind: "volatile" } as any] },
|
|
514
|
+
});
|
|
515
|
+
const spec = await generateServiceSpec(form);
|
|
516
|
+
const volDeploy = spec.deploymentConfig.resources.find((r: any) => r.volume?.volume?.type === "volatile");
|
|
517
|
+
expect(volDeploy).toBeDefined();
|
|
518
|
+
expect((volDeploy as any).volume.volume.size).toBe(5000);
|
|
519
|
+
});
|
|
356
520
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
521
|
+
it("maneja volúmenes con type persistent", async () => {
|
|
522
|
+
const form = createMockForm({
|
|
523
|
+
config: { parameters: [], resources: [{ name: "vol2", type: "volume", size: 10, kind: "persistent" } as any] },
|
|
524
|
+
});
|
|
525
|
+
const spec = await generateServiceSpec(form);
|
|
526
|
+
const volDeploy = spec.deploymentConfig.resources.find((r: any) => r.volume?.volume?.type === "persistent");
|
|
527
|
+
expect(volDeploy).toBeDefined();
|
|
528
|
+
});
|
|
361
529
|
|
|
362
|
-
|
|
530
|
+
it("maneja volúmenes con type nonreplicated (default)", async () => {
|
|
531
|
+
const form = createMockForm({
|
|
532
|
+
config: { parameters: [], resources: [{ name: "vol3", type: "volume", size: 15, kind: "nonreplicated" } as any] },
|
|
533
|
+
});
|
|
534
|
+
const spec = await generateServiceSpec(form);
|
|
535
|
+
const volDeploy = spec.deploymentConfig.resources.find((r: any) => r.volume?.volume?.type === "nonreplicated");
|
|
536
|
+
expect(volDeploy).toBeDefined();
|
|
363
537
|
});
|
|
364
538
|
|
|
365
|
-
it("maneja
|
|
539
|
+
it("maneja volúmenes sin kind (usa nonreplicated por defecto)", async () => {
|
|
366
540
|
const form = createMockForm({
|
|
367
|
-
|
|
368
|
-
cpu: { up: "80", down: "20" },
|
|
369
|
-
memory: { up: "80", down: "20" },
|
|
370
|
-
instances: { min: 1, max: 5 },
|
|
371
|
-
histeresys: "10",
|
|
372
|
-
},
|
|
541
|
+
config: { parameters: [], resources: [{ name: "vol4", type: "volume", size: 20 } as any] },
|
|
373
542
|
});
|
|
543
|
+
const spec = await generateServiceSpec(form);
|
|
544
|
+
const volDeploy = spec.deploymentConfig.resources.find((r: any) => r.volume?.volume?.type === "nonreplicated");
|
|
545
|
+
expect(volDeploy).toBeDefined();
|
|
546
|
+
});
|
|
374
547
|
|
|
375
|
-
|
|
548
|
+
it("genera scaling configuration", async () => {
|
|
549
|
+
const form = createMockForm({
|
|
550
|
+
scaling: { cpu: { up: "80", down: "20" }, memory: { up: "80", down: "20" }, instances: { min: 1, max: 5 }, histeresys: "10" },
|
|
551
|
+
});
|
|
552
|
+
const spec = await generateServiceSpec(form);
|
|
553
|
+
expect(spec.deploymentConfig.meta.scaling).toBeDefined();
|
|
554
|
+
expect(spec.deploymentConfig.meta.scaling.simple["svc1"].max_replicas).toBe(5);
|
|
555
|
+
expect(spec.deploymentConfig.meta.scaling.simple["svc1"].hysteresis).toBe(10);
|
|
556
|
+
});
|
|
376
557
|
|
|
377
|
-
|
|
378
|
-
|
|
558
|
+
it("sin scaling: meta es objeto vacío", async () => {
|
|
559
|
+
const spec = await generateServiceSpec(createMockForm());
|
|
560
|
+
expect(spec.deploymentConfig.meta).toEqual({});
|
|
379
561
|
});
|
|
380
562
|
|
|
381
563
|
it("genera spec con marketplace item de tipo service", async () => {
|
|
382
564
|
const form = createMockForm();
|
|
383
|
-
const
|
|
384
|
-
type: "service"
|
|
565
|
+
const mktItem = {
|
|
566
|
+
type: "service",
|
|
385
567
|
domain: "kumori.systems",
|
|
386
|
-
module: "marketplace/
|
|
568
|
+
module: "marketplace/svc",
|
|
387
569
|
version: "1.2.3",
|
|
388
|
-
serviceName: "
|
|
389
|
-
deploymentData: { name: "
|
|
570
|
+
serviceName: "MktSvc",
|
|
571
|
+
deploymentData: { name: "mktRole" },
|
|
390
572
|
roles: ["role1", "role2"],
|
|
391
573
|
};
|
|
574
|
+
const spec = await generateServiceSpec(form, mktItem as any);
|
|
575
|
+
const role = spec.roles.find((r) => r.name === "mktRole");
|
|
576
|
+
expect(role?.artifact.artifactKind).toBe("service");
|
|
577
|
+
expect(role?.artifact.moduleVersion).toEqual([1, 2, 3]);
|
|
578
|
+
expect(spec.local_components).toEqual({});
|
|
579
|
+
});
|
|
392
580
|
|
|
393
|
-
|
|
581
|
+
it("genera spec con marketplace item de tipo component", async () => {
|
|
582
|
+
const form = createMockForm();
|
|
583
|
+
const mktItem = {
|
|
584
|
+
type: "component",
|
|
585
|
+
domain: "kumori.systems",
|
|
586
|
+
module: "marketplace/comp",
|
|
587
|
+
version: "2.0.0",
|
|
588
|
+
serviceName: "MktComp",
|
|
589
|
+
deploymentData: { name: "compRole" },
|
|
590
|
+
roles: [],
|
|
591
|
+
};
|
|
592
|
+
const spec = await generateServiceSpec(form, mktItem as any);
|
|
593
|
+
const role = spec.roles.find((r) => r.name === "compRole");
|
|
594
|
+
expect(role?.artifact.artifactKind).toBe("component");
|
|
595
|
+
expect(spec.local_components).toEqual({});
|
|
596
|
+
});
|
|
394
597
|
|
|
395
|
-
|
|
396
|
-
|
|
598
|
+
it("genera spec con parámetros de tipo string en roles", async () => {
|
|
599
|
+
const form = createMockForm({
|
|
600
|
+
config: {
|
|
601
|
+
parameters: [{ name: "KEY", value: "val", type: "string" }],
|
|
602
|
+
resources: [],
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
const spec = await generateServiceSpec(form);
|
|
606
|
+
const mainRole = spec.roles.find((r) => r.name === "svc1");
|
|
607
|
+
expect(mainRole?.artifact.config.parameters).toContainEqual(
|
|
608
|
+
expect.objectContaining({ name: "KEY", type: "string" }),
|
|
397
609
|
);
|
|
398
|
-
|
|
399
|
-
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it("genera spec con parámetros de tipo boolean en roles", async () => {
|
|
613
|
+
const form = createMockForm({
|
|
614
|
+
config: {
|
|
615
|
+
parameters: [{ name: "FLAG", value: "true", type: "boolean" }],
|
|
616
|
+
resources: [],
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
const spec = await generateServiceSpec(form);
|
|
620
|
+
const params = spec.deploymentConfig.parameters;
|
|
621
|
+
expect(params).toContainEqual(expect.objectContaining({ name: "FLAG", type: "bool" }));
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it("genera spec con parámetros de tipo number en deploymentConfig", async () => {
|
|
625
|
+
const form = createMockForm({
|
|
626
|
+
config: {
|
|
627
|
+
parameters: [{ name: "COUNT", value: "5", type: "number" }],
|
|
628
|
+
resources: [],
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
const spec = await generateServiceSpec(form);
|
|
632
|
+
const params = spec.deploymentConfig.parameters;
|
|
633
|
+
expect(params).toContainEqual(expect.objectContaining({ name: "COUNT", type: "number" }));
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it("genera spec con parámetros de tipo fileContent en deploymentConfig", async () => {
|
|
637
|
+
const form = createMockForm({
|
|
638
|
+
config: {
|
|
639
|
+
parameters: [{ name: "CFG", value: "v", type: "fileContent", content: "file-content" }],
|
|
640
|
+
resources: [],
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
const spec = await generateServiceSpec(form);
|
|
644
|
+
const params = spec.deploymentConfig.parameters;
|
|
645
|
+
expect(params).toContainEqual(expect.objectContaining({ type: "string", value: "file-content" }));
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it("genera spec con volatile volume en parameters (volatileVolumeResources)", async () => {
|
|
649
|
+
const form = createMockForm({
|
|
650
|
+
config: {
|
|
651
|
+
parameters: [{ name: "tempVol", value: "/tmp", type: "volume", size: 2 }],
|
|
652
|
+
resources: [],
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
const spec = await generateServiceSpec(form);
|
|
656
|
+
expect(spec.config.resources).toContainEqual({ volume: { name: "tempVol" } });
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it("genera spec con marketplaceItem y volatile volumes (llama createVolume)", async () => {
|
|
660
|
+
const { createVolume } = await import("../api/resources-api-service");
|
|
661
|
+
(createVolume as jest.Mock).mockResolvedValue({});
|
|
662
|
+
const form = createMockForm({
|
|
663
|
+
config: {
|
|
664
|
+
parameters: [],
|
|
665
|
+
resources: [{ name: "vol1", type: "volume", size: 3, kind: "volatile" } as any],
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
const mktItem = {
|
|
669
|
+
type: "service",
|
|
670
|
+
domain: "kumori",
|
|
671
|
+
module: "mod",
|
|
672
|
+
version: "1.0.0",
|
|
673
|
+
serviceName: "S",
|
|
674
|
+
deploymentData: { name: "r1" },
|
|
675
|
+
roles: [],
|
|
676
|
+
};
|
|
677
|
+
await generateServiceSpec(form, mktItem as any);
|
|
678
|
+
expect(createVolume).toHaveBeenCalled();
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it("lanza error si createVolume falla con marketplaceItem", async () => {
|
|
682
|
+
const { createVolume } = await import("../api/resources-api-service");
|
|
683
|
+
(createVolume as jest.Mock).mockRejectedValue(new Error("volume-error"));
|
|
684
|
+
const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
685
|
+
const form = createMockForm({
|
|
686
|
+
config: {
|
|
687
|
+
parameters: [],
|
|
688
|
+
resources: [{ name: "volFail", type: "volume", size: 3, kind: "volatile" } as any],
|
|
689
|
+
},
|
|
690
|
+
});
|
|
691
|
+
const mktItem: any = { type: "service", domain: "k", module: "m", version: "1.0.0", serviceName: "S", deploymentData: { name: "r" }, roles: [] };
|
|
692
|
+
await expect(generateServiceSpec(form, mktItem)).rejects.toThrow("volume-error");
|
|
693
|
+
consoleSpy.mockRestore();
|
|
400
694
|
});
|
|
401
695
|
});
|
|
402
696
|
|
|
403
|
-
|
|
697
|
+
// ── deployServiceHelper ───────────────────────────────────────────────────────
|
|
698
|
+
|
|
699
|
+
describe("deployServiceHelper", () => {
|
|
404
700
|
const mockClick = jest.fn();
|
|
405
|
-
|
|
406
|
-
beforeAll(() => {
|
|
407
|
-
(global as any).URL = {
|
|
408
|
-
createObjectURL: jest.fn(() => "blob:mock-url"),
|
|
409
|
-
};
|
|
410
701
|
|
|
702
|
+
beforeAll(() => {
|
|
703
|
+
(global as any).URL = { createObjectURL: jest.fn(() => "blob:mock-url") };
|
|
411
704
|
(global as any).document = {
|
|
412
|
-
createElement: jest.fn(() => ({
|
|
413
|
-
click: mockClick,
|
|
414
|
-
target: "",
|
|
415
|
-
href: "",
|
|
416
|
-
})),
|
|
705
|
+
createElement: jest.fn(() => ({ click: mockClick, target: "", href: "" })),
|
|
417
706
|
};
|
|
418
707
|
});
|
|
419
708
|
|
|
@@ -422,33 +711,21 @@ describe("deployService", () => {
|
|
|
422
711
|
});
|
|
423
712
|
|
|
424
713
|
const createMockService = (overrides = {}): Service => ({
|
|
425
|
-
id: "
|
|
714
|
+
id: "s1",
|
|
426
715
|
tenant: "tenant1",
|
|
427
716
|
account: "account1",
|
|
428
717
|
environment: "env1",
|
|
429
|
-
name: "
|
|
718
|
+
name: "TestService",
|
|
430
719
|
logo: "",
|
|
431
720
|
description: "",
|
|
432
721
|
revisions: [],
|
|
433
|
-
status: {
|
|
434
|
-
|
|
435
|
-
timestamp: "2024-01-01",
|
|
436
|
-
args: [],
|
|
437
|
-
code: "200",
|
|
438
|
-
},
|
|
439
|
-
role: [{ name: "servicio1", instances: [] }],
|
|
722
|
+
status: { message: "running", timestamp: "2024-01-01", args: [], code: "200" },
|
|
723
|
+
role: [{ name: "s1", instances: [] }],
|
|
440
724
|
links: [],
|
|
441
725
|
resources: [],
|
|
442
726
|
parameters: [],
|
|
443
727
|
usage: {
|
|
444
|
-
current: {
|
|
445
|
-
cpu: 2,
|
|
446
|
-
memory: 512,
|
|
447
|
-
storage: 100,
|
|
448
|
-
volatileStorage: 10,
|
|
449
|
-
nonReplicatedStorage: 10,
|
|
450
|
-
persistentStorage: 20,
|
|
451
|
-
},
|
|
728
|
+
current: { cpu: 2, memory: 512, storage: 100, volatileStorage: 10, nonReplicatedStorage: 10, persistentStorage: 20 },
|
|
452
729
|
limit: {
|
|
453
730
|
cpu: { max: 4, min: 1 },
|
|
454
731
|
memory: { max: 1024, min: 256 },
|
|
@@ -459,14 +736,11 @@ describe("deployService", () => {
|
|
|
459
736
|
},
|
|
460
737
|
cost: 100,
|
|
461
738
|
},
|
|
462
|
-
|
|
463
|
-
maxReplicas: 3,
|
|
464
|
-
lastDeployed: "",
|
|
465
|
-
project: "project1",
|
|
739
|
+
project: "proj1",
|
|
466
740
|
registry: "docker.io",
|
|
467
|
-
imageName: "
|
|
468
|
-
entrypoint: "/
|
|
469
|
-
cmd: "
|
|
741
|
+
imageName: "myImage",
|
|
742
|
+
entrypoint: "/start",
|
|
743
|
+
cmd: "run",
|
|
470
744
|
serverChannels: [],
|
|
471
745
|
clientChannels: [],
|
|
472
746
|
duplexChannels: [],
|
|
@@ -475,24 +749,18 @@ describe("deployService", () => {
|
|
|
475
749
|
});
|
|
476
750
|
|
|
477
751
|
it("retorna FormData con bundle y meta cuando download es false", async () => {
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
const formData = await deployService(service);
|
|
481
|
-
|
|
752
|
+
const formData = await deployService(createMockService({ download: false }));
|
|
482
753
|
expect(formData.get("bundle")).toBeTruthy();
|
|
483
754
|
expect(formData.get("meta")).toBeTruthy();
|
|
755
|
+
expect(formData.get("labels")).toBeTruthy();
|
|
484
756
|
expect(formData.get("comment")).toBe(" ");
|
|
485
|
-
|
|
486
757
|
const meta = JSON.parse(formData.get("meta") as string);
|
|
487
758
|
expect(meta.targetAccount).toBe("account1");
|
|
488
759
|
expect(meta.targetEnvironment).toBe("env1");
|
|
489
760
|
});
|
|
490
761
|
|
|
491
762
|
it("retorna FormData vacío y ejecuta descarga cuando download es true", async () => {
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
const formData = await deployService(service);
|
|
495
|
-
|
|
763
|
+
const formData = await deployService(createMockService({ download: true }));
|
|
496
764
|
expect((global as any).URL.createObjectURL).toHaveBeenCalled();
|
|
497
765
|
expect((global as any).document.createElement).toHaveBeenCalledWith("a");
|
|
498
766
|
expect(mockClick).toHaveBeenCalled();
|
|
@@ -500,19 +768,185 @@ describe("deployService", () => {
|
|
|
500
768
|
});
|
|
501
769
|
|
|
502
770
|
it("maneja deployment con marketplace item", async () => {
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
type: "service" as const,
|
|
771
|
+
const mktItem = {
|
|
772
|
+
type: "service",
|
|
506
773
|
domain: "kumori.systems",
|
|
507
|
-
module: "marketplace/
|
|
774
|
+
module: "marketplace/svc",
|
|
508
775
|
version: "1.0.0",
|
|
509
|
-
serviceName: "
|
|
776
|
+
serviceName: "TestSvc",
|
|
510
777
|
deploymentData: { name: "testRole" },
|
|
511
778
|
roles: ["role1"],
|
|
512
779
|
};
|
|
780
|
+
const formData = await deployService(createMockService(), mktItem as any);
|
|
781
|
+
expect(formData.get("bundle")).toBeTruthy();
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it("incluye project en labels cuando está definido", async () => {
|
|
785
|
+
const formData = await deployService(createMockService({ project: "myProject" }));
|
|
786
|
+
const labels = JSON.parse(formData.get("labels") as string);
|
|
787
|
+
expect(labels.project).toBe("myProject");
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it("maneja servicio con canales HTTPS públicos", async () => {
|
|
791
|
+
const service = createMockService({
|
|
792
|
+
serverChannels: [{ name: "web", protocol: "http", isPublic: true, portNum: 443 }],
|
|
793
|
+
});
|
|
794
|
+
const formData = await deployService(service);
|
|
795
|
+
expect(formData.get("bundle")).toBeTruthy();
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it("maneja servicio con canales TCP públicos", async () => {
|
|
799
|
+
const service = createMockService({
|
|
800
|
+
serverChannels: [{ name: "tcp1", protocol: "tcp", isPublic: true, portNum: 9000 }],
|
|
801
|
+
});
|
|
802
|
+
const formData = await deployService(service);
|
|
803
|
+
expect(formData.get("bundle")).toBeTruthy();
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it("maneja servicio con recursos secret y volume", async () => {
|
|
807
|
+
const service = createMockService({
|
|
808
|
+
resources: [
|
|
809
|
+
{ name: "sec1", type: "secret", value: "mySecret" },
|
|
810
|
+
{ name: "vol1", type: "volume", value: "myVol" },
|
|
811
|
+
],
|
|
812
|
+
});
|
|
813
|
+
const formData = await deployService(service);
|
|
814
|
+
expect(formData.get("bundle")).toBeTruthy();
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it("maneja parámetros de tipo boolean en generateServiceSpecDSL", async () => {
|
|
818
|
+
const service = createMockService({
|
|
819
|
+
parameters: [{ name: "FLAG", value: true, type: "boolean" }],
|
|
820
|
+
});
|
|
821
|
+
const formData = await deployService(service);
|
|
822
|
+
expect(formData.get("bundle")).toBeTruthy();
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it("maneja parámetros de tipo bool en generateServiceSpecDSL", async () => {
|
|
826
|
+
const service = createMockService({
|
|
827
|
+
parameters: [{ name: "FLAG2", value: "true", type: "bool" }],
|
|
828
|
+
});
|
|
829
|
+
const formData = await deployService(service);
|
|
830
|
+
expect(formData.get("bundle")).toBeTruthy();
|
|
831
|
+
});
|
|
513
832
|
|
|
514
|
-
|
|
833
|
+
it("maneja parámetros de tipo number en generateServiceSpecDSL", async () => {
|
|
834
|
+
const service = createMockService({
|
|
835
|
+
parameters: [{ name: "COUNT", value: 5, type: "number" }],
|
|
836
|
+
});
|
|
837
|
+
const formData = await deployService(service);
|
|
838
|
+
expect(formData.get("bundle")).toBeTruthy();
|
|
839
|
+
});
|
|
515
840
|
|
|
841
|
+
it("maneja parámetros de tipo file en generateServiceSpecDSL", async () => {
|
|
842
|
+
const service = createMockService({
|
|
843
|
+
parameters: [{ name: "/etc/config.conf", value: "/path/to/file", type: "file" }],
|
|
844
|
+
});
|
|
845
|
+
const formData = await deployService(service);
|
|
516
846
|
expect(formData.get("bundle")).toBeTruthy();
|
|
517
847
|
});
|
|
518
|
-
|
|
848
|
+
|
|
849
|
+
it("maneja parámetros de tipo string en generateServiceSpecDSL", async () => {
|
|
850
|
+
const service = createMockService({
|
|
851
|
+
parameters: [{ name: "KEY", value: "val", type: "string" }],
|
|
852
|
+
});
|
|
853
|
+
const formData = await deployService(service);
|
|
854
|
+
expect(formData.get("bundle")).toBeTruthy();
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it("maneja parámetros de tipo secret en generateServiceSpecDSL", async () => {
|
|
858
|
+
const service = createMockService({
|
|
859
|
+
parameters: [{ name: "SEC", value: "secRef", type: "secret" }],
|
|
860
|
+
});
|
|
861
|
+
const formData = await deployService(service);
|
|
862
|
+
expect(formData.get("bundle")).toBeTruthy();
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
it("maneja parámetros de tipo clientChannel en generateServiceSpecDSL", async () => {
|
|
866
|
+
const service = createMockService({
|
|
867
|
+
parameters: [{ name: "test", value: "ClientChannel1aaa", type: "clientChannel" }],
|
|
868
|
+
});
|
|
869
|
+
const formData = await deployService(service);
|
|
870
|
+
expect(formData.get("bundle")).toBeTruthy();
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
it("maneja resources de tipo volume sin size en generateServiceSpecDSL", async () => {
|
|
874
|
+
const service = createMockService({
|
|
875
|
+
resources: [{ name: "vol1", type: "volume", value: "existing-vol" }],
|
|
876
|
+
});
|
|
877
|
+
const formData = await deployService(service);
|
|
878
|
+
expect(formData.get("bundle")).toBeTruthy();
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
it("maneja resources de tipo volume con size persistent en generateServiceSpecDSL", async () => {
|
|
882
|
+
const service = createMockService({
|
|
883
|
+
resources: [{ name: "vol1", type: "volume", maxItems: 10, kind: "persistent" } as any],
|
|
884
|
+
});
|
|
885
|
+
const formData = await deployService(service);
|
|
886
|
+
expect(formData.get("bundle")).toBeTruthy();
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it("maneja resources de tipo volume con size volatile en generateServiceSpecDSL", async () => {
|
|
890
|
+
const service = createMockService({
|
|
891
|
+
resources: [{ name: "vol2", type: "volume", maxItems: 5, kind: "volatile" } as any],
|
|
892
|
+
});
|
|
893
|
+
const formData = await deployService(service);
|
|
894
|
+
expect(formData.get("bundle")).toBeTruthy();
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
it("maneja canal HTTPS público sin certificateResource en generateServiceSpecDSL", async () => {
|
|
898
|
+
const service = createMockService({
|
|
899
|
+
serverChannels: [{ name: "web", protocol: "http", isPublic: true, portNum: 443 }],
|
|
900
|
+
});
|
|
901
|
+
const formData = await deployService(service);
|
|
902
|
+
expect(formData.get("bundle")).toBeTruthy();
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it("maneja canal HTTPS público con certificateResource en generateServiceSpecDSL", async () => {
|
|
906
|
+
const service = createMockService({
|
|
907
|
+
serverChannels: [{ name: "web", protocol: "http", isPublic: true, certificateResource: "my-cert" }],
|
|
908
|
+
});
|
|
909
|
+
const formData = await deployService(service);
|
|
910
|
+
expect(formData.get("bundle")).toBeTruthy();
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
it("maneja canal HTTPS con mTLS en generateServiceSpecDSL", async () => {
|
|
914
|
+
const service = createMockService({
|
|
915
|
+
serverChannels: [{ name: "web", protocol: "http", isPublic: true, withMtls: true, caResource: "my-ca", certificateResource: "my-cert" }],
|
|
916
|
+
});
|
|
917
|
+
const formData = await deployService(service);
|
|
918
|
+
expect(formData.get("bundle")).toBeTruthy();
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it("maneja canal TCP público en generateServiceSpecDSL", async () => {
|
|
922
|
+
const service = createMockService({
|
|
923
|
+
serverChannels: [{ name: "tcp1", protocol: "tcp", isPublic: true }],
|
|
924
|
+
});
|
|
925
|
+
const formData = await deployService(service);
|
|
926
|
+
expect(formData.get("bundle")).toBeTruthy();
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
it("maneja clientChannels en generateServiceSpecDSL", async () => {
|
|
930
|
+
const service = createMockService({
|
|
931
|
+
clientChannels: [{ name: "clientCh" }],
|
|
932
|
+
});
|
|
933
|
+
const formData = await deployService(service);
|
|
934
|
+
expect(formData.get("bundle")).toBeTruthy();
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
it("maneja marketplace item con package (hasMarketplacePackage)", async () => {
|
|
938
|
+
const mktItem = {
|
|
939
|
+
type: "service",
|
|
940
|
+
domain: "kumori.systems",
|
|
941
|
+
module: "marketplace/svc",
|
|
942
|
+
version: "2.1.0",
|
|
943
|
+
serviceName: "PkgSvc",
|
|
944
|
+
artifact: "my-artifact",
|
|
945
|
+
package: "s3://my-bucket/pkg.zip",
|
|
946
|
+
deploymentData: { name: "pkgRole" },
|
|
947
|
+
roles: ["role1"],
|
|
948
|
+
};
|
|
949
|
+
const formData = await deployService(createMockService(), mktItem as any);
|
|
950
|
+
expect(formData.get("bundle")).toBeTruthy();
|
|
951
|
+
});
|
|
952
|
+
});
|