@masonator/coolify-mcp 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,6 +20,8 @@ describe('CoolifyClient', () => {
20
20
  ip: '192.168.1.1',
21
21
  user: 'root',
22
22
  port: 22,
23
+ status: 'running',
24
+ is_reachable: true,
23
25
  created_at: '2024-01-01',
24
26
  updated_at: '2024-01-01',
25
27
  },
@@ -51,6 +53,45 @@ describe('CoolifyClient', () => {
51
53
  name: 'test-service',
52
54
  type: 'code-server',
53
55
  status: 'running',
56
+ domains: ['test.example.com'],
57
+ created_at: '2024-01-01',
58
+ updated_at: '2024-01-01',
59
+ };
60
+ const mockApplication = {
61
+ id: 1,
62
+ uuid: 'app-uuid',
63
+ name: 'test-app',
64
+ status: 'running',
65
+ fqdn: 'https://app.example.com',
66
+ git_repository: 'https://github.com/user/repo',
67
+ git_branch: 'main',
68
+ created_at: '2024-01-01',
69
+ updated_at: '2024-01-01',
70
+ };
71
+ const mockDatabase = {
72
+ id: 1,
73
+ uuid: 'db-uuid',
74
+ name: 'test-db',
75
+ type: 'postgresql',
76
+ status: 'running',
77
+ is_public: false,
78
+ created_at: '2024-01-01',
79
+ updated_at: '2024-01-01',
80
+ };
81
+ const mockDeployment = {
82
+ id: 1,
83
+ uuid: 'dep-uuid',
84
+ deployment_uuid: 'dep-123',
85
+ application_name: 'test-app',
86
+ status: 'finished',
87
+ created_at: '2024-01-01',
88
+ updated_at: '2024-01-01',
89
+ };
90
+ const mockProject = {
91
+ id: 1,
92
+ uuid: 'proj-uuid',
93
+ name: 'test-project',
94
+ description: 'A test project',
54
95
  created_at: '2024-01-01',
55
96
  updated_at: '2024-01-01',
56
97
  };
@@ -98,6 +139,24 @@ describe('CoolifyClient', () => {
98
139
  mockFetch.mockResolvedValueOnce(mockResponse(errorResponse, false, 404));
99
140
  await expect(client.listServers()).rejects.toThrow('Resource not found');
100
141
  });
142
+ it('should support pagination options', async () => {
143
+ mockFetch.mockResolvedValueOnce(mockResponse(mockServers));
144
+ await client.listServers({ page: 2, per_page: 10 });
145
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/servers?page=2&per_page=10', expect.any(Object));
146
+ });
147
+ it('should return summary when requested', async () => {
148
+ mockFetch.mockResolvedValueOnce(mockResponse(mockServers));
149
+ const result = await client.listServers({ summary: true });
150
+ expect(result).toEqual([
151
+ {
152
+ uuid: 'test-uuid',
153
+ name: 'test-server',
154
+ ip: '192.168.1.1',
155
+ status: 'running',
156
+ is_reachable: true,
157
+ },
158
+ ]);
159
+ });
101
160
  });
102
161
  describe('getServer', () => {
103
162
  it('should get server info', async () => {
@@ -160,6 +219,26 @@ describe('CoolifyClient', () => {
160
219
  body: JSON.stringify(createData),
161
220
  }));
162
221
  });
222
+ it('should create a service with docker_compose_raw instead of type', async () => {
223
+ const responseData = {
224
+ uuid: 'compose-uuid',
225
+ domains: ['custom.example.com'],
226
+ };
227
+ mockFetch.mockResolvedValueOnce(mockResponse(responseData));
228
+ const createData = {
229
+ name: 'custom-compose-service',
230
+ project_uuid: 'project-uuid',
231
+ environment_uuid: 'env-uuid',
232
+ server_uuid: 'server-uuid',
233
+ docker_compose_raw: 'dmVyc2lvbjogIjMiCnNlcnZpY2VzOgogIGFwcDoKICAgIGltYWdlOiBuZ2lueA==',
234
+ };
235
+ const result = await client.createService(createData);
236
+ expect(result).toEqual(responseData);
237
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services', expect.objectContaining({
238
+ method: 'POST',
239
+ body: JSON.stringify(createData),
240
+ }));
241
+ });
163
242
  });
