cdd-cli 3.1.9 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/coverage/clover.xml +604 -0
- package/coverage/coverage-final.json +17 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/helpers/constants.js.html +235 -0
- package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +211 -0
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +853 -0
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +289 -0
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +131 -0
- package/coverage/lcov-report/helpers/index.html +176 -0
- package/coverage/lcov-report/helpers/logger.js.html +424 -0
- package/coverage/lcov-report/helpers/safeCall.js.html +139 -0
- package/coverage/lcov-report/helpers/validationHelpers.js.html +394 -0
- package/coverage/lcov-report/hooks/creation/index.html +146 -0
- package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +292 -0
- package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +478 -0
- package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +238 -0
- package/coverage/lcov-report/hooks/debug/index.html +116 -0
- package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +172 -0
- package/coverage/lcov-report/hooks/index.html +161 -0
- package/coverage/lcov-report/hooks/navigation/index.html +116 -0
- package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +160 -0
- package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +415 -0
- package/coverage/lcov-report/hooks/useControls.js.html +685 -0
- package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +202 -0
- package/coverage/lcov-report/hooks/useExitHandler.js.html +283 -0
- package/coverage/lcov-report/index.html +191 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +1216 -0
- package/package.json +1 -1
- package/src/helpers/constants.js +20 -0
- package/src/helpers/dockerService/serviceComponents/containerActions.js +55 -8
- package/src/helpers/validationHelpers.js +75 -18
- package/src/hooks/creation/useContainerCreation.js +19 -4
- package/src/hooks/useControls.js +8 -3
- package/test/containerActions.test.js +104 -13
- package/test/containerOptionsBuilder.test.js +36 -0
- package/test/useContainerCreation.dom.test.js +43 -2
- package/test/useControls.dom.test.js +117 -0
- package/test/validationHelpers.test.js +51 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import React, { useEffect } from 'react';
|
|
5
|
+
import { render, act } from '@testing-library/react';
|
|
6
|
+
import { jest } from '@jest/globals';
|
|
7
|
+
|
|
8
|
+
// Mock ink so that useInput and useApp are no-ops (require real TTY in production)
|
|
9
|
+
await jest.unstable_mockModule('ink', () => ({
|
|
10
|
+
useInput: () => {},
|
|
11
|
+
useApp: () => ({ exit: () => {} }),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
// Mock containerActions — expose all named exports so dependents don't break
|
|
15
|
+
const mockSvcCreateContainer = jest.fn();
|
|
16
|
+
await jest.unstable_mockModule(
|
|
17
|
+
'../src/helpers/dockerService/serviceComponents/containerActions.js',
|
|
18
|
+
() => ({
|
|
19
|
+
createContainer: mockSvcCreateContainer,
|
|
20
|
+
removeContainer: jest.fn().mockResolvedValue(undefined),
|
|
21
|
+
startContainer: jest.fn().mockResolvedValue(undefined),
|
|
22
|
+
stopContainer: jest.fn().mockResolvedValue(undefined),
|
|
23
|
+
restartContainer: jest.fn().mockResolvedValue(undefined),
|
|
24
|
+
})
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// Mock getLogsStream to avoid side effects
|
|
28
|
+
await jest.unstable_mockModule(
|
|
29
|
+
'../src/helpers/dockerService/serviceComponents/containerLogs.js',
|
|
30
|
+
() => ({ getLogsStream: () => {} })
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const { useControls } = await import('../src/hooks/useControls.js');
|
|
34
|
+
|
|
35
|
+
function HookTester({ containers, expose }) {
|
|
36
|
+
const hook = useControls(containers);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (expose) expose.current = hook;
|
|
39
|
+
});
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Helper: advance creation wizard to the final step and trigger onCreate
|
|
44
|
+
async function completeCreationWizard(expose, imageName = 'nginx') {
|
|
45
|
+
act(() => { expose.current.creation.setImageName(imageName); });
|
|
46
|
+
act(() => { expose.current.creation.nextStep(); }); // step 0 → 1
|
|
47
|
+
act(() => { expose.current.creation.nextStep(); }); // step 1 → 2
|
|
48
|
+
act(() => { expose.current.creation.nextStep(); }); // step 2 → 3
|
|
49
|
+
// step 3: call nextStep to trigger onCreate (async)
|
|
50
|
+
await act(async () => { expose.current.creation.nextStep(); });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('useControls (FR6 — port mapping in success message)', () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
mockSvcCreateContainer.mockReset();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('success message includes port mapping after container creation', async () => {
|
|
59
|
+
const ports = [
|
|
60
|
+
{ containerPort: '3306', hostPort: '3306', protocol: 'tcp', source: 'auto' },
|
|
61
|
+
];
|
|
62
|
+
mockSvcCreateContainer.mockResolvedValue({ id: 'cid-abc', ports });
|
|
63
|
+
|
|
64
|
+
const expose = { current: null };
|
|
65
|
+
render(<HookTester containers={[]} expose={expose} />);
|
|
66
|
+
|
|
67
|
+
await completeCreationWizard(expose, 'nginx');
|
|
68
|
+
|
|
69
|
+
expect(expose.current.actions.message).toBe(
|
|
70
|
+
'Created container cid-abc | Ports: 3306→3306/tcp'
|
|
71
|
+
);
|
|
72
|
+
expect(expose.current.actions.messageColor).toBe('green');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('success message has no port section when ports array is empty', async () => {
|
|
76
|
+
mockSvcCreateContainer.mockResolvedValue({ id: 'cid-xyz', ports: [] });
|
|
77
|
+
|
|
78
|
+
const expose = { current: null };
|
|
79
|
+
render(<HookTester containers={[]} expose={expose} />);
|
|
80
|
+
|
|
81
|
+
await completeCreationWizard(expose, 'alpine');
|
|
82
|
+
|
|
83
|
+
expect(expose.current.actions.message).toBe('Created container cid-xyz');
|
|
84
|
+
expect(expose.current.actions.messageColor).toBe('green');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('success message includes multiple port mappings', async () => {
|
|
88
|
+
const ports = [
|
|
89
|
+
{ containerPort: '80', hostPort: '8080', protocol: 'tcp', source: 'user' },
|
|
90
|
+
{ containerPort: '443', hostPort: '8443', protocol: 'tcp', source: 'user' },
|
|
91
|
+
];
|
|
92
|
+
mockSvcCreateContainer.mockResolvedValue({ id: 'cid-multi', ports });
|
|
93
|
+
|
|
94
|
+
const expose = { current: null };
|
|
95
|
+
render(<HookTester containers={[]} expose={expose} />);
|
|
96
|
+
|
|
97
|
+
await completeCreationWizard(expose, 'nginx');
|
|
98
|
+
|
|
99
|
+
expect(expose.current.actions.message).toBe(
|
|
100
|
+
'Created container cid-multi | Ports: 8080→80/tcp, 8443→443/tcp'
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('error message is shown when svcCreateContainer rejects', async () => {
|
|
105
|
+
mockSvcCreateContainer.mockRejectedValue(new Error('pull access denied'));
|
|
106
|
+
|
|
107
|
+
const expose = { current: null };
|
|
108
|
+
render(<HookTester containers={[]} expose={expose} />);
|
|
109
|
+
|
|
110
|
+
await completeCreationWizard(expose, 'badimage');
|
|
111
|
+
|
|
112
|
+
expect(expose.current.actions.message).toBe(
|
|
113
|
+
'Error creating container: pull access denied'
|
|
114
|
+
);
|
|
115
|
+
expect(expose.current.actions.messageColor).toBe('red');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
let validatePorts;
|
|
2
2
|
let validateEnvVars;
|
|
3
|
+
let IMAGE_PROFILES;
|
|
3
4
|
|
|
4
5
|
beforeAll(async () => {
|
|
5
6
|
const mod = await import('../src/helpers/validationHelpers.js');
|
|
6
7
|
validatePorts = mod.validatePorts;
|
|
7
8
|
validateEnvVars = mod.validateEnvVars;
|
|
9
|
+
const constants = await import('../src/helpers/constants.js');
|
|
10
|
+
IMAGE_PROFILES = constants.IMAGE_PROFILES;
|
|
8
11
|
});
|
|
9
12
|
|
|
10
13
|
describe('validatePorts', () => {
|
|
@@ -58,3 +61,51 @@ describe('validateEnvVars', () => {
|
|
|
58
61
|
expect(validateEnvVars('MY-VAR=value')).toBe(false);
|
|
59
62
|
});
|
|
60
63
|
});
|
|
64
|
+
|
|
65
|
+
describe('validateEnvVars — contextual validation with IMAGE_PROFILES', () => {
|
|
66
|
+
test('mysql:8 — missing MYSQL_ROOT_PASSWORD is invalid', () => {
|
|
67
|
+
const result = validateEnvVars('FOO=bar', 'mysql:8', IMAGE_PROFILES);
|
|
68
|
+
expect(result.valid).toBe(false);
|
|
69
|
+
expect(result.errors.some(e => e.includes('MYSQL_ROOT_PASSWORD'))).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('mysql:8 — with MYSQL_ROOT_PASSWORD is valid', () => {
|
|
73
|
+
const result = validateEnvVars('MYSQL_ROOT_PASSWORD=secret', 'mysql:8', IMAGE_PROFILES);
|
|
74
|
+
expect(result.valid).toBe(true);
|
|
75
|
+
expect(result.errors).toHaveLength(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('docker.io/library/postgres:16 — missing POSTGRES_PASSWORD is invalid', () => {
|
|
79
|
+
const result = validateEnvVars('', 'docker.io/library/postgres:16', IMAGE_PROFILES);
|
|
80
|
+
expect(result.valid).toBe(false);
|
|
81
|
+
expect(result.errors.some(e => e.includes('POSTGRES_PASSWORD'))).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('docker.io/library/postgres:16 — with POSTGRES_PASSWORD is valid', () => {
|
|
85
|
+
const result = validateEnvVars('POSTGRES_PASSWORD=pass', 'docker.io/library/postgres:16', IMAGE_PROFILES);
|
|
86
|
+
expect(result.valid).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('TOKEN=a=b is preserved (split on first = only)', () => {
|
|
90
|
+
const result = validateEnvVars('TOKEN=a=b', 'nginx', IMAGE_PROFILES);
|
|
91
|
+
expect(result.valid).toBe(true);
|
|
92
|
+
expect(result.parsedEnv['TOKEN']).toBe('a=b');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('image not in profiles — only syntactic validation applies', () => {
|
|
96
|
+
const result = validateEnvVars('FOO=bar', 'nginx', IMAGE_PROFILES);
|
|
97
|
+
expect(result.valid).toBe(true);
|
|
98
|
+
expect(result.parsedEnv['FOO']).toBe('bar');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('empty input for image not in profiles — valid', () => {
|
|
102
|
+
const result = validateEnvVars('', 'nginx', IMAGE_PROFILES);
|
|
103
|
+
expect(result.valid).toBe(true);
|
|
104
|
+
expect(result.parsedEnv).toEqual({});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('syntactically invalid var is still rejected', () => {
|
|
108
|
+
const result = validateEnvVars('INVALID-VAR=x', 'nginx', IMAGE_PROFILES);
|
|
109
|
+
expect(result.valid).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|