@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.
@@ -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 este no es null o undefined", () => {
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 parámetros básicos (string, number, boolean)", () => {
40
- const formParams = [
41
- { name: "param1", value: "valor1", type: "string" },
42
- { name: "param2", value: "42", type: "number" },
43
- { name: "param3", value: "true", type: "boolean" },
44
- ];
45
- const result = handleParametersToGenerateData(formParams, []);
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
- expect(result.parameters).toHaveLength(3);
48
- expect(result.environment).toHaveLength(3);
49
- expect(result.parameters[0]).toEqual({
50
- name: "param1",
51
- type: "string",
52
- defaultValue: "valor1",
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 parámetros de tipo secret", () => {
59
- const formParams = [{ name: "sec1", value: "secretValue", type: "secret" }];
60
- const result = handleParametersToGenerateData(formParams, []);
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
- expect(result.environment).toContainEqual({
63
- varName: "sec1",
64
- secret: "secretValue",
65
- });
66
- expect(result.resources).toContainEqual({
67
- secret: { name: "secretValue" },
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 parámetros de tipo file", () => {
72
- const formParams = [
73
- { name: "file1", value: "/path/to/file", type: "file" },
74
- ];
75
- const result = handleParametersToGenerateData(formParams, []);
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
- expect(result.fileSystem).toContainEqual({
78
- path: "/app/config/file1",
79
- param: "file1",
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 parámetros de tipo fileContent", () => {
84
- const formParams = [
85
- {
86
- name: "file1",
87
- value: "/custom/path",
88
- type: "fileContent",
89
- content: "contenido",
90
- },
91
- ];
92
- const result = handleParametersToGenerateData(formParams, []);
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 parámetros de tipo volume", () => {
99
- const formParams = [{ name: "vol1", value: "/data", type: "volume" }];
100
- const result = handleParametersToGenerateData(formParams, []);
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 parámetros de tipo serviceUrl", () => {
110
- const formParams = [
111
- { name: "url1", value: "https://api.example.com", type: "serviceUrl" },
112
- ];
113
- const result = handleParametersToGenerateData(formParams, []);
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 formParams = [{ name: "param1", value: "valor", type: "unknown" }];
121
-
122
- handleParametersToGenerateData(formParams, []);
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 recursos de tipo secret", () => {
129
- const formResources = [
130
- { name: "res1", type: "secret", value: "secretValue" },
131
- ];
132
- const result = handleParametersToGenerateData([], formResources);
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: "servicio1",
248
+ id: "s1",
141
249
  tenant: "tenant1",
142
250
  account: "account1",
143
251
  environment: "env1",
144
- name: "Servicio Prueba",
252
+ name: "TestService",
145
253
  logo: "",
146
254
  description: "",
147
255
  revisions: [],
148
- status: {
149
- message: "running",
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
- minReplicas: 1,
178
- maxReplicas: 3,
179
- lastDeployed: "",
180
- project: "project1",
273
+ project: "proj1",
181
274
  registry: "docker.io",
182
- imageName: "miImagen",
183
- entrypoint: "/iniciar",
184
- cmd: "start",
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 correctamente un objeto Service básico", () => {
193
- const service = createMockService({
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.serviceId).toBe("Servicio Prueba");
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("miImagen");
217
- expect(form.clientChannels).toEqual([{ name: "clientChannel1" }]);
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("maneja serverChannels con protocolo TCP", () => {
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("maneja scaling configuration", () => {
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
- role: [
240
- {
241
- name: "servicio1",
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: "servicio1",
389
+ serviceId: "svc1",
265
390
  cpuRequirements: 2,
266
391
  memoryRequirements: 512,
267
392
  registryUrl: "docker.io",
268
- imageTag: "miImagen",
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 un ServiceWithLocalComponentSpec básico", async () => {
280
- const form = createMockForm({
281
- clientChannels: [{ name: "clientChannel1" }],
282
- });
283
-
284
- const serviceSpec = await generateServiceSpec(form);
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
- expect(serviceSpec.type).toBe("service");
287
- expect(serviceSpec.subtype).toBe("service_with_local_component");
288
- expect(serviceSpec.tenantId).toBe("tenant1");
289
- expect(serviceSpec.local_components["servicio1"].channels.client).toEqual([
290
- "clientChannel1",
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 roles para canales HTTPS públicos", async () => {
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
- const serviceSpec = await generateServiceSpec(form);
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
- const inboundRole = serviceSpec.roles.find(
310
- (role) => role.name === "https1_inbound"
311
- );
312
- expect(inboundRole).toBeDefined();
313
- expect(inboundRole?.artifact.config.parameters).toContainEqual({
314
- name: "type",
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 roles para canales TCP públicos", async () => {
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
- const serviceSpec = await generateServiceSpec(form);
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
- const inboundRole = serviceSpec.roles.find(
336
- (role) => role.name === "tcp1_inbound"
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 volúmenes con diferentes tipos", async () => {
342
- const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
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
- const serviceSpec = await generateServiceSpec(form);
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
- const resources = serviceSpec.deploymentConfig.resources;
358
- expect(resources.some((r: any) => r.volume?.volume?.type === "volatile")).toBe(true);
359
- expect(resources.some((r: any) => r.volume?.volume?.type === "persistent")).toBe(true);
360
- expect(resources.some((r: any) => r.volume?.volume?.type === "nonreplicated")).toBe(true);
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
- consoleSpy.mockRestore();
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 scaling configuration", async () => {
539
+ it("maneja volúmenes sin kind (usa nonreplicated por defecto)", async () => {
366
540
  const form = createMockForm({
367
- scaling: {
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
- const serviceSpec = await generateServiceSpec(form);
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
- expect(serviceSpec.deploymentConfig.meta.scaling).toBeDefined();
378
- expect(serviceSpec.deploymentConfig.meta.scaling.simple.servicio1.max_replicas).toBe(5);
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 marketplaceItem = {
384
- type: "service" as const,
565
+ const mktItem = {
566
+ type: "service",
385
567
  domain: "kumori.systems",
386
- module: "marketplace/service",
568
+ module: "marketplace/svc",
387
569
  version: "1.2.3",
388
- serviceName: "MarketplaceService",
389
- deploymentData: { name: "marketplaceRole" },
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
- const serviceSpec = await generateServiceSpec(form, marketplaceItem as any);
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
- const marketplaceRole = serviceSpec.roles.find(
396
- (role) => role.name === "marketplaceRole"
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
- expect(marketplaceRole?.artifact.artifactKind).toBe("service");
399
- expect(serviceSpec.local_components).toEqual({});
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
- describe("deployService", () => {
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: "servicio1",
714
+ id: "s1",
426
715
  tenant: "tenant1",
427
716
  account: "account1",
428
717
  environment: "env1",
429
- name: "Servicio Prueba",
718
+ name: "TestService",
430
719
  logo: "",
431
720
  description: "",
432
721
  revisions: [],
433
- status: {
434
- message: "running",
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
- minReplicas: 1,
463
- maxReplicas: 3,
464
- lastDeployed: "",
465
- project: "project1",
739
+ project: "proj1",
466
740
  registry: "docker.io",
467
- imageName: "miImagen",
468
- entrypoint: "/iniciar",
469
- cmd: "start",
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 service = createMockService({ download: false });
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 service = createMockService({ download: true });
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 service = createMockService();
504
- const marketplaceItem = {
505
- type: "service" as const,
771
+ const mktItem = {
772
+ type: "service",
506
773
  domain: "kumori.systems",
507
- module: "marketplace/service",
774
+ module: "marketplace/svc",
508
775
  version: "1.0.0",
509
- serviceName: "TestService",
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
- const formData = await deployService(service, marketplaceItem as any);
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
+ });