164
243
  describe('deleteService', () => {
165
244
  it('should delete a service', async () => {
@@ -180,6 +259,18 @@ describe('CoolifyClient', () => {
180
259
  method: 'DELETE',
181
260
  }));
182
261
  });
262
+ it('should delete a service with all options', async () => {
263
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Service deleted' }));
264
+ await client.deleteService('test-uuid', {
265
+ deleteConfigurations: true,
266
+ deleteVolumes: true,
267
+ dockerCleanup: true,
268
+ deleteConnectedNetworks: true,
269
+ });
270
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/test-uuid?delete_configurations=true&delete_volumes=true&docker_cleanup=true&delete_connected_networks=true', expect.objectContaining({
271
+ method: 'DELETE',
272
+ }));
273
+ });
183
274
  });
184
275
  describe('applications', () => {
185
276
  it('should list applications', async () => {
@@ -282,5 +373,653 @@ describe('CoolifyClient', () => {
282
373
  const result = await client.deleteServer('test-uuid');
283
374
  expect(result).toEqual({});
284
375
  });
376
+ it('should handle API errors without message', async () => {
377
+ mockFetch.mockResolvedValueOnce(mockResponse({}, false, 500));
378
+ await expect(client.listServers()).rejects.toThrow('HTTP 500: Error');
379
+ });
380
+ });
381
+ // =========================================================================
382
+ // Server endpoints - additional coverage
383
+ // =========================================================================
384
+ describe('server operations', () => {
385
+ it('should create a server', async () => {
386
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-server-uuid' }));
387
+ const result = await client.createServer({
388
+ name: 'new-server',
389
+ ip: '10.0.0.1',
390
+ private_key_uuid: 'key-uuid',
391
+ });
392
+ expect(result).toEqual({ uuid: 'new-server-uuid' });
393
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/servers', expect.objectContaining({
394
+ method: 'POST',
395
+ }));
396
+ });
397
+ it('should update a server', async () => {
398
+ mockFetch.mockResolvedValueOnce(mockResponse({ ...mockServerInfo, name: 'updated-server' }));
399
+ const result = await client.updateServer('test-uuid', { name: 'updated-server' });
400
+ expect(result.name).toBe('updated-server');
401
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/servers/test-uuid', expect.objectContaining({ method: 'PATCH' }));
402
+ });
403
+ it('should get server domains', async () => {
404
+ const mockDomains = [{ domain: 'example.com', ip: '1.2.3.4' }];
405
+ mockFetch.mockResolvedValueOnce(mockResponse(mockDomains));
406
+ const result = await client.getServerDomains('test-uuid');
407
+ expect(result).toEqual(mockDomains);
408
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/servers/test-uuid/domains', expect.any(Object));
409
+ });
410
+ it('should validate a server', async () => {
411
+ const mockValidation = { valid: true };
412
+ mockFetch.mockResolvedValueOnce(mockResponse(mockValidation));
413
+ const result = await client.validateServer('test-uuid');
414
+ expect(result).toEqual(mockValidation);
415
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/servers/test-uuid/validate', expect.any(Object));
416
+ });
417
+ });
418
+ // =========================================================================
419
+ // Project endpoints
420
+ // =========================================================================
421
+ describe('projects', () => {
422
+ it('should list projects', async () => {
423
+ mockFetch.mockResolvedValueOnce(mockResponse([mockProject]));
424
+ const result = await client.listProjects();
425
+ expect(result).toEqual([mockProject]);
426
+ });
427
+ it('should list projects with pagination', async () => {
428
+ mockFetch.mockResolvedValueOnce(mockResponse([mockProject]));
429
+ await client.listProjects({ page: 1, per_page: 5 });
430
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/projects?page=1&per_page=5', expect.any(Object));
431
+ });
432
+ it('should list projects with summary', async () => {
433
+ mockFetch.mockResolvedValueOnce(mockResponse([mockProject]));
434
+ const result = await client.listProjects({ summary: true });
435
+ expect(result).toEqual([
436
+ {
437
+ uuid: 'proj-uuid',
438
+ name: 'test-project',
439
+ description: 'A test project',
440
+ },
441
+ ]);
442
+ });
443
+ it('should get a project', async () => {
444
+ mockFetch.mockResolvedValueOnce(mockResponse(mockProject));
445
+ const result = await client.getProject('proj-uuid');
446
+ expect(result).toEqual(mockProject);
447
+ });
448
+ it('should create a project', async () => {
449
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-proj-uuid' }));
450
+ const result = await client.createProject({ name: 'new-project' });
451
+ expect(result).toEqual({ uuid: 'new-proj-uuid' });
452
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/projects', expect.objectContaining({ method: 'POST' }));
453
+ });
454
+ it('should update a project', async () => {
455
+ mockFetch.mockResolvedValueOnce(mockResponse({ ...mockProject, name: 'updated-project' }));
456
+ const result = await client.updateProject('proj-uuid', { name: 'updated-project' });
457
+ expect(result.name).toBe('updated-project');
458
+ });
459
+ it('should delete a project', async () => {
460
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deleted' }));
461
+ const result = await client.deleteProject('proj-uuid');
462
+ expect(result).toEqual({ message: 'Deleted' });
463
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/projects/proj-uuid', expect.objectContaining({ method: 'DELETE' }));
464
+ });
465
+ });
466
+ // =========================================================================
467
+ // Environment endpoints
468
+ // =========================================================================
469
+ describe('environments', () => {
470
+ const mockEnvironment = {
471
+ id: 1,
472
+ uuid: 'env-uuid',
473
+ name: 'production',
474
+ project_uuid: 'proj-uuid',
475
+ created_at: '2024-01-01',
476
+ updated_at: '2024-01-01',
477
+ };
478
+ it('should list project environments', async () => {
479
+ mockFetch.mockResolvedValueOnce(mockResponse([mockEnvironment]));
480
+ const result = await client.listProjectEnvironments('proj-uuid');
481
+ expect(result).toEqual([mockEnvironment]);
482
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/projects/proj-uuid/environments', expect.any(Object));
483
+ });
484
+ it('should get a project environment', async () => {
485
+ mockFetch.mockResolvedValueOnce(mockResponse(mockEnvironment));
486
+ const result = await client.getProjectEnvironment('proj-uuid', 'production');
487
+ expect(result).toEqual(mockEnvironment);
488
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/projects/proj-uuid/production', expect.any(Object));
489
+ });
490
+ it('should create a project environment', async () => {
491
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-env-uuid' }));
492
+ const result = await client.createProjectEnvironment('proj-uuid', { name: 'staging' });
493
+ expect(result).toEqual({ uuid: 'new-env-uuid' });
494
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/projects/proj-uuid/environments', expect.objectContaining({ method: 'POST' }));
495
+ });
496
+ it('should delete a project environment', async () => {
497
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deleted' }));
498
+ const result = await client.deleteProjectEnvironment('env-uuid');
499
+ expect(result).toEqual({ message: 'Deleted' });
500
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/projects/environments/env-uuid', expect.objectContaining({ method: 'DELETE' }));
501
+ });
502
+ });
503
+ // =========================================================================
504
+ // Application endpoints - extended coverage
505
+ // =========================================================================
506
+ describe('applications extended', () => {
507
+ it('should list applications with pagination', async () => {
508
+ mockFetch.mockResolvedValueOnce(mockResponse([mockApplication]));
509
+ await client.listApplications({ page: 1, per_page: 20 });
510
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications?page=1&per_page=20', expect.any(Object));
511
+ });
512
+ it('should list applications with summary', async () => {
513
+ mockFetch.mockResolvedValueOnce(mockResponse([mockApplication]));
514
+ const result = await client.listApplications({ summary: true });
515
+ expect(result).toEqual([
516
+ {
517
+ uuid: 'app-uuid',
518
+ name: 'test-app',
519
+ status: 'running',
520
+ fqdn: 'https://app.example.com',
521
+ git_repository: 'https://github.com/user/repo',
522
+ git_branch: 'main',
523
+ },
524
+ ]);
525
+ });
526
+ it('should get an application', async () => {
527
+ mockFetch.mockResolvedValueOnce(mockResponse(mockApplication));
528
+ const result = await client.getApplication('app-uuid');
529
+ expect(result).toEqual(mockApplication);
530
+ });
531
+ it('should create application from public repo', async () => {
532
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-app-uuid' }));
533
+ const result = await client.createApplicationPublic({
534
+ project_uuid: 'proj-uuid',
535
+ server_uuid: 'server-uuid',
536
+ git_repository: 'https://github.com/user/repo',
537
+ git_branch: 'main',
538
+ build_pack: 'nixpacks',
539
+ ports_exposes: '3000',
540
+ });
541
+ expect(result).toEqual({ uuid: 'new-app-uuid' });
542
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/public', expect.objectContaining({ method: 'POST' }));
543
+ });
544
+ it('should create application from private GH repo', async () => {
545
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-app-uuid' }));
546
+ const result = await client.createApplicationPrivateGH({
547
+ project_uuid: 'proj-uuid',
548
+ server_uuid: 'server-uuid',
549
+ github_app_uuid: 'gh-app-uuid',
550
+ git_repository: 'user/repo',
551
+ git_branch: 'main',
552
+ build_pack: 'nixpacks',
553
+ ports_exposes: '3000',
554
+ });
555
+ expect(result).toEqual({ uuid: 'new-app-uuid' });
556
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/private-github-app', expect.objectContaining({ method: 'POST' }));
557
+ });
558
+ it('should create application from private key repo', async () => {
559
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-app-uuid' }));
560
+ const result = await client.createApplicationPrivateKey({
561
+ project_uuid: 'proj-uuid',
562
+ server_uuid: 'server-uuid',
563
+ private_key_uuid: 'key-uuid',
564
+ git_repository: 'git@github.com:user/repo.git',
565
+ git_branch: 'main',
566
+ build_pack: 'nixpacks',
567
+ ports_exposes: '22',
568
+ });
569
+ expect(result).toEqual({ uuid: 'new-app-uuid' });
570
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/private-deploy-key', expect.objectContaining({ method: 'POST' }));
571
+ });
572
+ it('should create application from dockerfile', async () => {
573
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-app-uuid' }));
574
+ const result = await client.createApplicationDockerfile({
575
+ project_uuid: 'proj-uuid',
576
+ server_uuid: 'server-uuid',
577
+ dockerfile: 'FROM node:18',
578
+ });
579
+ expect(result).toEqual({ uuid: 'new-app-uuid' });
580
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/dockerfile', expect.objectContaining({ method: 'POST' }));
581
+ });
582
+ it('should create application from docker image', async () => {
583
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-app-uuid' }));
584
+ const result = await client.createApplicationDockerImage({
585
+ project_uuid: 'proj-uuid',
586
+ server_uuid: 'server-uuid',
587
+ docker_registry_image_name: 'nginx:latest',
588
+ ports_exposes: '80',
589
+ });
590
+ expect(result).toEqual({ uuid: 'new-app-uuid' });
591
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/dockerimage', expect.objectContaining({ method: 'POST' }));
592
+ });
593
+ it('should create application from docker compose', async () => {
594
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-app-uuid' }));
595
+ const result = await client.createApplicationDockerCompose({
596
+ project_uuid: 'proj-uuid',
597
+ server_uuid: 'server-uuid',
598
+ docker_compose_raw: 'version: "3"',
599
+ });
600
+ expect(result).toEqual({ uuid: 'new-app-uuid' });
601
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/dockercompose', expect.objectContaining({ method: 'POST' }));
602
+ });
603
+ it('should update an application', async () => {
604
+ mockFetch.mockResolvedValueOnce(mockResponse({ ...mockApplication, name: 'updated-app' }));
605
+ const result = await client.updateApplication('app-uuid', { name: 'updated-app' });
606
+ expect(result.name).toBe('updated-app');
607
+ });
608
+ it('should delete an application', async () => {
609
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deleted' }));
610
+ const result = await client.deleteApplication('app-uuid');
611
+ expect(result).toEqual({ message: 'Deleted' });
612
+ });
613
+ it('should delete an application with options', async () => {
614
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deleted' }));
615
+ await client.deleteApplication('app-uuid', {
616
+ deleteVolumes: true,
617
+ dockerCleanup: true,
618
+ deleteConfigurations: true,
619
+ deleteConnectedNetworks: true,
620
+ });
621
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid?delete_configurations=true&delete_volumes=true&docker_cleanup=true&delete_connected_networks=true', expect.objectContaining({ method: 'DELETE' }));
622
+ });
623
+ it('should get application logs', async () => {
624
+ mockFetch.mockResolvedValueOnce(mockResponse('log line 1\nlog line 2'));
625
+ const result = await client.getApplicationLogs('app-uuid', 50);
626
+ expect(result).toBe('log line 1\nlog line 2');
627
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/logs?lines=50', expect.any(Object));
628
+ });
629
+ it('should restart an application', async () => {
630
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Restarted' }));
631
+ const result = await client.restartApplication('app-uuid');
632
+ expect(result).toEqual({ message: 'Restarted' });
633
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/restart', expect.objectContaining({ method: 'POST' }));
634
+ });
635
+ });
636
+ // =========================================================================
637
+ // Application Environment Variables
638
+ // =========================================================================
639
+ describe('application environment variables', () => {
640
+ const mockEnvVar = {
641
+ uuid: 'env-var-uuid',
642
+ key: 'API_KEY',
643
+ value: 'secret123',
644
+ is_build_time: false,
645
+ };
646
+ it('should list application env vars', async () => {
647
+ mockFetch.mockResolvedValueOnce(mockResponse([mockEnvVar]));
648
+ const result = await client.listApplicationEnvVars('app-uuid');
649
+ expect(result).toEqual([mockEnvVar]);
650
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/envs', expect.any(Object));
651
+ });
652
+ it('should create application env var', async () => {
653
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-env-uuid' }));
654
+ const result = await client.createApplicationEnvVar('app-uuid', {
655
+ key: 'NEW_VAR',
656
+ value: 'new-value',
657
+ is_build_time: true,
658
+ });
659
+ expect(result).toEqual({ uuid: 'new-env-uuid' });
660
+ });
661
+ it('should update application env var', async () => {
662
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Updated' }));
663
+ const result = await client.updateApplicationEnvVar('app-uuid', {
664
+ key: 'API_KEY',
665
+ value: 'updated-secret',
666
+ });
667
+ expect(result).toEqual({ message: 'Updated' });
668
+ });
669
+ it('should bulk update application env vars', async () => {
670
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Updated' }));
671
+ const result = await client.bulkUpdateApplicationEnvVars('app-uuid', {
672
+ data: [
673
+ { key: 'VAR1', value: 'val1' },
674
+ { key: 'VAR2', value: 'val2' },
675
+ ],
676
+ });
677
+ expect(result).toEqual({ message: 'Updated' });
678
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/envs/bulk', expect.objectContaining({ method: 'PATCH' }));
679
+ });
680
+ it('should delete application env var', async () => {
681
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deleted' }));
682
+ const result = await client.deleteApplicationEnvVar('app-uuid', 'env-var-uuid');
683
+ expect(result).toEqual({ message: 'Deleted' });
684
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/envs/env-var-uuid', expect.objectContaining({ method: 'DELETE' }));
685
+ });
686
+ });
687
+ // =========================================================================
688
+ // Database endpoints - extended coverage
689
+ // =========================================================================
690
+ describe('databases extended', () => {
691
+ it('should list databases with pagination', async () => {
692
+ mockFetch.mockResolvedValueOnce(mockResponse([mockDatabase]));
693
+ await client.listDatabases({ page: 1, per_page: 10 });
694
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases?page=1&per_page=10', expect.any(Object));
695
+ });
696
+ it('should list databases with summary', async () => {
697
+ mockFetch.mockResolvedValueOnce(mockResponse([mockDatabase]));
698
+ const result = await client.listDatabases({ summary: true });
699
+ expect(result).toEqual([
700
+ {
701
+ uuid: 'db-uuid',
702
+ name: 'test-db',
703
+ type: 'postgresql',
704
+ status: 'running',
705
+ is_public: false,
706
+ },
707
+ ]);
708
+ });
709
+ it('should get a database', async () => {
710
+ mockFetch.mockResolvedValueOnce(mockResponse(mockDatabase));
711
+ const result = await client.getDatabase('db-uuid');
712
+ expect(result).toEqual(mockDatabase);
713
+ });
714
+ it('should update a database', async () => {
715
+ mockFetch.mockResolvedValueOnce(mockResponse({ ...mockDatabase, name: 'updated-db' }));
716
+ const result = await client.updateDatabase('db-uuid', { name: 'updated-db' });
717
+ expect(result.name).toBe('updated-db');
718
+ });
719
+ it('should delete a database', async () => {
720
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deleted' }));
721
+ const result = await client.deleteDatabase('db-uuid');
722
+ expect(result).toEqual({ message: 'Deleted' });
723
+ });
724
+ it('should delete a database with options', async () => {
725
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deleted' }));
726
+ await client.deleteDatabase('db-uuid', { deleteVolumes: true, dockerCleanup: true });
727
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid?delete_volumes=true&docker_cleanup=true', expect.objectContaining({ method: 'DELETE' }));
728
+ });
729
+ it('should stop a database', async () => {
730
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Stopped' }));
731
+ const result = await client.stopDatabase('db-uuid');
732
+ expect(result).toEqual({ message: 'Stopped' });
733
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/stop', expect.objectContaining({ method: 'POST' }));
734
+ });
735
+ it('should restart a database', async () => {
736
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Restarted' }));
737
+ const result = await client.restartDatabase('db-uuid');
738
+ expect(result).toEqual({ message: 'Restarted' });
739
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/restart', expect.objectContaining({ method: 'POST' }));
740
+ });
741
+ it('should list database backups', async () => {
742
+ const mockBackups = [{ uuid: 'backup-uuid', status: 'completed' }];
743
+ mockFetch.mockResolvedValueOnce(mockResponse(mockBackups));
744
+ const result = await client.listDatabaseBackups('db-uuid');
745
+ expect(result).toEqual(mockBackups);
746
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/backups', expect.any(Object));
747
+ });
748
+ it('should create a database backup', async () => {
749
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-backup-uuid', message: 'Backup created' }));
750
+ const result = await client.createDatabaseBackup('db-uuid', {
751
+ frequency: 'daily',
752
+ backup_retention: 7,
753
+ });
754
+ expect(result).toEqual({ uuid: 'new-backup-uuid', message: 'Backup created' });
755
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/backups', expect.objectContaining({ method: 'POST' }));
756
+ });
757
+ });
758
+ // =========================================================================
759
+ // Service endpoints - extended coverage
760
+ // =========================================================================
761
+ describe('services extended', () => {
762
+ it('should list services with pagination', async () => {
763
+ mockFetch.mockResolvedValueOnce(mockResponse([mockService]));
764
+ await client.listServices({ page: 2, per_page: 5 });
765
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services?page=2&per_page=5', expect.any(Object));
766
+ });
767
+ it('should list services with summary', async () => {
768
+ mockFetch.mockResolvedValueOnce(mockResponse([mockService]));
769
+ const result = await client.listServices({ summary: true });
770
+ expect(result).toEqual([
771
+ {
772
+ uuid: 'test-uuid',
773
+ name: 'test-service',
774
+ type: 'code-server',
775
+ status: 'running',
776
+ domains: ['test.example.com'],
777
+ },
778
+ ]);
779
+ });
780
+ it('should update a service', async () => {
781
+ mockFetch.mockResolvedValueOnce(mockResponse({ ...mockService, name: 'updated-service' }));
782
+ const result = await client.updateService('test-uuid', { name: 'updated-service' });
783
+ expect(result.name).toBe('updated-service');
784
+ });
785
+ it('should start a service', async () => {
786
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Started' }));
787
+ const result = await client.startService('test-uuid');
788
+ expect(result).toEqual({ message: 'Started' });
789
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/test-uuid/start', expect.objectContaining({ method: 'GET' }));
790
+ });
791
+ it('should stop a service', async () => {
792
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Stopped' }));
793
+ const result = await client.stopService('test-uuid');
794
+ expect(result).toEqual({ message: 'Stopped' });
795
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/test-uuid/stop', expect.objectContaining({ method: 'GET' }));
796
+ });
797
+ it('should restart a service', async () => {
798
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Restarted' }));
799
+ const result = await client.restartService('test-uuid');
800
+ expect(result).toEqual({ message: 'Restarted' });
801
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/test-uuid/restart', expect.objectContaining({ method: 'GET' }));
802
+ });
803
+ });
804
+ // =========================================================================
805
+ // Service Environment Variables
806
+ // =========================================================================
807
+ describe('service environment variables', () => {
808
+ const mockEnvVar = {
809
+ uuid: 'svc-env-uuid',
810
+ key: 'SVC_KEY',
811
+ value: 'svc-value',
812
+ };
813
+ it('should list service env vars', async () => {
814
+ mockFetch.mockResolvedValueOnce(mockResponse([mockEnvVar]));
815
+ const result = await client.listServiceEnvVars('test-uuid');
816
+ expect(result).toEqual([mockEnvVar]);
817
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/test-uuid/envs', expect.any(Object));
818
+ });
819
+ it('should create service env var', async () => {
820
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-env-uuid' }));
821
+ const result = await client.createServiceEnvVar('test-uuid', {
822
+ key: 'NEW_SVC_VAR',
823
+ value: 'new-value',
824
+ });
825
+ expect(result).toEqual({ uuid: 'new-env-uuid' });
826
+ });
827
+ it('should update service env var', async () => {
828
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Updated' }));
829
+ const result = await client.updateServiceEnvVar('test-uuid', {
830
+ key: 'SVC_KEY',
831
+ value: 'updated-value',
832
+ });
833
+ expect(result).toEqual({ message: 'Updated' });
834
+ });
835
+ it('should delete service env var', async () => {
836
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deleted' }));
837
+ const result = await client.deleteServiceEnvVar('test-uuid', 'svc-env-uuid');
838
+ expect(result).toEqual({ message: 'Deleted' });
839
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/test-uuid/envs/svc-env-uuid', expect.objectContaining({ method: 'DELETE' }));
840
+ });
841
+ });
842
+ // =========================================================================
843
+ // Deployment endpoints - extended coverage
844
+ // =========================================================================
845
+ describe('deployments extended', () => {
846
+ it('should list deployments with pagination', async () => {
847
+ mockFetch.mockResolvedValueOnce(mockResponse([mockDeployment]));
848
+ await client.listDeployments({ page: 1, per_page: 25 });
849
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deployments?page=1&per_page=25', expect.any(Object));
850
+ });
851
+ it('should list deployments with summary', async () => {
852
+ mockFetch.mockResolvedValueOnce(mockResponse([mockDeployment]));
853
+ const result = await client.listDeployments({ summary: true });
854
+ expect(result).toEqual([
855
+ {
856
+ uuid: 'dep-uuid',
857
+ deployment_uuid: 'dep-123',
858
+ application_name: 'test-app',
859
+ status: 'finished',
860
+ created_at: '2024-01-01',
861
+ },
862
+ ]);
863
+ });
864
+ it('should get a deployment', async () => {
865
+ mockFetch.mockResolvedValueOnce(mockResponse(mockDeployment));
866
+ const result = await client.getDeployment('dep-uuid');
867
+ expect(result).toEqual(mockDeployment);
868
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deployments/dep-uuid', expect.any(Object));
869
+ });
870
+ it('should list application deployments', async () => {
871
+ mockFetch.mockResolvedValueOnce(mockResponse([mockDeployment]));
872
+ const result = await client.listApplicationDeployments('app-uuid');
873
+ expect(result).toEqual([mockDeployment]);
874
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/deployments', expect.any(Object));
875
+ });
876
+ });
877
+ // =========================================================================
878
+ // Team endpoints - extended coverage
879
+ // =========================================================================
880
+ describe('teams extended', () => {
881
+ it('should get a team by id', async () => {
882
+ const mockTeam = { id: 1, name: 'team-one', personal_team: false };
883
+ mockFetch.mockResolvedValueOnce(mockResponse(mockTeam));
884
+ const result = await client.getTeam(1);
885
+ expect(result).toEqual(mockTeam);
886
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/teams/1', expect.any(Object));
887
+ });
888
+ it('should get team members', async () => {
889
+ const mockMembers = [{ id: 1, name: 'User One', email: 'user@example.com' }];
890
+ mockFetch.mockResolvedValueOnce(mockResponse(mockMembers));
891
+ const result = await client.getTeamMembers(1);
892
+ expect(result).toEqual(mockMembers);
893
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/teams/1/members', expect.any(Object));
894
+ });
895
+ it('should get current team members', async () => {
896
+ const mockMembers = [{ id: 1, name: 'Current User', email: 'current@example.com' }];
897
+ mockFetch.mockResolvedValueOnce(mockResponse(mockMembers));
898
+ const result = await client.getCurrentTeamMembers();
899
+ expect(result).toEqual(mockMembers);
900
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/teams/current/members', expect.any(Object));
901
+ });
902
+ });
903
+ // =========================================================================
904
+ // Private Key endpoints - extended coverage
905
+ // =========================================================================
906
+ describe('private keys extended', () => {
907
+ const mockPrivateKey = {
908
+ uuid: 'key-uuid',
909
+ name: 'my-key',
910
+ fingerprint: 'SHA256:xxx',
911
+ };
912
+ it('should get a private key', async () => {
913
+ mockFetch.mockResolvedValueOnce(mockResponse(mockPrivateKey));
914
+ const result = await client.getPrivateKey('key-uuid');
915
+ expect(result).toEqual(mockPrivateKey);
916
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/security/keys/key-uuid', expect.any(Object));
917
+ });
918
+ it('should update a private key', async () => {
919
+ mockFetch.mockResolvedValueOnce(mockResponse({ ...mockPrivateKey, name: 'updated-key' }));
920
+ const result = await client.updatePrivateKey('key-uuid', { name: 'updated-key' });
921
+ expect(result.name).toBe('updated-key');
922
+ });
923
+ it('should delete a private key', async () => {
924
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deleted' }));
925
+ const result = await client.deletePrivateKey('key-uuid');
926
+ expect(result).toEqual({ message: 'Deleted' });
927
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/security/keys/key-uuid', expect.objectContaining({ method: 'DELETE' }));
928
+ });
929
+ });
930
+ // =========================================================================
931
+ // Cloud Token endpoints
932
+ // =========================================================================
933
+ describe('cloud tokens', () => {
934
+ const mockCloudToken = {
935
+ uuid: 'token-uuid',
936
+ name: 'hetzner-token',
937
+ provider: 'hetzner',
938
+ };
939
+ it('should list cloud tokens', async () => {
940
+ mockFetch.mockResolvedValueOnce(mockResponse([mockCloudToken]));
941
+ const result = await client.listCloudTokens();
942
+ expect(result).toEqual([mockCloudToken]);
943
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/cloud-tokens', expect.any(Object));
944
+ });
945
+ it('should get a cloud token', async () => {
946
+ mockFetch.mockResolvedValueOnce(mockResponse(mockCloudToken));
947
+ const result = await client.getCloudToken('token-uuid');
948
+ expect(result).toEqual(mockCloudToken);
949
+ });
950
+ it('should create a cloud token', async () => {
951
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-token-uuid' }));
952
+ const result = await client.createCloudToken({
953
+ name: 'new-token',
954
+ provider: 'digitalocean',
955
+ token: 'do-token-value',
956
+ });
957
+ expect(result).toEqual({ uuid: 'new-token-uuid' });
958
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/cloud-tokens', expect.objectContaining({ method: 'POST' }));
959
+ });
960
+ it('should update a cloud token', async () => {
961
+ mockFetch.mockResolvedValueOnce(mockResponse({ ...mockCloudToken, name: 'updated-token' }));
962
+ const result = await client.updateCloudToken('token-uuid', { name: 'updated-token' });
963
+ expect(result.name).toBe('updated-token');
964
+ });
965
+ it('should delete a cloud token', async () => {
966
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deleted' }));
967
+ const result = await client.deleteCloudToken('token-uuid');
968
+ expect(result).toEqual({ message: 'Deleted' });
969
+ });
970
+ it('should validate a cloud token', async () => {
971
+ mockFetch.mockResolvedValueOnce(mockResponse({ valid: true }));
972
+ const result = await client.validateCloudToken('token-uuid');
973
+ expect(result).toEqual({ valid: true });
974
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/cloud-tokens/token-uuid/validate', expect.objectContaining({ method: 'POST' }));
975
+ });
976
+ });
977
+ // =========================================================================
978
+ // Health & Version
979
+ // =========================================================================
980
+ describe('health and version', () => {
981
+ it('should get version', async () => {
982
+ mockFetch.mockResolvedValueOnce({
983
+ ok: true,
984
+ status: 200,
985
+ text: async () => 'v4.0.0-beta.123',
986
+ });
987
+ const result = await client.getVersion();
988
+ expect(result).toEqual({ version: 'v4.0.0-beta.123' });
989
+ });
990
+ it('should handle version errors', async () => {
991
+ mockFetch.mockResolvedValueOnce({
992
+ ok: false,
993
+ status: 401,
994
+ statusText: 'Unauthorized',
995
+ });
996
+ await expect(client.getVersion()).rejects.toThrow('HTTP 401: Unauthorized');
997
+ });
998
+ it('should validate connection successfully', async () => {
999
+ mockFetch.mockResolvedValueOnce({
1000
+ ok: true,
1001
+ status: 200,
1002
+ text: async () => 'v4.0.0',
1003
+ });
1004
+ await expect(client.validateConnection()).resolves.not.toThrow();
1005
+ });
1006
+ it('should throw on failed connection validation', async () => {
1007
+ mockFetch.mockRejectedValueOnce(new TypeError('fetch failed'));
1008
+ await expect(client.validateConnection()).rejects.toThrow('Failed to connect to Coolify server');
1009
+ });
1010
+ it('should handle non-Error exceptions in validateConnection', async () => {
1011
+ mockFetch.mockRejectedValueOnce('string error');
1012
+ await expect(client.validateConnection()).rejects.toThrow('Failed to connect to Coolify server: Unknown error');
1013
+ });
1014
+ it('should use default lines for getApplicationLogs', async () => {
1015
+ mockFetch.mockResolvedValueOnce(mockResponse('log output'));
1016
+ await client.getApplicationLogs('app-uuid');
1017
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/logs?lines=100', expect.any(Object));
1018
+ });
1019
+ it('should use default force=false for deployByTagOrUuid', async () => {
1020
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deployed' }));
1021
+ await client.deployByTagOrUuid('my-tag');
1022
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deploy?tag=my-tag&force=false', expect.any(Object));
1023
+ });
285
1024
  });
286
1025
  });