@ibm-cloud/cd-tools 1.1.1 → 1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibm-cloud/cd-tools",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Tools and utilities for the IBM Cloud Continuous Delivery service and resources",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,6 +14,9 @@
14
14
  },
15
15
  "author": "IBM Corp.",
16
16
  "license": "Apache-2.0",
17
+ "scripts": {
18
+ "test": "mocha --require \"test/setup.js\" \"test/copy-toolchain/*.test.js\""
19
+ },
17
20
  "dependencies": {
18
21
  "@cdktf/hcl2json": "^0.21.0",
19
22
  "axios": "^1.12.2",
@@ -27,5 +30,11 @@
27
30
  "bin": {
28
31
  "cd-tools": "index.js"
29
32
  },
30
- "type": "module"
33
+ "type": "module",
34
+ "devDependencies": {
35
+ "chai": "^6.2.0",
36
+ "mocha": "^11.7.4",
37
+ "nconf": "^0.13.0",
38
+ "node-pty": "^1.0.0"
39
+ }
31
40
  }
package/test/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # Test
2
+
3
+ All automated tests live in this directory.
4
+ Before running tests, ensure that you have completed the setup steps in the main [README.md](../README.md), including installing all prerequisites and dependencies.
5
+
6
+ ## Getting Started
7
+ 1. **Clone the repository**
8
+ ```bash
9
+ git clone https://github.com/IBM/continuous-delivery-tools.git
10
+ cd continuous-delivery-tools
11
+ ```
12
+ 2. **Install dependencies**
13
+ ```bash
14
+ npm install
15
+ ```
16
+ 3. **Test configuration**
17
+
18
+ Before running tests, create a local configuration file:
19
+ ```bash
20
+ cp test/config/local.template.json test/config/local.json
21
+ ```
22
+ Then open `test/config/local.json` and replace all placeholder values with your local or test environment settings.
23
+ 4. **Running the tests**
24
+
25
+ To execute the test suite:
26
+ ```bash
27
+ npm run test
28
+ ```
29
+
30
+ ### Test Configuration
31
+ You can customize the behavior of the tests by defining configuration properties in the `test/config/local.json` file.
32
+
33
+ | Property | Type | Default | Description |
34
+ | ------------------ | --------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------- |
35
+ | `TEST_DEBUG_MODE` | `boolean` | `false` | When set to `true`, files generated by test cases in `TEST_TEMP_DIR` and log files generated in `TEST_LOG_DIR` are preserved |
36
+ | `TEST_TEMP_DIR` | `string` | `test/.tmp` | The directory to store temporary files generated by test cases |
37
+ | `TEST_LOG_DIR` | `string` | `test/.logs` | The directory to store test run log files |
38
+ | `IBMCLOUD_API_KEY` | `string` | `null` | The IBM Cloud API Key used to run the tests |
39
+ | `LOG_DUMP` | `boolean` | `false` | When set to `true`, individual test case's process's log file generation is enabled |
40
+ | `DISABLE_SPINNER` | `boolean` | `true` | When set to `true`, visual spinner is disabled across all test cases' processes |
@@ -0,0 +1,8 @@
1
+ {
2
+ "TEST_DEBUG_MODE": false,
3
+ "TEST_TEMP_DIR": "test/.tmp",
4
+ "TEST_LOG_DIR": "test/.logs",
5
+ "IBMCLOUD_API_KEY": "<YOUR IBMCLOUD API KEY>",
6
+ "LOG_DUMP": true,
7
+ "DISABLE_SPINNER": true
8
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Licensed Materials - Property of IBM
3
+ * (c) Copyright IBM Corporation 2025. All Rights Reserved.
4
+ *
5
+ * Note to U.S. Government Users Restricted Rights:
6
+ * Use, duplication or disclosure restricted by GSA ADP Schedule
7
+ * Contract with IBM Corp.
8
+ */
9
+
10
+ describe('copy-toolchain: Test import-terraform output', function () {
11
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Licensed Materials - Property of IBM
3
+ * (c) Copyright IBM Corporation 2025. All Rights Reserved.
4
+ *
5
+ * Note to U.S. Government Users Restricted Rights:
6
+ * Use, duplication or disclosure restricted by GSA ADP Schedule
7
+ * Contract with IBM Corp.
8
+ */
9
+
10
+ describe('copy-toolchain: Test Terraform output', function () {
11
+ });
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Licensed Materials - Property of IBM
3
+ * (c) Copyright IBM Corporation 2025. All Rights Reserved.
4
+ *
5
+ * Note to U.S. Government Users Restricted Rights:
6
+ * Use, duplication or disclosure restricted by GSA ADP Schedule
7
+ * Contract with IBM Corp.
8
+ */
9
+
10
+ import path from 'node:path';
11
+ import nconf from 'nconf';
12
+
13
+ import * as chai from 'chai';
14
+ chai.config.truncateThreshold = 0;
15
+
16
+ import mocks from '../data/mocks.js';
17
+ import { testSuiteCleanup, expectExecError, expectPtyOutputToMatch } from '../utils/testUtils.js';
18
+ import { TEST_TOOLCHAINS } from '../data/test-toolchains.js';
19
+ import { TARGET_REGIONS } from '../../config.js';
20
+
21
+ nconf.env('__');
22
+ nconf.file('local', 'test/config/local.json');
23
+
24
+ const CLI_PATH = path.resolve('index.js');
25
+ const COMMAND = 'copy-toolchain';
26
+
27
+ const toolchainsToDelete = new Map();
28
+
29
+ after(async () => await testSuiteCleanup(toolchainsToDelete));
30
+
31
+ describe('copy-toolchain: Test user input handling', function () {
32
+ this.timeout('60s');
33
+ this.command = 'copy-toolchain';
34
+
35
+ const validCrn = TEST_TOOLCHAINS['empty'].crn;
36
+
37
+ const invalidArgsCases = [
38
+ {
39
+ name: 'Toolchain CRN not specified',
40
+ cmd: [CLI_PATH, COMMAND],
41
+ expected: /required option '-c, --toolchain-crn <crn>' not specified/,
42
+ },
43
+ {
44
+ name: 'Region is not specified',
45
+ cmd: [CLI_PATH, COMMAND, '-c', validCrn],
46
+ expected: /required option '-r, --region <region>' not specified/,
47
+ },
48
+ {
49
+ name: 'API Key is not specified',
50
+ cmd: [CLI_PATH, COMMAND, '-c', validCrn, '-r', TARGET_REGIONS[0]],
51
+ expected: /Environment variable 'IBMCLOUD_API_KEY' is required but not set/,
52
+ options: { env: { ...process.env, IBMCLOUD_API_KEY: '' } }
53
+ },
54
+ {
55
+ name: 'Invalid API Key provided',
56
+ cmd: [CLI_PATH, COMMAND, '-c', validCrn, '-r', TARGET_REGIONS[0]],
57
+ expected: /There was a problem getting a bearer token using IBMCLOUD_API_KEY/,
58
+ options: { env: { ...process.env, IBMCLOUD_API_KEY: 'not-a-valid-apikey' } }
59
+ },
60
+ {
61
+ name: 'Invalid region is provided',
62
+ cmd: [CLI_PATH, COMMAND, '-c', validCrn, '-r', mocks.invalidRegion],
63
+ expected: new RegExp(`option '-r, --region <region>' argument '${mocks.invalidRegion}' is invalid`)
64
+ },
65
+ {
66
+ name: 'Invalid CRN is provided',
67
+ cmd: [CLI_PATH, COMMAND, '-c', mocks.invalidCrn, '-r', TARGET_REGIONS[0]],
68
+ expected: /Provided toolchain CRN is invalid/,
69
+ },
70
+ {
71
+ name: 'Invalid Toolchain tag is provided',
72
+ cmd: [CLI_PATH, COMMAND, '-c', validCrn, '-r', TARGET_REGIONS[0], '-t', mocks.invalidTag],
73
+ expected: /Provided tag is invalid/,
74
+ },
75
+ {
76
+ name: 'Invalid Toolchain name is provided',
77
+ cmd: [CLI_PATH, COMMAND, '-c', validCrn, '-r', TARGET_REGIONS[0], '-n', mocks.invalidTcName],
78
+ expected: /Provided toolchain name is invalid/,
79
+ },
80
+ {
81
+ name: 'Invalid Resource Group name is provided',
82
+ cmd: [CLI_PATH, COMMAND, '-c', validCrn, '-r', TARGET_REGIONS[0], '-g', mocks.invalidRgName],
83
+ expected: /The resource group with provided ID or name was not found or is not accessible/,
84
+ },
85
+ {
86
+ name: 'Invalid Resource Group ID is provided',
87
+ cmd: [CLI_PATH, COMMAND, '-c', validCrn, '-r', TARGET_REGIONS[0], '-g', mocks.invalidRgId],
88
+ expected: /The resource group with provided ID or name was not found or is not accessible/,
89
+ },
90
+ ];
91
+
92
+ for (const { name, cmd, expected, options } of invalidArgsCases) {
93
+ it(`Invalid args: ${name}`, async () => {
94
+ await expectExecError(cmd, expected, options);
95
+ });
96
+ }
97
+
98
+ const invalidUserInputCases = [
99
+ {
100
+ name: 'Invalid Toolchain tag is provided',
101
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['empty'].crn, '-r', TARGET_REGIONS[0]],
102
+ expected: /Provided tag is invalid/,
103
+ options: {
104
+ questionAnswerMap: { '(Recommended) Add a tag to the cloned toolchain (Ctrl-C to abort):' : mocks.invalidTag },
105
+ exitCondition: 'Validation failed',
106
+ timeout: 5000
107
+ }
108
+ },
109
+ {
110
+ name: 'Invalid Toolchain name is provided',
111
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['empty'].crn, '-r', TEST_TOOLCHAINS['empty'].region],
112
+ expected: /Provided toolchain name is invalid/,
113
+ options: {
114
+ questionAnswerMap: { [`(Recommended) Edit the cloned toolchain's name [default: ${TEST_TOOLCHAINS['empty'].name}] (Ctrl-C to abort):`] : mocks.invalidTcName },
115
+ exitCondition: 'Validation failed',
116
+ timeout: 5000
117
+ }
118
+ },
119
+ ];
120
+
121
+ for (const { name, cmd, expected, options } of invalidUserInputCases) {
122
+ it(`Invalid user input in prompts: ${name}`, async () => {
123
+ await expectPtyOutputToMatch(cmd, expected, options);
124
+ });
125
+ }
126
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Licensed Materials - Property of IBM
3
+ * (c) Copyright IBM Corporation 2025. All Rights Reserved.
4
+ *
5
+ * Note to U.S. Government Users Restricted Rights:
6
+ * Use, duplication or disclosure restricted by GSA ADP Schedule
7
+ * Contract with IBM Corp.
8
+ */
9
+
10
+ const invalidCrn = 'crn:v1:bluemix:public:not-a-toolchain:ca-tor:a/955ce52f7b4f4aad8020fbee3e7a8sje:dacff581-8a40sdsdf3kfsd-12n3s::';
11
+
12
+ const invalidRegion = 'not-br-sao';
13
+
14
+ const invalidTcName = 'invalidToolchainName@';
15
+
16
+ const invalidTag = 'invalid@Tag';
17
+
18
+ const invalidRgId = 'invalid#RgId';
19
+
20
+ const invalidRgName = 'invalid#Rg@Name';
21
+
22
+ export default {
23
+ invalidCrn,
24
+ invalidRegion,
25
+ invalidTcName,
26
+ invalidTag,
27
+ invalidRgId,
28
+ invalidRgName
29
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Licensed Materials - Property of IBM
3
+ * (c) Copyright IBM Corporation 2025. All Rights Reserved.
4
+ *
5
+ * Note to U.S. Government Users Restricted Rights:
6
+ * Use, duplication or disclosure restricted by GSA ADP Schedule
7
+ * Contract with IBM Corp.
8
+ */
9
+
10
+ export const TEST_TOOLCHAINS = {
11
+ 'empty': {
12
+ 'name': 'empty-toolchain',
13
+ 'crn': 'crn:v1:bluemix:public:toolchain:ca-tor:a/9e8559fac61ee9fc74d3e595fa75d147:a3d4a26a-b447-490e-af84-356bbe63dd1c::',
14
+ 'region': 'ca-tor'
15
+ },
16
+ 'misconfigured': {
17
+ 'name': 'misconfigured-toolchain',
18
+ 'crn': 'crn:v1:bluemix:public:toolchain:eu-es:a/9e8559fac61ee9fc74d3e595fa75d147:0ccfaa70-ca90-47db-8246-f4ecfc6ad8f3::',
19
+ 'region': 'eu-es'
20
+ }
21
+ };
package/test/setup.js ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Licensed Materials - Property of IBM
3
+ * (c) Copyright IBM Corporation 2025. All Rights Reserved.
4
+ *
5
+ * Note to U.S. Government Users Restricted Rights:
6
+ * Use, duplication or disclosure restricted by GSA ADP Schedule
7
+ * Contract with IBM Corp.
8
+ */
9
+
10
+ import fs from 'node:fs';
11
+ import { resolve } from 'node:path'
12
+ import nconf from 'nconf';
13
+
14
+ import { logger } from '../cmd/utils/logger.js';
15
+
16
+ nconf.env('__');
17
+ nconf.file('local', 'test/config/local.json');
18
+ process.env.IBMCLOUD_API_KEY = nconf.get('IBMCLOUD_API_KEY');
19
+ process.env.DISABLE_SPINNER = nconf.get('DISABLE_SPINNER');
20
+ process.env.LOG_DUMP = nconf.get('LOG_DUMP') || false; // Disable each individual test case's process's log file generation by default
21
+
22
+ const TEMP_DIR = resolve(nconf.get('TEST_TEMP_DIR'));
23
+ const LOG_DIR = resolve(nconf.get('TEST_LOG_DIR'));
24
+ const DEBUG_MODE = nconf.get('TEST_DEBUG_MODE');
25
+
26
+ export const mochaHooks = {
27
+ beforeAll() {
28
+ if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true });
29
+ if (fs.existsSync(LOG_DIR)) fs.rmSync(LOG_DIR, { recursive: true });
30
+ },
31
+ beforeEach() {
32
+ if (DEBUG_MODE === true && LOG_DIR) {
33
+ const testTitle = this.currentTest.title.toLowerCase().replaceAll(':', '').replaceAll(' ', '-');
34
+ const logFile = this.currentTest.parent.command ?
35
+ resolve(LOG_DIR, this.currentTest.parent.command, testTitle + '.log') :
36
+ resolve(LOG_DIR, testTitle + '.log');
37
+ logger.createLogStream(logFile);
38
+ }
39
+ },
40
+ afterEach() {
41
+ logger.close();
42
+ },
43
+ afterAll() {
44
+ if (fs.existsSync(TEMP_DIR) && DEBUG_MODE === false) fs.rmSync(TEMP_DIR, { recursive: true });
45
+ },
46
+ };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Licensed Materials - Property of IBM
3
+ * (c) Copyright IBM Corporation 2025. All Rights Reserved.
4
+ *
5
+ * Note to U.S. Government Users Restricted Rights:
6
+ * Use, duplication or disclosure restricted by GSA ADP Schedule
7
+ * Contract with IBM Corp.
8
+ */
9
+
10
+ import { promisify } from 'util';
11
+ import child_process from 'child_process';
12
+ import stripAnsi from 'strip-ansi';
13
+ import pty from 'node-pty';
14
+ import nconf from 'nconf';
15
+ import { expect } from 'chai';
16
+
17
+ import { getBearerToken, deleteToolchain } from '../../cmd/utils/requests.js';
18
+ import { logger } from '../../cmd/utils/logger.js';
19
+
20
+ nconf.env('__');
21
+ nconf.file('local', 'test/config/local.json');
22
+
23
+ const IBMCLOUD_API_KEY = nconf.get('IBMCLOUD_API_KEY');
24
+
25
+ function cleanOutput(data) {
26
+ if (typeof data === 'string') return stripAnsi(data).replace(/\r/g, '').trim();
27
+ }
28
+
29
+ export async function execCommand(fullCommand, options) {
30
+ const commandStr = `node ${fullCommand.join(' ')}`;
31
+ const execPromise = promisify(child_process.exec);
32
+ try {
33
+ const { stdout, stderr } = await execPromise(commandStr, options);
34
+ if (stderr) {
35
+ const err = new Error(cleanOutput(stderr));
36
+ err.stdout = cleanOutput(stdout);
37
+ err.stderr = cleanOutput(stderr);
38
+ throw err;
39
+ }
40
+ return cleanOutput(stdout);
41
+ } catch (e) {
42
+ const err = new Error(cleanOutput(e.message));
43
+ err.stdout = cleanOutput(e.stdout);
44
+ err.stderr = cleanOutput(e.stderr);
45
+ err.code = e.code;
46
+ err.signal = e.signal;
47
+ throw err;
48
+ }
49
+ }
50
+
51
+ export function runPtyProcess(fullCommand, options) {
52
+ const {
53
+ timeout = 0,
54
+ cwd = process.cwd(),
55
+ env = process.env,
56
+ questionAnswerMap = {},
57
+ exitCondition = '',
58
+ } = options;
59
+
60
+ return new Promise((resolve, reject) => {
61
+ try {
62
+ const ptyProcess = pty.spawn('node', fullCommand, {
63
+ name: 'xterm-color',
64
+ cols: 80,
65
+ rows: 30,
66
+ cwd: cwd,
67
+ env: env
68
+ });
69
+
70
+ let output = '';
71
+ let timedOut = false;
72
+
73
+ const timer = timeout > 0 ? setTimeout(() => {
74
+ timedOut = true;
75
+ ptyProcess.kill();
76
+ }, timeout) : null;
77
+
78
+ ptyProcess.onData((data) => {
79
+ output += data;
80
+ for (const [question, answer] of Object.entries(questionAnswerMap)) {
81
+ if (data.includes(question)) {
82
+ ptyProcess.write(answer + '\r');
83
+ }
84
+ }
85
+ if (exitCondition.length > 0 && data.includes(exitCondition)) {
86
+ ptyProcess.kill();
87
+ resolve(cleanOutput(output));
88
+ }
89
+ });
90
+
91
+ ptyProcess.onExit(({ exitCode }) => {
92
+ if (timer) clearTimeout(timer);
93
+ if (timedOut) {
94
+ reject(new Error(`ERROR: Process timed out after ${timeout}ms\n\nCommand: ${'node ' + fullCommand.join(' ')}\n\nOutput:\n${cleanOutput(output)}`));
95
+ }
96
+ if (exitCode !== 0) {
97
+ reject(new Error(`ERROR: Process exited with code ${exitCode}\n\nCommand: ${'node ' + fullCommand.join(' ')}\n\nOutput:\n${cleanOutput(output)}`));
98
+ } else {
99
+ resolve(cleanOutput(output));
100
+ }
101
+ });
102
+
103
+ } catch (err) {
104
+ reject(err);
105
+ }
106
+ });
107
+ }
108
+
109
+ export async function testSuiteCleanup(toolchainsToDelete) {
110
+ if (toolchainsToDelete && typeof toolchainsToDelete === 'object' && toolchainsToDelete.size > 0) {
111
+ const token = await getBearerToken(IBMCLOUD_API_KEY);
112
+ const deletePromises = [...toolchainsToDelete.entries()].map(([id, region]) => deleteToolchain(token, id, region));
113
+ await Promise.all(deletePromises);
114
+ }
115
+ }
116
+
117
+ export async function expectExecError(fullCommand, expectedMessage, options) {
118
+ try {
119
+ const output = await execCommand(fullCommand, options);
120
+ logger.dump(output);
121
+ throw new Error('Expected command to fail but it succeeded');
122
+ } catch (e) {
123
+ logger.dump(e.message);
124
+ expect(e.message).to.match(expectedMessage);
125
+ }
126
+ }
127
+
128
+ export async function expectPtyOutputToMatch(fullCommand, expectedMessage, options) {
129
+ try {
130
+ const output = await runPtyProcess(fullCommand, options);
131
+ logger.dump(output);
132
+ expect(output).to.match(expectedMessage);
133
+ } catch (e) {
134
+ logger.dump(e.message);
135
+ throw (e);
136
+ }
137
+ }