@ibm-cloud/cd-tools 1.2.3 → 1.2.5

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/README.md CHANGED
@@ -48,7 +48,7 @@ The tools are provided as an [npx](https://docs.npmjs.com/cli/commands/npx) comm
48
48
 
49
49
  ```shell-session
50
50
  $ npx @ibm-cloud/cd-tools
51
- Usage: npx @ibm-cloud/cd-tools [options] [command]
51
+ Usage: @ibm-cloud/cd-tools [options] [command]
52
52
 
53
53
  Tools for migrating Toolchains, Delivery Pipelines, and Git Repos and Issue Tracking projects.
54
54
 
@@ -237,7 +237,7 @@ async function main(options) {
237
237
  }, 5000);
238
238
 
239
239
  await initProviderFile(sourceRegion, TEMP_DIR);
240
- await runTerraformInit(TEMP_DIR);
240
+ await runTerraformInit(TEMP_DIR, verbosity);
241
241
 
242
242
  nonSecretRefs = await importTerraform(bearer, apiKey, sourceRegion, sourceToolchainId, targetToolchainName, policyIds, TEMP_DIR, isCompact, verbosity);
243
243
  };
@@ -303,7 +303,8 @@ async function main(options) {
303
303
  'Running terraform init...',
304
304
  'Terraform successfully initialized',
305
305
  LOG_STAGES.tf,
306
- outputDir
306
+ outputDir,
307
+ verbosity
307
308
  );
308
309
 
309
310
  logger.info(`DRY_RUN: ${dryRun}, running terraform apply...`, LOG_STAGES.tf);
@@ -10,8 +10,11 @@
10
10
  import axios from 'axios';
11
11
  import axiosRetry from 'axios-retry';
12
12
 
13
+ import mocks from '../../test/data/mocks.js'
13
14
  import { logger, LOG_STAGES } from './logger.js';
14
15
 
16
+ const MOCK_ALL_REQUESTS = process.env.MOCK_ALL_REQUESTS === 'true' || 'false';
17
+
15
18
  axiosRetry(axios, {
16
19
  retries: 3,
17
20
  retryDelay: axiosRetry.exponentialDelay,
@@ -142,6 +145,10 @@ async function getToolchainsByName(bearer, accountId, toolchainName) {
142
145
  }
143
146
 
144
147
  async function getCdInstanceByRegion(bearer, accountId, region) {
148
+ if (MOCK_ALL_REQUESTS && process.env.MOCK_GET_CD_INSTANCE_BY_REGION_SCENARIO) {
149
+ return mocks.getCdInstanceByRegionResponses[process.env.MOCK_GET_CD_INSTANCE_BY_REGION_SCENARIO].data.items.length > 0;
150
+ }
151
+
145
152
  const apiBaseUrl = 'https://api.global-search-tagging.cloud.ibm.com/v3';
146
153
  const options = {
147
154
  url: apiBaseUrl + '/resources/search',
@@ -403,7 +410,7 @@ async function deleteToolchain(bearer, toolchainId, region) {
403
410
  };
404
411
  const response = await axios(options);
405
412
  switch (response.status) {
406
- case 200:
413
+ case 204:
407
414
  return toolchainId;
408
415
  default:
409
416
  throw Error(response.statusText);
@@ -15,7 +15,7 @@ import { parse as tfToJson } from '@cdktf/hcl2json'
15
15
  import { jsonToTf } from 'json-to-tf';
16
16
 
17
17
  import { validateToolchainId, validateGritUrl } from './validate.js';
18
- import { logger } from './logger.js';
18
+ import { logger, LOG_STAGES } from './logger.js';
19
19
  import { getRandChars, promptUserInput, replaceUrlRegion } from './utils.js';
20
20
 
21
21
  // promisify
@@ -300,12 +300,16 @@ async function setupTerraformFiles({ token, srcRegion, targetRegion, targetTag,
300
300
  return Promise.all(promises);
301
301
  }
302
302
 
303
- async function runTerraformPlanGenerate(dir, fileName) {
304
- return await execPromise(`terraform plan -generate-config-out="${fileName}"`, { cwd: dir });
303
+ async function runTerraformInit(dir, verbosity) {
304
+ logger.log('Running command \'terraform init\'', LOG_STAGES.tf);
305
+ const out = await execPromise('terraform init', { cwd: dir });
306
+ if (verbosity >= 2) logger.print(out, '\n');
307
+ logger.log('Command \'terraform init\' completed', LOG_STAGES.tf);
308
+ return out;
305
309
  }
306
310
 
307
- async function runTerraformInit(dir) {
308
- return await execPromise('terraform init', { cwd: dir });
311
+ async function runTerraformPlanGenerate(dir, fileName) {
312
+ return await execPromise(`terraform plan -generate-config-out="${fileName}"`, { cwd: dir });
309
313
  }
310
314
 
311
315
  // primarily used to get number of resources to be used
package/config.js CHANGED
@@ -126,90 +126,90 @@ Format:
126
126
  {
127
127
  key: str, // tool parameter key
128
128
  tfKey?: str, // terraform-equivalent key
129
- prereq?: { key: string, values: [string] }, // proceed only if tool parameter "prereq.key" is one of "values"
129
+ prereq?: { key: string, values: [string] }, // proceed only if tool parameter 'prereq.key' is one of 'values'
130
130
  required?: bool // is this key required for terraform?
131
131
  }
132
132
  ... which represents a secret/sensitive value
133
133
  */
134
134
  const SECRET_KEYS_MAP = {
135
- "artifactory": [
136
- { key: "token", tfKey: "token" }
135
+ 'artifactory': [
136
+ { key: 'token', tfKey: 'token' }
137
137
  ],
138
- "cloudobjectstorage": [
139
- { key: "cos_api_key", tfKey: "cos_api_key", prereq: { key: "auth_type", values: ["apikey"] } },
140
- { key: "hmac_access_key_id", tfKey: "hmac_access_key_id", prereq: { key: "auth_type", values: ["hmac"] } },
141
- { key: "hmac_secret_access_key", tfKey: "hmac_secret_access_key", prereq: { key: "auth_type", values: ["hmac"] } },
138
+ 'cloudobjectstorage': [
139
+ { key: 'cos_api_key', tfKey: 'cos_api_key', prereq: { key: 'auth_type', values: ['apikey'] } },
140
+ { key: 'hmac_access_key_id', tfKey: 'hmac_access_key_id', prereq: { key: 'auth_type', values: ['hmac'] } },
141
+ { key: 'hmac_secret_access_key', tfKey: 'hmac_secret_access_key', prereq: { key: 'auth_type', values: ['hmac'] } },
142
142
  ],
143
- "github_integrated": [
144
- { key: "api_token" } // no terraform equivalent
143
+ 'github_integrated': [
144
+ { key: 'api_token' } // no terraform equivalent
145
145
  ],
146
- "githubconsolidated": [
147
- { key: "api_token", tfKey: "api_token", prereq: { key: "auth_type", values: ["pat"] } },
146
+ 'githubconsolidated': [
147
+ { key: 'api_token', tfKey: 'api_token', prereq: { key: 'auth_type', values: ['pat'] } },
148
148
  ],
149
- "gitlab": [
150
- { key: "api_token", tfKey: "api_token", prereq: { key: "auth_type", values: ["pat"] } },
149
+ 'gitlab': [
150
+ { key: 'api_token', tfKey: 'api_token', prereq: { key: 'auth_type', values: ['pat'] } },
151
151
  ],
152
- "hashicorpvault": [
153
- { key: "token", tfKey: "token", prereq: { key: "authentication_method", values: ["github", "token"] } },
154
- { key: "role_id", tfKey: "role_id", prereq: { key: "authentication_method", values: ["approle"] } },
155
- { key: "secret_id", tfKey: "secret_id", prereq: { key: "authentication_method", values: ["approle"] } },
156
- { key: "password", tfKey: "password", prereq: { key: "authentication_method", values: ["userpass"] } },
152
+ 'hashicorpvault': [
153
+ { key: 'token', tfKey: 'token', prereq: { key: 'authentication_method', values: ['github', 'token'] } },
154
+ { key: 'role_id', tfKey: 'role_id', prereq: { key: 'authentication_method', values: ['approle'] } },
155
+ { key: 'secret_id', tfKey: 'secret_id', prereq: { key: 'authentication_method', values: ['approle'] } },
156
+ { key: 'password', tfKey: 'password', prereq: { key: 'authentication_method', values: ['userpass'] } },
157
157
  ],
158
- "hostedgit": [
159
- { key: "api_token", tfKey: "api_token", prereq: { key: "auth_type", values: ["pat"] } },
158
+ 'hostedgit': [
159
+ { key: 'api_token', tfKey: 'api_token', prereq: { key: 'auth_type', values: ['pat'] } },
160
160
  ],
161
- "jenkins": [
162
- { key: "api_token", tfKey: "api_token" },
161
+ 'jenkins': [
162
+ { key: 'api_token', tfKey: 'api_token' },
163
163
  ],
164
- "jira": [
165
- { key: "password", tfKey: "api_token" },
164
+ 'jira': [
165
+ { key: 'password', tfKey: 'api_token' },
166
166
  ],
167
- "nexus": [
168
- { key: "token", tfKey: "token" },
167
+ 'nexus': [
168
+ { key: 'token', tfKey: 'token' },
169
169
  ],
170
- "pagerduty": [
171
- { key: "service_key", tfKey: "service_key", required: true },
170
+ 'pagerduty': [
171
+ { key: 'service_key', tfKey: 'service_key', required: true },
172
172
  ],
173
- "private_worker": [
174
- { key: "workerQueueCredentials", tfKey: "worker_queue_credentials", required: true },
173
+ 'private_worker': [
174
+ { key: 'workerQueueCredentials', tfKey: 'worker_queue_credentials', required: true },
175
175
  ],
176
- "saucelabs": [
177
- { key: "key", tfKey: "access_key", required: true },
176
+ 'saucelabs': [
177
+ { key: 'key', tfKey: 'access_key', required: true },
178
178
  ],
179
- "security_compliance": [
180
- { key: "scc_api_key", tfKey: "scc_api_key", prereq: { key: "use_profile_attachment", values: ["enabled"] } },
179
+ 'security_compliance': [
180
+ { key: 'scc_api_key', tfKey: 'scc_api_key', prereq: { key: 'use_profile_attachment', values: ['enabled'] } },
181
181
  ],
182
- "slack": [
183
- { key: "api_token", tfKey: "webhook", required: true },
182
+ 'slack': [
183
+ { key: 'api_token', tfKey: 'webhook', required: true },
184
184
  ],
185
- "sonarqube": [
186
- { key: "user_password", tfKey: "user_password" },
185
+ 'sonarqube': [
186
+ { key: 'user_password', tfKey: 'user_password' },
187
187
  ]
188
188
  };
189
189
 
190
190
  // maps tool parameter tool_type_id to terraform resource type
191
191
  const SUPPORTED_TOOLS_MAP = {
192
- "appconfig": "ibm_cd_toolchain_tool_appconfig",
193
- "artifactory": "ibm_cd_toolchain_tool_artifactory",
194
- "bitbucketgit": "ibm_cd_toolchain_tool_bitbucketgit",
195
- "private_worker": "ibm_cd_toolchain_tool_privateworker",
196
- "draservicebroker": "ibm_cd_toolchain_tool_devopsinsights",
197
- "eventnotifications": "ibm_cd_toolchain_tool_eventnotifications",
198
- "hostedgit": "ibm_cd_toolchain_tool_hostedgit",
199
- "githubconsolidated": "ibm_cd_toolchain_tool_githubconsolidated",
200
- "gitlab": "ibm_cd_toolchain_tool_gitlab",
201
- "hashicorpvault": "ibm_cd_toolchain_tool_hashicorpvault",
202
- "jenkins": "ibm_cd_toolchain_tool_jenkins",
203
- "jira": "ibm_cd_toolchain_tool_jira",
204
- "keyprotect": "ibm_cd_toolchain_tool_keyprotect",
205
- "nexus": "ibm_cd_toolchain_tool_nexus",
206
- "customtool": "ibm_cd_toolchain_tool_custom",
207
- "saucelabs": "ibm_cd_toolchain_tool_saucelabs",
208
- "secretsmanager": "ibm_cd_toolchain_tool_secretsmanager",
209
- "security_compliance": "ibm_cd_toolchain_tool_securitycompliance",
210
- "slack": "ibm_cd_toolchain_tool_slack",
211
- "sonarqube": "ibm_cd_toolchain_tool_sonarqube",
212
- "pipeline": "ibm_cd_toolchain_tool_pipeline"
192
+ 'appconfig': 'ibm_cd_toolchain_tool_appconfig',
193
+ 'artifactory': 'ibm_cd_toolchain_tool_artifactory',
194
+ 'bitbucketgit': 'ibm_cd_toolchain_tool_bitbucketgit',
195
+ 'private_worker': 'ibm_cd_toolchain_tool_privateworker',
196
+ 'draservicebroker': 'ibm_cd_toolchain_tool_devopsinsights',
197
+ 'eventnotifications': 'ibm_cd_toolchain_tool_eventnotifications',
198
+ 'hostedgit': 'ibm_cd_toolchain_tool_hostedgit',
199
+ 'githubconsolidated': 'ibm_cd_toolchain_tool_githubconsolidated',
200
+ 'gitlab': 'ibm_cd_toolchain_tool_gitlab',
201
+ 'hashicorpvault': 'ibm_cd_toolchain_tool_hashicorpvault',
202
+ 'jenkins': 'ibm_cd_toolchain_tool_jenkins',
203
+ 'jira': 'ibm_cd_toolchain_tool_jira',
204
+ 'keyprotect': 'ibm_cd_toolchain_tool_keyprotect',
205
+ 'nexus': 'ibm_cd_toolchain_tool_nexus',
206
+ 'customtool': 'ibm_cd_toolchain_tool_custom',
207
+ 'saucelabs': 'ibm_cd_toolchain_tool_saucelabs',
208
+ 'secretsmanager': 'ibm_cd_toolchain_tool_secretsmanager',
209
+ 'security_compliance': 'ibm_cd_toolchain_tool_securitycompliance',
210
+ 'slack': 'ibm_cd_toolchain_tool_slack',
211
+ 'sonarqube': 'ibm_cd_toolchain_tool_sonarqube',
212
+ 'pipeline': 'ibm_cd_toolchain_tool_pipeline'
213
213
  };
214
214
 
215
215
  const VAULT_REGEX = [
package/index.js CHANGED
@@ -10,11 +10,12 @@
10
10
 
11
11
  import { program } from 'commander';
12
12
  import * as commands from './cmd/index.js'
13
+ import packageJson from './package.json' with { type: "json" };
13
14
 
14
15
  program
15
- .name('index.js')
16
+ .name(packageJson.name)
16
17
  .description('Tools and utilities for the IBM Cloud Continuous Delivery service and resources.')
17
- .version('0.0.1')
18
+ .version(packageJson.version)
18
19
  .showHelpAfterError();
19
20
 
20
21
  for (let i in commands) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibm-cloud/cd-tools",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "Tools and utilities for the IBM Cloud Continuous Delivery service and resources",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,7 +15,7 @@
15
15
  "author": "IBM Corp.",
16
16
  "license": "Apache-2.0",
17
17
  "scripts": {
18
- "test": "mocha --require \"test/setup.js\" \"test/copy-toolchain/*.test.js\""
18
+ "test": "mocha --require \"test/setup.js\" --retries 3 --parallel \"test/copy-toolchain/*.test.js\""
19
19
  },
20
20
  "dependencies": {
21
21
  "@cdktf/hcl2json": "^0.21.0",
package/test/README.md CHANGED
@@ -30,11 +30,12 @@ Before running tests, ensure that you have completed the setup steps in the main
30
30
  ### Test Configuration
31
31
  You can customize the behavior of the tests by defining configuration properties in the `test/config/local.json` file.
32
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 |
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/.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 |
41
+ | `VERBOSE_MODE` | `boolean` | `false` | When set to `true`, each test case's log output increases |
@@ -4,5 +4,6 @@
4
4
  "TEST_LOG_DIR": "test/.logs",
5
5
  "IBMCLOUD_API_KEY": "<YOUR IBMCLOUD API KEY>",
6
6
  "LOG_DUMP": true,
7
- "DISABLE_SPINNER": true
7
+ "DISABLE_SPINNER": true,
8
+ "VERBOSE_MODE": false
8
9
  }
@@ -0,0 +1,198 @@
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
+ import fs from 'node:fs';
13
+
14
+ import * as chai from 'chai';
15
+ chai.config.truncateThreshold = 0;
16
+ import { expect, assert } from 'chai';
17
+
18
+ import { assertPtyOutput, assertExecError, areFilesInDir, deleteCreatedToolchains, parseTcIdAndRegion } from '../utils/testUtils.js';
19
+ import { getBearerToken, getToolchain } from '../../cmd/utils/requests.js';
20
+ import { TEST_TOOLCHAINS, DEFAULT_RG_ID, R2R_CLI_RG_ID } from '../data/test-toolchains.js';
21
+ import { TARGET_REGIONS } from '../../config.js';
22
+
23
+ nconf.env('__');
24
+ nconf.file('local', 'test/config/local.json');
25
+
26
+ const TEMP_DIR = nconf.get('TEST_TEMP_DIR');
27
+ const VERBOSE_MODE = nconf.get('VERBOSE_MODE');
28
+ const IBMCLOUD_API_KEY = nconf.get('IBMCLOUD_API_KEY');
29
+
30
+ const CLI_PATH = path.resolve('index.js');
31
+ const COMMAND = 'copy-toolchain';
32
+
33
+ const toolchainsToDelete = new Map();
34
+ after(async () => await deleteCreatedToolchains(toolchainsToDelete));
35
+
36
+ describe('copy-toolchain: Test functionalities', function () {
37
+ this.timeout('300s');
38
+ this.command = COMMAND;
39
+ const testCases = [
40
+ {
41
+ name: 'Terraform Version Verification',
42
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['empty'].crn, '-r', TARGET_REGIONS[0]],
43
+ expected: /✔ Terraform Version:/,
44
+ options: {
45
+ exitCondition: '(Recommended) Add a tag to the cloned toolchain (Ctrl-C to abort):',
46
+ timeout: 10000
47
+ }
48
+ },
49
+ {
50
+ name: 'Check if CD instance exists in target region',
51
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['empty'].crn, '-r', TARGET_REGIONS[0]],
52
+ expected: new RegExp(`Could not find a Continuous Delivery instance in the target region '${TARGET_REGIONS[0]}', please create one before proceeding.`),
53
+ options: {
54
+ exitCondition: `Could not find a Continuous Delivery instance in the target region '${TARGET_REGIONS[0]}', please create one before proceeding.`,
55
+ timeout: 10000,
56
+ env: { ...process.env, MOCK_ALL_REQUESTS: 'true', MOCK_GET_CD_INSTANCE_BY_REGION_SCENARIO: 'NOT_FOUND' }
57
+ }
58
+ },
59
+ {
60
+ name: 'Log file is created successfully',
61
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['empty'].crn, '-r', TARGET_REGIONS[0]],
62
+ expected: null,
63
+ options: {
64
+ exitCondition: '(Recommended) Add a tag to the cloned toolchain (Ctrl-C to abort):',
65
+ timeout: 10000,
66
+ cwd: TEMP_DIR + '/' + 'log-file-is-created-successfully'
67
+ },
68
+ assertionFunc: () => areFilesInDir(TEMP_DIR + '/' + 'log-file-is-created-successfully', ['.log'])
69
+ },
70
+ {
71
+ name: 'Force Flag bypasses all user prompts',
72
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['empty'].crn, '-r', TEST_TOOLCHAINS['empty'].region, '-f'],
73
+ expected: null,
74
+ options: {
75
+ timeout: 120000,
76
+ },
77
+ assertionFunc: async (output) => {
78
+ // Should bypass everything and clone the toolchain
79
+ output.match(/See cloned toolchain:/);
80
+ const { toolchainId, region } = parseTcIdAndRegion(output);
81
+ const token = await getBearerToken(IBMCLOUD_API_KEY);
82
+ const toolchainData = await getToolchain(token, toolchainId, region);
83
+ assert.isTrue(toolchainData.id === toolchainId, 'Was toolchain created successfully without any confirmations?');
84
+ }
85
+ },
86
+ {
87
+ name: 'Prompt User when toolchain name already exists in resource group',
88
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['empty'].crn, '-r', TARGET_REGIONS[0]],
89
+ expected: new RegExp(`Warning! A toolchain named \'${TEST_TOOLCHAINS['empty'].name}\' already exists in:[\\s\\S]*?Resource Group:[\\s\\S]*?${R2R_CLI_RG_ID}`),
90
+ options: {
91
+ exitCondition: '(Recommended) Add a tag to the cloned toolchain (Ctrl-C to abort):',
92
+ timeout: 10000
93
+ }
94
+ },
95
+ {
96
+ name: 'Prompt User when toolchain name already exists in region',
97
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['empty'].crn, '-r', TEST_TOOLCHAINS['empty'].region, '-g', DEFAULT_RG_ID],
98
+ expected: new RegExp(`Warning! A toolchain named \'${TEST_TOOLCHAINS['empty'].name}\' already exists in:[\\s\\S]*?Region: ${TEST_TOOLCHAINS['empty'].region}`),
99
+ options: {
100
+ exitCondition: '(Recommended) Add a tag to the cloned toolchain (Ctrl-C to abort):',
101
+ timeout: 10000
102
+ }
103
+ },
104
+ {
105
+ name: 'Dry Run Flag does not clone a toolchain',
106
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['empty'].crn, '-r', TEST_TOOLCHAINS['empty'].region, '-D'],
107
+ expected: null,
108
+ options: {
109
+ timeout: 100000,
110
+ questionAnswerMap: {
111
+ '(Recommended) Add a tag to the cloned toolchain (Ctrl-C to abort):': '',
112
+ [`(Recommended) Edit the cloned toolchain's name [default: ${TEST_TOOLCHAINS['empty'].name}] (Ctrl-C to abort):`]: '',
113
+ },
114
+ cwd: TEMP_DIR + '/' + 'dry-run-flag-does-not-clone-a-toolchain'
115
+ },
116
+ assertionFunc: (output) => {
117
+ expect(output).to.match(/DRY_RUN: true, skipping terraform apply/);
118
+ assert.isTrue(areFilesInDir(TEMP_DIR + '/' + 'dry-run-flag-does-not-clone-a-toolchain', ['cd_toolchain.tf', 'output.tf']));
119
+ }
120
+ },
121
+ {
122
+ name: 'Silent flag suppresses info, debug and log messages',
123
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['empty'].crn, '-r', TEST_TOOLCHAINS['empty'].region, '-s'],
124
+ expected: null,
125
+ options: {
126
+ timeout: 100000,
127
+ questionAnswerMap: {
128
+ '(Recommended) Add a tag to the cloned toolchain (Ctrl-C to abort):': '',
129
+ [`(Recommended) Edit the cloned toolchain's name [default: ${TEST_TOOLCHAINS['empty'].name}] (Ctrl-C to abort):`]: '',
130
+ },
131
+ },
132
+ assertionFunc: (output) => {
133
+ // finds any [INFO] level logs that matches '[INFO] ...' but not '[INFO] See cloned toolchain...'
134
+ expect(output).to.not.match(/^(?!.*\[INFO\]\s+See cloned toolchain).*\[INFO\].*$/m);
135
+
136
+ expect(output).to.not.match(/\[DEBUG\]/);
137
+ expect(output).to.not.match(/\[LOG\]/);
138
+ }
139
+ },
140
+ {
141
+ name: 'Compact flag only generates one terraform file',
142
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['single-pl'].crn, '-r', TEST_TOOLCHAINS['single-pl'].region, '-D', '-f', '-C'],
143
+ expected: null,
144
+ options: {
145
+ timeout: 100000,
146
+ cwd: TEMP_DIR + '/' + 'compact-flag-only-generates-one-tf-file'
147
+ },
148
+ assertionFunc: () => {
149
+ // check only resources.tf is created
150
+ assert.isFalse(
151
+ areFilesInDir(TEMP_DIR + '/' + 'compact-flag-only-generates-one-tf-file', [
152
+ 'cd_toolchain.tf',
153
+ 'cd_toolchain_tool_pipeline.tf',
154
+ 'cd_tekton_pipeline.tf',
155
+ ])
156
+ );
157
+ assert.isTrue(areFilesInDir(TEMP_DIR + '/' + 'compact-flag-only-generates-one-tf-file', ['resources.tf']));
158
+ }
159
+ },
160
+ {
161
+ name: 'Prompt user when OAuth does not exist for Git tool in target region',
162
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['single-pl'].crn, '-r', TEST_TOOLCHAINS['single-pl'].region, '-D'],
163
+ expected: /Warning! The following git tool integration\(s\) are not authorized in the target region/,
164
+ options: {
165
+ timeout: 60000,
166
+ questionAnswerMap: {
167
+ '(Recommended) Add a tag to the cloned toolchain (Ctrl-C to abort):': '',
168
+ [`(Recommended) Edit the cloned toolchain's name [default: ${TEST_TOOLCHAINS['single-pl'].name}] (Ctrl-C to abort):`]: '',
169
+ 'Only \'yes\' will be accepted to proceed. (Ctrl-C to abort)': 'yes'
170
+ },
171
+ }
172
+ }
173
+ ];
174
+
175
+ for (const { name, cmd, expected, options, assertionFunc } of testCases) {
176
+ if (VERBOSE_MODE) cmd.push('-v');
177
+ it(`${name}`, async () => {
178
+ const res = await assertPtyOutput(cmd, expected, options, assertionFunc);
179
+ if (res) toolchainsToDelete.set(res.toolchainId, res.region);
180
+ });
181
+ }
182
+
183
+ it('Check for existing .tf files in output directory', async () => {
184
+ const testDir = path.resolve(TEMP_DIR, 'check-for-existing-tf-files-in-out-dir');
185
+ const tfFilePath = path.resolve(testDir, 'empty.tf');
186
+ if (!fs.existsSync(testDir)) fs.mkdirSync(testDir, { recursive: true });
187
+ fs.writeFileSync(tfFilePath, '');
188
+
189
+ const cmd = [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['empty'].crn, '-r', TARGET_REGIONS[0], '-d', testDir];
190
+ if (VERBOSE_MODE) cmd.push('-v');
191
+
192
+ await assertExecError(
193
+ cmd,
194
+ /Output directory already has 1 '.tf' files, please specify a different output directory/,
195
+ { cwd: testDir }
196
+ );
197
+ });
198
+ });
@@ -9,31 +9,32 @@
9
9
 
10
10
  import path from 'node:path';
11
11
  import nconf from 'nconf';
12
+ import fs from 'node:fs';
12
13
 
13
14
  import * as chai from 'chai';
15
+ import { expect } from 'chai';
14
16
  chai.config.truncateThreshold = 0;
15
17
 
16
18
  import mocks from '../data/mocks.js';
17
- import { testSuiteCleanup, expectExecError, expectPtyOutputToMatch } from '../utils/testUtils.js';
19
+ import { assertExecError, assertPtyOutput } from '../utils/testUtils.js';
18
20
  import { TEST_TOOLCHAINS } from '../data/test-toolchains.js';
19
21
  import { TARGET_REGIONS } from '../../config.js';
20
22
 
21
23
  nconf.env('__');
22
24
  nconf.file('local', 'test/config/local.json');
23
25
 
26
+ const VERBOSE_MODE = nconf.get('VERBOSE_MODE');
27
+ const TEMP_DIR = nconf.get('TEST_TEMP_DIR');
28
+
24
29
  const CLI_PATH = path.resolve('index.js');
25
30
  const COMMAND = 'copy-toolchain';
26
31
 
27
- const toolchainsToDelete = new Map();
28
-
29
- after(async () => await testSuiteCleanup(toolchainsToDelete));
30
32
 
31
33
  describe('copy-toolchain: Test user input handling', function () {
32
- this.timeout('60s');
33
- this.command = 'copy-toolchain';
34
+ this.timeout('120s');
35
+ this.command = COMMAND;
34
36
 
35
37
  const validCrn = TEST_TOOLCHAINS['empty'].crn;
36
-
37
38
  const invalidArgsCases = [
38
39
  {
39
40
  name: 'Toolchain CRN not specified',
@@ -87,23 +88,51 @@ describe('copy-toolchain: Test user input handling', function () {
87
88
  cmd: [CLI_PATH, COMMAND, '-c', validCrn, '-r', TARGET_REGIONS[0], '-g', mocks.invalidRgId],
88
89
  expected: /The resource group with provided ID or name was not found or is not accessible/,
89
90
  },
91
+ {
92
+ name: 'Non-existent GRIT mapping file provided',
93
+ cmd: [CLI_PATH, COMMAND, '-c', validCrn, '-r', TARGET_REGIONS[0], '-G', 'non-existent.json'],
94
+ expected: /ENOENT: no such file or directory/
95
+ }
90
96
  ];
91
97
 
92
- for (const { name, cmd, expected, options } of invalidArgsCases) {
98
+ for (const { name, cmd, expected, options, assertionFn } of invalidArgsCases) {
99
+ if (VERBOSE_MODE) cmd.push('-v');
93
100
  it(`Invalid args: ${name}`, async () => {
94
- await expectExecError(cmd, expected, options);
101
+ await assertExecError(cmd, expected, options, assertionFn);
95
102
  });
96
103
  }
97
104
 
105
+ it('Invalid GRIT URL mapping provided in file', async () => {
106
+
107
+ const gritTestDir = path.resolve(TEMP_DIR, 'invalid-grit-mapping-provided-in-file');
108
+ const gritMappingFilePath = path.resolve(gritTestDir, mocks.invalidGritFileName);
109
+
110
+ if (!fs.existsSync(gritTestDir)) fs.mkdirSync(gritTestDir, { recursive: true });
111
+ fs.writeFileSync(gritMappingFilePath, JSON.stringify(mocks.invalidGritMapping, null, 2));
112
+
113
+ const cmd = [CLI_PATH, COMMAND, '-c', validCrn, '-r', TARGET_REGIONS[0], '-G', mocks.invalidGritFileName];
114
+ if (VERBOSE_MODE) cmd.push('-v');
115
+
116
+ await assertExecError(
117
+ cmd,
118
+ null,
119
+ { cwd: gritTestDir },
120
+ (err) => {
121
+ expect(err).to.match(/Error: Provided full GRIT url is not valid/);
122
+ expect(err).to.match(/One or more invalid entries in GRIT mapping file, error count: 2/);
123
+ }
124
+ );
125
+ });
126
+
98
127
  const invalidUserInputCases = [
99
128
  {
100
129
  name: 'Invalid Toolchain tag is provided',
101
130
  cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['empty'].crn, '-r', TARGET_REGIONS[0]],
102
131
  expected: /Provided tag is invalid/,
103
132
  options: {
104
- questionAnswerMap: { '(Recommended) Add a tag to the cloned toolchain (Ctrl-C to abort):' : mocks.invalidTag },
133
+ questionAnswerMap: { '(Recommended) Add a tag to the cloned toolchain (Ctrl-C to abort):': mocks.invalidTag },
105
134
  exitCondition: 'Validation failed',
106
- timeout: 5000
135
+ timeout: 10000
107
136
  }
108
137
  },
109
138
  {
@@ -111,16 +140,17 @@ describe('copy-toolchain: Test user input handling', function () {
111
140
  cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['empty'].crn, '-r', TEST_TOOLCHAINS['empty'].region],
112
141
  expected: /Provided toolchain name is invalid/,
113
142
  options: {
114
- questionAnswerMap: { [`(Recommended) Edit the cloned toolchain's name [default: ${TEST_TOOLCHAINS['empty'].name}] (Ctrl-C to abort):`] : mocks.invalidTcName },
143
+ questionAnswerMap: { [`(Recommended) Edit the cloned toolchain's name [default: ${TEST_TOOLCHAINS['empty'].name}] (Ctrl-C to abort):`]: mocks.invalidTcName },
115
144
  exitCondition: 'Validation failed',
116
- timeout: 5000
145
+ timeout: 10000
117
146
  }
118
- },
147
+ }
119
148
  ];
120
149
 
121
150
  for (const { name, cmd, expected, options } of invalidUserInputCases) {
151
+ if (VERBOSE_MODE) cmd.push('-v');
122
152
  it(`Invalid user input in prompts: ${name}`, async () => {
123
- await expectPtyOutputToMatch(cmd, expected, options);
153
+ await assertPtyOutput(cmd, expected, options);
124
154
  });
125
155
  }
126
156
  });
@@ -0,0 +1,117 @@
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 { assertTfResourcesInDir, assertPtyOutput } from '../utils/testUtils.js';
17
+ import { TEST_TOOLCHAINS } from '../data/test-toolchains.js';
18
+
19
+ nconf.env('__');
20
+ nconf.file('local', 'test/config/local.json');
21
+
22
+ const TEMP_DIR = nconf.get('TEST_TEMP_DIR');
23
+ const VERBOSE_MODE = nconf.get('VERBOSE_MODE');
24
+
25
+ const CLI_PATH = path.resolve('index.js');
26
+ const COMMAND = 'copy-toolchain';
27
+
28
+ describe('copy-toolchain: Test import-terraform output', function () {
29
+ this.timeout('300s');
30
+ this.command = COMMAND;
31
+
32
+ const testCases = [
33
+ {
34
+ name: 'Import 1PL-GHE-CC Toolchain',
35
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['1pl-ghe-cc'].crn, '-r', TEST_TOOLCHAINS['1pl-ghe-cc'].region, '-D', '-f'],
36
+ expected: null,
37
+ options: {
38
+ timeout: 100000,
39
+ cwd: TEMP_DIR + '/' + 'import-1pl-ghe-cc-toolchain'
40
+ },
41
+ assertionFunc: async () => {
42
+ await assertTfResourcesInDir(TEMP_DIR + '/' + 'import-1pl-ghe-cc-toolchain', {
43
+ ibm_cd_tekton_pipeline: 1,
44
+ ibm_cd_tekton_pipeline_definition: 1,
45
+ ibm_cd_tekton_pipeline_property: 1,
46
+ ibm_cd_tekton_pipeline_trigger: 1,
47
+ ibm_cd_tekton_pipeline_trigger_property: 1,
48
+ ibm_cd_toolchain: 1,
49
+ ibm_cd_toolchain_tool_custom: 1,
50
+ ibm_cd_toolchain_tool_githubconsolidated: 1,
51
+ ibm_cd_toolchain_tool_pipeline: 1,
52
+ ibm_cd_toolchain_tool_secretsmanager: 1,
53
+ ibm_cd_toolchain_tool_slack: 1,
54
+ ibm_iam_authorization_policy: 1
55
+ });
56
+ }
57
+ },
58
+ {
59
+ name: 'Import 1PL-GHE-CD Toolchain',
60
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['1pl-ghe-cd'].crn, '-r', TEST_TOOLCHAINS['1pl-ghe-cd'].region, '-D', '-f'],
61
+ expected: null,
62
+ options: {
63
+ timeout: 100000,
64
+ cwd: TEMP_DIR + '/' + 'import-1pl-ghe-cd-toolchain'
65
+ },
66
+ assertionFunc: async () => {
67
+ await assertTfResourcesInDir(TEMP_DIR + '/' + 'import-1pl-ghe-cd-toolchain', {
68
+ ibm_cd_tekton_pipeline: 1,
69
+ ibm_cd_tekton_pipeline_definition: 1,
70
+ ibm_cd_tekton_pipeline_property: 1,
71
+ ibm_cd_tekton_pipeline_trigger: 1,
72
+ ibm_cd_tekton_pipeline_trigger_property: 1,
73
+ ibm_cd_toolchain: 1,
74
+ ibm_cd_toolchain_tool_custom: 1,
75
+ ibm_cd_toolchain_tool_githubconsolidated: 1,
76
+ ibm_cd_toolchain_tool_pipeline: 1,
77
+ ibm_cd_toolchain_tool_secretsmanager: 1,
78
+ ibm_cd_toolchain_tool_slack: 1,
79
+ ibm_iam_authorization_policy: 1
80
+ });
81
+ }
82
+ },
83
+ {
84
+ name: 'Import DevSecOps-GRIT-CI Toolchain',
85
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['devsecops-grit-ci'].crn, '-r', TEST_TOOLCHAINS['devsecops-grit-ci'].region, '-D', '-f'],
86
+ expected: null,
87
+ options: {
88
+ timeout: 100000,
89
+ cwd: TEMP_DIR + '/' + 'import-devsecops-grit-ci-toolchain'
90
+ },
91
+ assertionFunc: async () => {
92
+ await assertTfResourcesInDir(TEMP_DIR + '/' + 'import-devsecops-grit-ci-toolchain', {
93
+ ibm_cd_tekton_pipeline: 1,
94
+ ibm_cd_tekton_pipeline_definition: 1,
95
+ ibm_cd_tekton_pipeline_property: 1,
96
+ ibm_cd_tekton_pipeline_trigger: 1,
97
+ ibm_cd_tekton_pipeline_trigger_property: 1,
98
+ ibm_cd_toolchain: 1,
99
+ ibm_cd_toolchain_tool_custom: 1,
100
+ ibm_cd_toolchain_tool_devopsinsights: 1,
101
+ ibm_cd_toolchain_tool_hostedgit: 1,
102
+ ibm_cd_toolchain_tool_pipeline: 1,
103
+ ibm_cd_toolchain_tool_secretsmanager: 1,
104
+ ibm_cd_toolchain_tool_slack: 1,
105
+ ibm_iam_authorization_policy: 1
106
+ })
107
+ }
108
+ },
109
+ ];
110
+
111
+ for (const { name, cmd, expected, options, assertionFunc } of testCases) {
112
+ if (VERBOSE_MODE) cmd.push('-v');
113
+ it(`${name}`, async () => {
114
+ await assertPtyOutput(cmd, expected, options, assertionFunc);
115
+ });
116
+ }
117
+ });
@@ -0,0 +1,107 @@
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
+ import { expect } from 'chai';
16
+
17
+ import { assertPtyOutput, deleteCreatedToolchains } 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 VERBOSE_MODE = nconf.get('VERBOSE_MODE');
25
+
26
+ const CLI_PATH = path.resolve('index.js');
27
+ const COMMAND = 'copy-toolchain';
28
+
29
+ const toolchainsToDelete = new Map();
30
+ after(async () => await deleteCreatedToolchains(toolchainsToDelete));
31
+
32
+ describe('copy-toolchain: Test tool validation', function () {
33
+ this.timeout('300s');
34
+ this.command = COMMAND;
35
+ const testCases = [
36
+ {
37
+ name: 'Misconfigured tool identified',
38
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['1pl-ghe-cd'].crn, '-r', TARGET_REGIONS[10]],
39
+ expected: /slack[\s\S]*?misconfigured/,
40
+ options: {
41
+ exitCondition: 'Caution: The above tool(s) will not be properly configured post migration. Do you want to proceed?',
42
+ questionAnswerMap: {
43
+ '(Recommended) Add a tag to the cloned toolchain (Ctrl-C to abort):': '',
44
+ },
45
+ timeout: 30000
46
+ }
47
+ },
48
+ {
49
+ name: 'Tools with plain text secrets identified',
50
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['1pl-ghe-cd'].crn, '-r', TARGET_REGIONS[10]],
51
+ expected: null,
52
+ options: {
53
+ exitCondition: 'Caution: The above tool(s) will not be properly configured post migration. Do you want to proceed?',
54
+ questionAnswerMap: {
55
+ '(Recommended) Add a tag to the cloned toolchain (Ctrl-C to abort):': '',
56
+ },
57
+ timeout: 30000
58
+ },
59
+ assertionFunc: (output) => {
60
+ expect(output).to.match(/Warning! The following tools contain secrets that cannot be migrated/);
61
+ expect(output).to.match(/cloudobjectstorage[\s\S]*?cos_api_key/);
62
+ expect(output).to.match(/slack[\s\S]*?api_token/);
63
+ expect(output).to.match(/pipeline[\s\S]*?properties.doi-ibmcloud-api-key/);
64
+ }
65
+ },
66
+ {
67
+ name: 'Classic pipelines are identified',
68
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['misconfigured'].crn, '-r', TARGET_REGIONS[10]],
69
+ expected: /Warning! Classic pipelines are currently not supported in migration/,
70
+ options: {
71
+ exitCondition: 'Caution: The above tool(s) will not be properly configured post migration. Do you want to proceed?',
72
+ questionAnswerMap: {
73
+ '(Recommended) Add a tag to the cloned toolchain (Ctrl-C to abort):': '',
74
+ },
75
+ timeout: 30000
76
+ }
77
+ },
78
+ {
79
+ name: 'Git tools using PAT are identified',
80
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['misconfigured'].crn, '-r', TARGET_REGIONS[10]],
81
+ expected: null,
82
+ options: {
83
+ exitCondition: 'Caution: The above tool(s) will not be properly configured post migration. Do you want to proceed?',
84
+ questionAnswerMap: {
85
+ '(Recommended) Add a tag to the cloned toolchain (Ctrl-C to abort):': '',
86
+ },
87
+ timeout: 30000
88
+ },
89
+ assertionFunc: (output) => {
90
+ expect(output).to.match(/Warning! The following GRIT integration\(s\) are using auth_type "pat", please switch to auth_type "oauth" before proceeding/);
91
+ expect(output).to.match(/hostedgit/);
92
+ expect(output).to.match(/Warning! The following tools contain secrets that cannot be migrated/);
93
+ expect(output).to.match(/githubconsolidated[\s\S]*?api_token/);
94
+ expect(output).to.match(/github_integrated[\s\S]*?api_token/);
95
+ expect(output).to.match(/gitlab[\s\S]*?api_token/);
96
+ }
97
+ },
98
+ ];
99
+
100
+ for (const { name, cmd, expected, options, assertionFunc } of testCases) {
101
+ if (VERBOSE_MODE) cmd.push('-v');
102
+ it(`${name}`, async () => {
103
+ const res = await assertPtyOutput(cmd, expected, options, assertionFunc);
104
+ if (res) toolchainsToDelete.set(res.toolchainId, res.region);
105
+ });
106
+ }
107
+ });
@@ -19,11 +19,31 @@ const invalidRgId = 'invalid#RgId';
19
19
 
20
20
  const invalidRgName = 'invalid#Rg@Name';
21
21
 
22
+ const invalidGritMapping = {
23
+ 'ca-tor.git.cloud.ibm.com/fake-user/fake-repo': 'eu-gb.git.cloud.ibm.com/fake-user/fake-repo',
24
+ 'ibm.com/fake-user/fake-repo': 'ibm.com/fake-user/fake-repo'
25
+ };
26
+
27
+ const invalidGritFileName = 'invalid-mapping.json';
28
+
29
+ const getCdInstanceByRegionResponses = {
30
+ 'NOT_FOUND': {
31
+ status: 200,
32
+ data: {
33
+ items: [],
34
+ limit: 10
35
+ }
36
+ }
37
+ };
38
+
22
39
  export default {
23
40
  invalidCrn,
24
41
  invalidRegion,
25
42
  invalidTcName,
26
43
  invalidTag,
27
44
  invalidRgId,
28
- invalidRgName
45
+ invalidRgName,
46
+ invalidGritMapping,
47
+ invalidGritFileName,
48
+ getCdInstanceByRegionResponses
29
49
  };
@@ -9,13 +9,51 @@
9
9
 
10
10
  export const TEST_TOOLCHAINS = {
11
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'
12
+ name: 'KEEP-EMPTY-TOOLCHAIN',
13
+ crn: 'crn:v1:bluemix:public:toolchain:ca-tor:a/9e8559fac61ee9fc74d3e595fa75d147:0100aa9f-1e57-41d8-b4c7-5d84178d59bb::',
14
+ region: 'ca-tor',
15
15
  },
16
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'
17
+ name: 'KEEP-MISCONFIGURED-TOOLCHAIN',
18
+ crn: 'crn:v1:bluemix:public:toolchain:eu-es:a/9e8559fac61ee9fc74d3e595fa75d147:0ccfaa70-ca90-47db-8246-f4ecfc6ad8f3::',
19
+ region: 'eu-es'
20
+ },
21
+ '1pl-ghe-cc': {
22
+ name: 'KEEP-1PL-GHE-CC',
23
+ crn: 'crn:v1:bluemix:public:toolchain:eu-es:a/9e8559fac61ee9fc74d3e595fa75d147:9d51bb6b-f659-4ab7-9bc4-2eae1d61f4e7::',
24
+ region: 'eu-es'
25
+ },
26
+ '1pl-ghe-cd': {
27
+ name: 'KEEP-1PL-GHE-CD',
28
+ crn: 'crn:v1:bluemix:public:toolchain:eu-es:a/9e8559fac61ee9fc74d3e595fa75d147:6a70313f-a927-4b0e-8471-70f17330998d::',
29
+ region: 'eu-es'
30
+ },
31
+ '1pl-ghe-ci': {
32
+ name: 'KEEP-1PL-GHE-CI',
33
+ crn: 'crn:v1:bluemix:public:toolchain:eu-es:a/9e8559fac61ee9fc74d3e595fa75d147:6b8e27ae-5924-4a38-8819-f405366cb900::',
34
+ region: 'eu-es'
35
+ },
36
+ 'devsecops-grit-cc': {
37
+ name: 'KEEP-DevSecOps-GRIT-CC',
38
+ crn: 'crn:v1:bluemix:public:toolchain:eu-es:a/9e8559fac61ee9fc74d3e595fa75d147:920f6a94-4c1b-412b-b95c-baf823958744::',
39
+ region: 'eu-es'
40
+ },
41
+ 'devsecops-grit-cd': {
42
+ name: 'KEEP-DevSecOps-GRIT-CD',
43
+ crn: 'crn:v1:bluemix:public:toolchain:eu-es:a/9e8559fac61ee9fc74d3e595fa75d147:8618565f-08fa-4cac-9250-029cac7b41ba::',
44
+ region: 'eu-es'
45
+ },
46
+ 'devsecops-grit-ci': {
47
+ name: 'KEEP-DevSecOps-GRIT-CI',
48
+ crn: 'crn:v1:bluemix:public:toolchain:eu-es:a/9e8559fac61ee9fc74d3e595fa75d147:cdc271bc-cc07-4a85-beb2-895e033319b0::',
49
+ region: 'eu-es'
50
+ },
51
+ 'single-pl': {
52
+ name: 'KEEP-SINGLE-PIPELINE-TOOLCHAIN',
53
+ crn: 'crn:v1:bluemix:public:toolchain:us-east:a/9e8559fac61ee9fc74d3e595fa75d147:5ef88780-1e0f-4cda-94c7-f78909cc1140::',
54
+ region: 'us-east'
20
55
  }
21
56
  };
57
+
58
+ export const DEFAULT_RG_ID = '63b47433992f4295bc490852cbf1cb55';
59
+ export const R2R_CLI_RG_ID = 'f64e5eb8cfee406a983803bd79aa6c93';
package/test/setup.js CHANGED
@@ -23,10 +23,23 @@ const TEMP_DIR = resolve(nconf.get('TEST_TEMP_DIR'));
23
23
  const LOG_DIR = resolve(nconf.get('TEST_LOG_DIR'));
24
24
  const DEBUG_MODE = nconf.get('TEST_DEBUG_MODE');
25
25
 
26
+ function sleep(ms) {
27
+ return new Promise((resolve) => {
28
+ setTimeout(resolve, ms);
29
+ });
30
+ }
31
+
26
32
  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 });
33
+ async beforeAll() {
34
+ for (let i = 0; i < 3; i++) {
35
+ try {
36
+ fs.rmSync(TEMP_DIR, { recursive: true, force: true });
37
+ fs.rmSync(LOG_DIR, { recursive: true, force: true });
38
+ } catch {}
39
+ if (!fs.existsSync(TEMP_DIR) && !fs.existsSync(LOG_DIR)) break;
40
+ await sleep(1000);
41
+ }
42
+ fs.mkdirSync(TEMP_DIR, { recursive: true });
30
43
  },
31
44
  beforeEach() {
32
45
  if (DEBUG_MODE === true && LOG_DIR) {
@@ -35,10 +48,14 @@ export const mochaHooks = {
35
48
  resolve(LOG_DIR, this.currentTest.parent.command, testTitle + '.log') :
36
49
  resolve(LOG_DIR, testTitle + '.log');
37
50
  logger.createLogStream(logFile);
51
+ // Adding logging for log stream creation and closing, because there's cases of missing test log files when running tests in parallel, most likely because of the singleton logger,
52
+ // causing some sort of race condition happening
53
+ // console.info(`Created test log stream for test case '${this.currentTest.title}'`);
38
54
  }
39
55
  },
40
- afterEach() {
41
- logger.close();
56
+ async afterEach() {
57
+ await logger.close();
58
+ // console.info(`Closed test log stream for test case '${this.currentTest.title}'`);
42
59
  },
43
60
  afterAll() {
44
61
  if (fs.existsSync(TEMP_DIR) && DEBUG_MODE === false) fs.rmSync(TEMP_DIR, { recursive: true });
@@ -8,11 +8,14 @@
8
8
  */
9
9
 
10
10
  import { promisify } from 'util';
11
+ import fs from 'fs';
12
+ import path from 'path';
11
13
  import child_process from 'child_process';
12
14
  import stripAnsi from 'strip-ansi';
13
15
  import pty from 'node-pty';
16
+ import { parse as tfToJson } from '@cdktf/hcl2json'
14
17
  import nconf from 'nconf';
15
- import { expect } from 'chai';
18
+ import { expect, assert } from 'chai';
16
19
 
17
20
  import { getBearerToken, deleteToolchain } from '../../cmd/utils/requests.js';
18
21
  import { logger } from '../../cmd/utils/logger.js';
@@ -20,15 +23,53 @@ import { logger } from '../../cmd/utils/logger.js';
20
23
  nconf.env('__');
21
24
  nconf.file('local', 'test/config/local.json');
22
25
 
26
+ const TEMP_DIR = nconf.get('TEST_TEMP_DIR');
23
27
  const IBMCLOUD_API_KEY = nconf.get('IBMCLOUD_API_KEY');
24
28
 
25
29
  function cleanOutput(data) {
26
30
  if (typeof data === 'string') return stripAnsi(data).replace(/\r/g, '').trim();
27
31
  }
28
32
 
33
+ function searchDirectory(currentPath) {
34
+ const foundFiles = [];
35
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true });
36
+ for (const entry of entries) {
37
+ const fullPath = path.join(currentPath, entry.name);
38
+ if (entry.isDirectory()) {
39
+ foundFiles.push(...searchDirectory(fullPath));
40
+ } else {
41
+ foundFiles.push(path.join(currentPath, entry.name));
42
+ }
43
+ }
44
+ return foundFiles;
45
+ }
46
+
47
+ export function parseTcIdAndRegion(output) {
48
+ const pattern = /See cloned toolchain: https:\/\/cloud\.ibm\.com\/devops\/toolchains\/([a-zA-Z0-9-]+)\?env_id=ibm\:yp\:([a-zA-Z0-9-]+)/;
49
+ const match = output.match(pattern);
50
+
51
+ if (match) {
52
+ const toolchainId = match[1];
53
+ const region = match[2];
54
+ return { toolchainId, region };
55
+ } else {
56
+ return null;
57
+ }
58
+ }
59
+
29
60
  export async function execCommand(fullCommand, options) {
30
61
  const commandStr = `node ${fullCommand.join(' ')}`;
31
62
  const execPromise = promisify(child_process.exec);
63
+
64
+ if (!options) {
65
+ options = { cwd: TEMP_DIR }
66
+ } else {
67
+ options.cwd ??= TEMP_DIR;
68
+ if (!fs.existsSync(options.cwd)) {
69
+ fs.mkdirSync(options.cwd, { recursive: true });
70
+ }
71
+ }
72
+
32
73
  try {
33
74
  const { stdout, stderr } = await execPromise(commandStr, options);
34
75
  if (stderr) {
@@ -51,12 +92,14 @@ export async function execCommand(fullCommand, options) {
51
92
  export function runPtyProcess(fullCommand, options) {
52
93
  const {
53
94
  timeout = 0,
54
- cwd = process.cwd(),
95
+ cwd = TEMP_DIR,
55
96
  env = process.env,
56
97
  questionAnswerMap = {},
57
98
  exitCondition = '',
58
99
  } = options;
59
100
 
101
+ if (!fs.existsSync(cwd)) fs.mkdirSync(cwd, { recursive: true });
102
+
60
103
  return new Promise((resolve, reject) => {
61
104
  try {
62
105
  const ptyProcess = pty.spawn('node', fullCommand, {
@@ -106,7 +149,7 @@ export function runPtyProcess(fullCommand, options) {
106
149
  });
107
150
  }
108
151
 
109
- export async function testSuiteCleanup(toolchainsToDelete) {
152
+ export async function deleteCreatedToolchains(toolchainsToDelete) {
110
153
  if (toolchainsToDelete && typeof toolchainsToDelete === 'object' && toolchainsToDelete.size > 0) {
111
154
  const token = await getBearerToken(IBMCLOUD_API_KEY);
112
155
  const deletePromises = [...toolchainsToDelete.entries()].map(([id, region]) => deleteToolchain(token, id, region));
@@ -114,24 +157,83 @@ export async function testSuiteCleanup(toolchainsToDelete) {
114
157
  }
115
158
  }
116
159
 
117
- export async function expectExecError(fullCommand, expectedMessage, options) {
160
+ export async function assertExecError(fullCommand, expectedMessage, options, assertionFn) {
118
161
  try {
119
162
  const output = await execCommand(fullCommand, options);
120
163
  logger.dump(output);
121
164
  throw new Error('Expected command to fail but it succeeded');
122
165
  } catch (e) {
123
166
  logger.dump(e.message);
124
- expect(e.message).to.match(expectedMessage);
167
+ if (assertionFn) {
168
+ const res = assertionFn(e.message);
169
+ if (res instanceof Promise) await res;
170
+ } else if (expectedMessage) {
171
+ expect(e.message).to.match(expectedMessage);
172
+ } else {
173
+ assert.fail('No assertion function or expected message provided.');
174
+ }
125
175
  }
126
176
  }
127
177
 
128
- export async function expectPtyOutputToMatch(fullCommand, expectedMessage, options) {
178
+ export async function assertPtyOutput(fullCommand, expectedMessage, options, assertionFn) {
129
179
  try {
130
180
  const output = await runPtyProcess(fullCommand, options);
131
181
  logger.dump(output);
132
- expect(output).to.match(expectedMessage);
182
+ if (assertionFn) {
183
+ const res = assertionFn(output);
184
+ if (res instanceof Promise) await res;
185
+ } else if (expectedMessage) {
186
+ expect(output).to.match(expectedMessage);
187
+ } else {
188
+ assert.fail('No assertion function or expected message provided.');
189
+ }
190
+ return parseTcIdAndRegion(output);
133
191
  } catch (e) {
134
192
  logger.dump(e.message);
135
193
  throw (e);
136
194
  }
137
195
  }
196
+
197
+ export function areFilesInDir(dirPath, filePatterns) {
198
+ const foundFiles = searchDirectory(dirPath);
199
+ for (const pattern of filePatterns) {
200
+ const regex = new RegExp(pattern);
201
+ if (!foundFiles.some(file => regex.test(file))) {
202
+ return false;
203
+ }
204
+ }
205
+ return true;
206
+ }
207
+
208
+ export async function assertTfResourcesInDir(dirPath, expectedResourcesMap) {
209
+ const resourceCounter = {};
210
+
211
+ const foundFiles = searchDirectory(dirPath);
212
+ const allResources = [];
213
+ for (const file of foundFiles) {
214
+ if (!file.endsWith('.tf')) continue;
215
+ const fileName = path.basename(file);
216
+ const tfFile = fs.readFileSync(file, 'utf8');
217
+ const tfFileObject = await tfToJson(fileName, tfFile);
218
+ if (tfFileObject.resource) allResources.push(tfFileObject.resource);
219
+ }
220
+
221
+ for (const resourceMap of allResources) {
222
+ for (const resourceType of Object.keys(resourceMap)) {
223
+ resourceCounter[resourceType] = (resourceCounter[resourceType] || 0) + 1;
224
+ }
225
+ }
226
+ // Check if all expected resources are present
227
+ for (const [resourceType, expectedCount] of Object.entries(expectedResourcesMap)) {
228
+ if (resourceCounter[resourceType] !== expectedCount) {
229
+ assert.fail(`Expected ${expectedCount} ${resourceType} resource(s) but found ${resourceCounter[resourceType] || 0}`);
230
+ }
231
+ }
232
+ // Check if there are unexpected resources
233
+ for (const [resourceType, count] of Object.entries(resourceCounter)) {
234
+ if (!(resourceType in expectedResourcesMap)) {
235
+ assert.fail(`Unexpected ${resourceType} resource found. (Count: ${count})`);
236
+ }
237
+ }
238
+ assert.ok(true, 'Directory contains all expected resources');
239
+ }
@@ -1,11 +0,0 @@
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
- });
@@ -1,11 +0,0 @@
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
- });