@docupace/pipeline 1.0.67 → 1.0.68

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.
@@ -1,3 +1,4 @@
1
+ # bitbucket-pipelines.yml
1
2
  clone:
2
3
  depth: 2
3
4
 
@@ -6,23 +7,54 @@ pipelines:
6
7
  '**':
7
8
  - step:
8
9
  runs-on:
10
+ - self.hosted
9
11
  - linux.shell
10
- - onprem
11
12
  name: On Pull Request
12
13
  script:
14
+ - npm install -g pnpm
13
15
  - pnpm config set //registry.npmjs.org/:_authToken $DOCUPACE_NPM_TOKEN
14
16
  - pnpm install --frozen-lockfile
15
- - >
16
- npx eslint . --rule "no-unused-vars: warn" --rule "no-useless-escape: warn" --color
17
+ - 'npx eslint . --rule "no-unused-vars: warn" --rule "no-useless-escape: warn" --color'
18
+ - pnpm test
19
+ artifacts:
20
+ - coverage/**
21
+ after-script:
22
+ - echo "Pipeline completed with status $BITBUCKET_EXIT_CODE"
23
+
17
24
  custom:
18
25
  scheduled-tests:
19
26
  - step:
20
27
  runs-on:
28
+ - self.hosted
21
29
  - linux.shell
22
- - onprem
23
- name: Manual
30
+ name: Scheduled Tests
24
31
  script:
32
+ - npm install -g pnpm
25
33
  - pnpm config set //registry.npmjs.org/:_authToken $DOCUPACE_NPM_TOKEN
26
34
  - pnpm install --frozen-lockfile
27
- - >
28
- npx eslint . --rule "no-unused-vars: warn" --rule "no-useless-escape: warn" --color
35
+ - 'npx eslint . --rule "no-unused-vars: warn" --rule "no-useless-escape: warn" --color'
36
+ - pnpm test
37
+ artifacts:
38
+ - coverage/**
39
+ after-script:
40
+ - echo "Pipeline completed with status $BITBUCKET_EXIT_CODE"
41
+
42
+ branches:
43
+ main:
44
+ - step:
45
+ runs-on:
46
+ - self.hosted
47
+ - linux.shell
48
+ name: Main Branch Build
49
+ script:
50
+ - npm install -g pnpm
51
+ - pnpm config set //registry.npmjs.org/:_authToken $DOCUPACE_NPM_TOKEN
52
+ - pnpm install --frozen-lockfile
53
+ - 'npx eslint . --rule "no-unused-vars: warn" --rule "no-useless-escape: warn" --color'
54
+ - pnpm test
55
+ artifacts:
56
+ - coverage/**
57
+
58
+ definitions:
59
+ caches:
60
+ pnpm: ~/.pnpm-store
package/eslint.config.js CHANGED
@@ -4,17 +4,29 @@ import prettierConfig from 'eslint-config-prettier';
4
4
  import prettierPlugin from 'eslint-plugin-prettier';
5
5
  import importPlugin from 'eslint-plugin-import';
6
6
 
7
- /** @type {import('eslint').Linter.Config[]} */
8
7
  export default [
9
8
  {
10
9
  languageOptions: {
11
10
  globals: {
12
11
  ...globals.browser,
13
- ...globals.node, // Add Node.js globals
12
+ ...globals.node,
14
13
  },
15
14
  },
16
15
  },
16
+
17
+ // Core JS rules
17
18
  pluginJs.configs.recommended,
19
+
20
+ // Jest globals for test files only
21
+ {
22
+ files: ['**/*.test.js', '**/__tests__/**/*.js'],
23
+ languageOptions: {
24
+ globals: {
25
+ ...globals.jest,
26
+ },
27
+ },
28
+ },
29
+
18
30
  {
19
31
  plugins: {
20
32
  prettier: prettierPlugin,
@@ -29,7 +41,6 @@ export default [
29
41
  rules: {
30
42
  ...prettierConfig.rules,
31
43
  'prettier/prettier': 'error',
32
- // Import-related rules
33
44
  'import/no-unresolved': 'error',
34
45
  'import/named': 'error',
35
46
  'import/no-extraneous-dependencies': 'error',
package/jest.config.js ADDED
@@ -0,0 +1,15 @@
1
+ // jest.config.js
2
+ export default {
3
+ testEnvironment: 'node',
4
+ transform: {},
5
+ verbose: true,
6
+ collectCoverage: true,
7
+ coverageDirectory: 'coverage',
8
+ coverageReporters: ['text', 'lcov', 'json', 'html'],
9
+ collectCoverageFrom: [
10
+ 'src/**/*.{js,ts}',
11
+ '!src/**/cli.js',
12
+ '!src/**/*.test.{js,ts}',
13
+ '!**/node_modules/**',
14
+ ],
15
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docupace/pipeline",
3
- "version": "1.0.67",
3
+ "version": "1.0.68",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "@changesets/cli": "^2.27.10",
19
19
  "@docupace/core-client": "^0.1.94",
20
20
  "@docupace/pipeline": "^1.0.33-alpha-2",
21
+ "@jest/globals": "^30.1.2",
21
22
  "auth0-deploy-cli": "^8.8.0",
22
23
  "axios": "^1.7.9",
23
24
  "commander": "^12.1.0",
@@ -41,9 +42,11 @@
41
42
  "eslint-import-resolver-typescript": "^4.4.4",
42
43
  "eslint-plugin-prettier": "^5.2.1",
43
44
  "globals": "^15.14.0",
45
+ "jest": "^30.1.3",
44
46
  "prettier": "^3.4.2"
45
47
  },
46
48
  "scripts": {
49
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --testPathIgnorePatterns='src/next/test\\.js'",
47
50
  "test-deploy-core": "dp-next-cli deploy-core-pipeline dev template",
48
51
  "test-deploy-from-site": "dp-next-cli deploy-from-site --from-env dev --to-env dev --from-site financialdev --to-site template --deploy-core true",
49
52
  "test-run-pipline": "dp-next-cli run-pipeline -r docupace-next-e2e -b master -p run-tests-qa",
@@ -1,13 +1,8 @@
1
1
  import axios from 'axios';
2
-
3
2
  import { createAlias } from '../next/alias.js';
4
-
5
3
  import { getAuth0AuthToken } from './configure.js';
6
-
7
4
  import { getVaultSecrets } from './vault.js';
8
-
9
5
  import { getVaultSecretsConfigs } from '../next/config.js';
10
-
11
6
  import { cloneEnvRepository, readDeployConfig } from '../next/deploy.js';
12
7
 
13
8
  export async function getCCAlias({ application, auth0Domain, auth0AuthToken }) {
@@ -35,7 +30,14 @@ export async function getCCAlias({ application, auth0Domain, auth0AuthToken }) {
35
30
  }
36
31
  }
37
32
 
38
- export async function configureAlias(site, env, username, password, realm) {
33
+ export async function configureAlias(
34
+ site,
35
+ env,
36
+ username,
37
+ password,
38
+ realm,
39
+ auth0Tenant,
40
+ ) {
39
41
  console.log(`Creating alias for site: ${site} in env: ${env}`);
40
42
  // Site Common Secrets
41
43
  const siteCommonSecretsPath =
@@ -43,7 +45,12 @@ export async function configureAlias(site, env, username, password, realm) {
43
45
  console.log(`siteCommonSecretsPath => ${siteCommonSecretsPath}`);
44
46
  const siteCommonSecrets = await getVaultSecrets(siteCommonSecretsPath);
45
47
 
46
- const auth0Domain = `docupace-${env}-${site}.us.auth0.com`;
48
+ let auth0Domain;
49
+ if (auth0Tenant === '') {
50
+ auth0Domain = `docupace-${env}-${site}.us.auth0.com`;
51
+ } else {
52
+ auth0Domain = `${auth0Tenant}.us.auth0.com`;
53
+ }
47
54
  const auth0ClientId = siteCommonSecrets.AUTH0_CLIENT_ID;
48
55
  const auth0ClientSecret = siteCommonSecrets.AUTH0_CLIENT_SECRET;
49
56
  const auth0Token = await getAuth0AuthToken(
@@ -0,0 +1,172 @@
1
+ // alias.test.js
2
+ import { jest } from '@jest/globals';
3
+
4
+ // --------- ESM-Compatible Mocks ---------
5
+ jest.unstable_mockModule('axios', () => ({
6
+ default: {
7
+ get: jest.fn(),
8
+ },
9
+ }));
10
+
11
+ jest.unstable_mockModule('../next/alias.js', () => ({
12
+ createAlias: jest.fn(),
13
+ }));
14
+
15
+ jest.unstable_mockModule('./configure.js', () => ({
16
+ getAuth0AuthToken: jest.fn(),
17
+ }));
18
+
19
+ jest.unstable_mockModule('./vault.js', () => ({
20
+ getVaultSecrets: jest.fn(),
21
+ }));
22
+
23
+ jest.unstable_mockModule('../next/config.js', () => ({
24
+ getVaultSecretsConfigs: jest.fn(),
25
+ }));
26
+
27
+ jest.unstable_mockModule('../next/deploy.js', () => ({
28
+ cloneEnvRepository: jest.fn(),
29
+ readDeployConfig: jest.fn(),
30
+ }));
31
+
32
+ // --------- Import AFTER mocks ---------
33
+ const { configureAlias, getCCAlias } = await import('./alias.js');
34
+ const axios = (await import('axios')).default;
35
+ const { createAlias } = await import('../next/alias.js');
36
+ const { getAuth0AuthToken } = await import('./configure.js');
37
+ const { getVaultSecrets } = await import('./vault.js');
38
+ const { getVaultSecretsConfigs } = await import('../next/config.js');
39
+ const { cloneEnvRepository, readDeployConfig } = await import(
40
+ '../next/deploy.js'
41
+ );
42
+
43
+ // --------- Shared Test Setup ---------
44
+ beforeAll(() => {
45
+ jest.spyOn(console, 'log').mockImplementation(() => {}); // silence logs
46
+ });
47
+
48
+ afterAll(() => {
49
+ console.log.mockRestore();
50
+ });
51
+
52
+ describe('configureAlias', () => {
53
+ const site = 'example';
54
+ const env = 'dev';
55
+ const username = 'admin';
56
+ const password = 'pass';
57
+ const realm = 'Users';
58
+ const auth0Tenant = '';
59
+
60
+ const mockSecrets = {
61
+ AUTH0_CLIENT_ID: 'client-id',
62
+ AUTH0_CLIENT_SECRET: 'client-secret',
63
+ };
64
+
65
+ const mockToken = 'mocked-auth0-token';
66
+
67
+ const mockClients = [
68
+ { name: 'docupace-graphql-client-credentials', client_id: 'client-123' },
69
+ ];
70
+
71
+ const mockConfig = {
72
+ deployConfig: {
73
+ graphqlServer: {
74
+ coreUrl: 'https://core.example.com',
75
+ },
76
+ },
77
+ };
78
+
79
+ beforeEach(() => {
80
+ jest.clearAllMocks();
81
+
82
+ getVaultSecretsConfigs.mockReturnValue({
83
+ siteCommonSecretsPath: 'vault/path/to/secrets',
84
+ });
85
+
86
+ getVaultSecrets.mockResolvedValue(mockSecrets);
87
+ getAuth0AuthToken.mockResolvedValue(mockToken);
88
+ axios.get.mockResolvedValue({ data: mockClients });
89
+ cloneEnvRepository.mockResolvedValue(undefined);
90
+ readDeployConfig.mockResolvedValue(mockConfig);
91
+ createAlias.mockResolvedValue(undefined);
92
+ });
93
+
94
+ it('should configure alias with default tenant and call all required steps', async () => {
95
+ await configureAlias(site, env, username, password, realm, auth0Tenant);
96
+
97
+ expect(getVaultSecretsConfigs).toHaveBeenCalledWith(site);
98
+ expect(getVaultSecrets).toHaveBeenCalledWith('vault/path/to/secrets');
99
+
100
+ expect(getAuth0AuthToken).toHaveBeenCalledWith(
101
+ 'docupace-dev-example.us.auth0.com',
102
+ 'client-id',
103
+ 'client-secret',
104
+ );
105
+
106
+ expect(cloneEnvRepository).toHaveBeenCalledWith(env);
107
+
108
+ expect(readDeployConfig).toHaveBeenCalledWith(
109
+ 'graphql-server',
110
+ env,
111
+ site,
112
+ 'values',
113
+ );
114
+
115
+ expect(axios.get).toHaveBeenCalledWith(
116
+ 'https://docupace-dev-example.us.auth0.com/api/v2/clients',
117
+ { headers: { authorization: `Bearer ${mockToken}` } },
118
+ );
119
+
120
+ expect(createAlias).toHaveBeenCalledWith(
121
+ 'https://core.example.com',
122
+ username,
123
+ password,
124
+ realm,
125
+ 'client-123@clients',
126
+ );
127
+ });
128
+ });
129
+
130
+ describe('getCCAlias', () => {
131
+ const application = 'docupace-graphql-client-credentials';
132
+ const auth0Domain = 'example.auth0.com';
133
+ const auth0AuthToken = 'token';
134
+
135
+ it('should return correct alias string for client', async () => {
136
+ axios.get.mockResolvedValue({
137
+ data: [{ name: application, client_id: 'abc123' }],
138
+ });
139
+
140
+ const alias = await getCCAlias({
141
+ application,
142
+ auth0Domain,
143
+ auth0AuthToken,
144
+ });
145
+ expect(alias).toBe('abc123@clients');
146
+
147
+ expect(axios.get).toHaveBeenCalledWith(
148
+ 'https://example.auth0.com/api/v2/clients',
149
+ { headers: { authorization: 'Bearer token' } },
150
+ );
151
+ });
152
+
153
+ it('should log error and return undefined if axios fails', async () => {
154
+ axios.get.mockRejectedValue(new Error('Network error'));
155
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
156
+
157
+ const result = await getCCAlias({
158
+ application,
159
+ auth0Domain,
160
+ auth0AuthToken,
161
+ });
162
+ expect(result).toBeUndefined();
163
+
164
+ expect(errorSpy).toHaveBeenCalledWith(
165
+ expect.stringContaining(
166
+ 'Failed to form alias name for application: docupace-graphql-client-credentials: Network error',
167
+ ),
168
+ );
169
+
170
+ errorSpy.mockRestore();
171
+ });
172
+ });
package/src/auth0/cli.js CHANGED
@@ -57,24 +57,26 @@ program
57
57
  });
58
58
 
59
59
  program
60
- .command('configureAlias <site> <env> <username> <password> <realm>')
60
+ .command(
61
+ 'configureAlias <site> <env> <username> <password> <realm> [auth0Tenant]',
62
+ )
61
63
  .description('Configure Core Alias for the given site')
62
- .action(async (site, env, username, password, realm) => {
63
- await configureAlias(site, env, username, password, realm);
64
+ .action(async (site, env, username, password, realm, auth0Tenant = '') => {
65
+ await configureAlias(site, env, username, password, realm, auth0Tenant);
64
66
  });
65
67
 
66
68
  program
67
- .command('exportConf <site> <env>')
69
+ .command('exportConf <site> <env> [auth0Tenant]')
68
70
  .description('Export Auth0 configuration for the given site')
69
- .action(async (site, env) => {
70
- await exportConf(site, env);
71
+ .action(async (site, env, auth0Tenant = '') => {
72
+ await exportConf(site, env, auth0Tenant);
71
73
  });
72
74
 
73
75
  program
74
- .command('getAuth0Metadata <site> <env>')
76
+ .command('getAuth0Metadata <site> <env> [auth0Tenant]')
75
77
  .description('Get Auth0 metadata for the given site')
76
- .action(async (site, env) => {
77
- await getAuth0Metadata(site, env);
78
+ .action(async (site, env, auth0Tenant = '') => {
79
+ await getAuth0Metadata(site, env, auth0Tenant);
78
80
  });
79
81
 
80
82
  program.parse(process.argv);
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import axios from 'axios';
4
- import crypto from 'crypto';
4
+ import * as crypto from 'crypto';
5
5
 
6
6
  import {
7
7
  cloneEnvRepository,
@@ -11,6 +11,7 @@ import {
11
11
  } from '../next/deploy.js';
12
12
 
13
13
  import { getVaultSecrets, updateVaultSecrets } from './vault.js';
14
+
14
15
  import {
15
16
  coreSiteEnvMap,
16
17
  environmentConfig,
@@ -0,0 +1,167 @@
1
+ // @jest-environment node
2
+ import { jest } from '@jest/globals';
3
+
4
+ // ─── Mock All Required Modules ───────────────────────────────────────────────
5
+
6
+ jest.unstable_mockModule('fs-extra', () => ({
7
+ default: {
8
+ promises: {
9
+ mkdir: jest.fn(),
10
+ },
11
+ writeFile: jest.fn(),
12
+ ensureDir: jest.fn(),
13
+ },
14
+ writeFile: jest.fn(),
15
+ ensureDir: jest.fn(),
16
+ }));
17
+
18
+ jest.unstable_mockModule('axios', () => ({
19
+ default: {
20
+ post: jest.fn(),
21
+ get: jest.fn(),
22
+ },
23
+ }));
24
+
25
+ jest.unstable_mockModule('crypto', () => ({
26
+ randomBytes: () => ({
27
+ toString: () => 'mockBase64==',
28
+ }),
29
+ }));
30
+
31
+ jest.unstable_mockModule('../next/deploy.js', () => ({
32
+ cloneRepository: jest.fn(),
33
+ cloneEnvRepository: jest.fn(),
34
+ pushDeployConfig: jest.fn(),
35
+ readDeployConfig: jest.fn(),
36
+ }));
37
+
38
+ jest.unstable_mockModule('./vault.js', () => ({
39
+ getVaultSecrets: jest.fn(),
40
+ updateVaultSecrets: jest.fn(),
41
+ }));
42
+
43
+ jest.unstable_mockModule('../next/config.js', () => ({
44
+ coreSiteEnvMap: { dev: 'dev-env' },
45
+ environmentConfig: {
46
+ dev: {
47
+ newUrl: (site) => `dev.${site}.com`,
48
+ oldUrl: (site) => `legacy.${site}.com`,
49
+ ccUrl: (site) => `cc.${site}.com`,
50
+ docupaceHost: () => 'docupace.com',
51
+ },
52
+ },
53
+ getVaultSecretsConfigs: jest.fn(),
54
+ }));
55
+
56
+ // ─── Global References ───────────────────────────────────────────────────────
57
+
58
+ let fs, axios, deploy, vault, config;
59
+ let getAuth0AuthToken, cleanAndWritePropertiesFile, updateCoreSiteConfig;
60
+
61
+ const originalProcessExit = process.exit;
62
+
63
+ beforeAll(async () => {
64
+ // Mock process.exit to prevent killing the test runner
65
+ process.exit = jest.fn();
66
+
67
+ // Suppress noisy error output
68
+ jest.spyOn(console, 'error').mockImplementation(() => {});
69
+ jest.spyOn(console, 'log').mockImplementation(() => {});
70
+
71
+ // Import after mocks
72
+ ({ default: fs } = await import('fs-extra'));
73
+ ({ default: axios } = await import('axios'));
74
+ deploy = await import('../next/deploy.js');
75
+ vault = await import('./vault.js');
76
+ config = await import('../next/config.js');
77
+
78
+ const mod = await import('./configure.js');
79
+ ({ getAuth0AuthToken, cleanAndWritePropertiesFile, updateCoreSiteConfig } =
80
+ mod);
81
+ });
82
+
83
+ afterAll(() => {
84
+ // Restore
85
+ process.exit = originalProcessExit;
86
+ console.error.mockRestore();
87
+ console.log.mockRestore();
88
+ });
89
+
90
+ // ─── Tests ───────────────────────────────────────────────────────────────────
91
+
92
+ describe('Deployment Automation Module (ESM)', () => {
93
+ describe('getAuth0AuthToken', () => {
94
+ it('should return access token on success', async () => {
95
+ axios.post.mockResolvedValue({ data: { access_token: 'mock-token' } });
96
+ const token = await getAuth0AuthToken('auth.domain', 'cid', 'secret');
97
+ expect(token).toBe('mock-token');
98
+ });
99
+
100
+ it('should return undefined on failure', async () => {
101
+ axios.post.mockRejectedValue({ message: 'fail' });
102
+ const token = await getAuth0AuthToken('auth.domain', 'cid', 'secret');
103
+ expect(token).toBeUndefined();
104
+ expect(console.error).toHaveBeenCalledWith(
105
+ 'Error fetching Auth0 token:',
106
+ 'fail',
107
+ );
108
+ });
109
+ });
110
+
111
+ describe('cleanAndWritePropertiesFile', () => {
112
+ it('writes cleaned file content', async () => {
113
+ fs.writeFile.mockResolvedValue();
114
+ await cleanAndWritePropertiesFile('/tmp/test.properties', 'a=1\n\nb=2\n');
115
+ expect(fs.writeFile).toHaveBeenCalledWith(
116
+ '/tmp/test.properties',
117
+ 'a=1\nb=2',
118
+ 'utf8',
119
+ );
120
+ });
121
+
122
+ it('calls process.exit on write error', async () => {
123
+ fs.writeFile.mockRejectedValue(new Error('disk full'));
124
+ await cleanAndWritePropertiesFile('/bad/path', 'x=y');
125
+ expect(process.exit).toHaveBeenCalledWith(1);
126
+ });
127
+ });
128
+
129
+ describe('updateCoreSiteConfig', () => {
130
+ it('updates config when S3 is enabled', async () => {
131
+ config.getVaultSecretsConfigs.mockReturnValue({
132
+ awsSecretsPath: 'aws/path',
133
+ });
134
+
135
+ deploy.cloneRepository.mockResolvedValue('/repo/path');
136
+
137
+ vault.getVaultSecrets.mockResolvedValue({
138
+ AWS_ACCESS_KEY_ID: 'abc',
139
+ AWS_SECRET_ACCESS_KEY: 'def',
140
+ AWS_REGION: 'us-west-2',
141
+ });
142
+
143
+ fs.ensureDir.mockResolvedValue();
144
+ fs.writeFile.mockResolvedValue();
145
+ deploy.pushDeployConfig.mockResolvedValue();
146
+
147
+ await updateCoreSiteConfig('mysite', 'dev', true, false);
148
+
149
+ expect(fs.ensureDir).toHaveBeenCalledWith('/repo/path/MYSITE');
150
+ expect(fs.writeFile).toHaveBeenCalled();
151
+ expect(deploy.pushDeployConfig).toHaveBeenCalled();
152
+ });
153
+
154
+ it('calls process.exit if cloneRepository fails', async () => {
155
+ config.getVaultSecretsConfigs.mockReturnValue({
156
+ awsSecretsPath: 'aws/path',
157
+ });
158
+
159
+ deploy.cloneRepository.mockRejectedValue(new Error('bad clone'));
160
+
161
+ await updateCoreSiteConfig('mysite', 'dev', true, false);
162
+
163
+ expect(process.exit).toHaveBeenCalledWith(1);
164
+ expect(console.error).toHaveBeenCalledWith('Error:', 'bad clone');
165
+ });
166
+ });
167
+ });
@@ -2,7 +2,7 @@ import { getVaultSecrets } from './vault.js';
2
2
  import { getVaultSecretsConfigs } from '../next/config.js';
3
3
  import { dump } from 'auth0-deploy-cli';
4
4
 
5
- export async function exportConf(site, env) {
5
+ export async function exportConf(site, env, auth0Tenant) {
6
6
  try {
7
7
  console.log(
8
8
  `Exporting Auth0 configuration for site: ${site} and env: ${env}`,
@@ -13,7 +13,12 @@ export async function exportConf(site, env) {
13
13
  console.log(`siteCommonSecretsPath => ${siteCommonSecretsPath}`);
14
14
  const siteCommonSecrets = await getVaultSecrets(siteCommonSecretsPath);
15
15
 
16
- const auth0Domain = `docupace-${env}-${site}.us.auth0.com`;
16
+ let auth0Domain;
17
+ if (auth0Tenant === '') {
18
+ auth0Domain = `docupace-${env}-${site}.us.auth0.com`;
19
+ } else {
20
+ auth0Domain = `${auth0Tenant}.us.auth0.com`;
21
+ }
17
22
  const auth0ClientId = siteCommonSecrets.AUTH0_CLIENT_ID;
18
23
  const auth0ClientSecret = siteCommonSecrets.AUTH0_CLIENT_SECRET;
19
24
 
@@ -0,0 +1,103 @@
1
+ // exportConf.test.js
2
+ import { jest } from '@jest/globals';
3
+
4
+ // Mock modules using ESM-safe methods
5
+ jest.unstable_mockModule('./vault.js', () => ({
6
+ getVaultSecrets: jest.fn(),
7
+ }));
8
+
9
+ jest.unstable_mockModule('../next/config.js', () => ({
10
+ getVaultSecretsConfigs: jest.fn(),
11
+ }));
12
+
13
+ jest.unstable_mockModule('auth0-deploy-cli', () => ({
14
+ dump: jest.fn(),
15
+ }));
16
+
17
+ // Import the actual function and mocked dependencies after mocks are in place
18
+ const { exportConf } = await import('./export.js');
19
+ const { getVaultSecrets } = await import('./vault.js');
20
+ const { getVaultSecretsConfigs } = await import('../next/config.js');
21
+ const { dump } = await import('auth0-deploy-cli');
22
+
23
+ describe('exportConf', () => {
24
+ const site = 'example';
25
+ const env = 'dev';
26
+ const auth0Tenant = '';
27
+
28
+ const mockSecrets = {
29
+ AUTH0_CLIENT_ID: 'test-client-id',
30
+ AUTH0_CLIENT_SECRET: 'test-client-secret',
31
+ };
32
+
33
+ const mockPath = 'vault/path/to/secrets';
34
+
35
+ beforeEach(() => {
36
+ jest.clearAllMocks();
37
+
38
+ getVaultSecretsConfigs.mockReturnValue({
39
+ siteCommonSecretsPath: mockPath,
40
+ });
41
+
42
+ getVaultSecrets.mockResolvedValue(mockSecrets);
43
+
44
+ dump.mockResolvedValue(undefined);
45
+ });
46
+
47
+ it('should export Auth0 configuration with default tenant format', async () => {
48
+ const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
49
+ await exportConf(site, env, auth0Tenant);
50
+
51
+ expect(getVaultSecretsConfigs).toHaveBeenCalledWith(site);
52
+ expect(getVaultSecrets).toHaveBeenCalledWith(mockPath);
53
+ expect(dump).toHaveBeenCalledWith(
54
+ expect.objectContaining({
55
+ config: {
56
+ AUTH0_DOMAIN: 'docupace-dev-example.us.auth0.com',
57
+ AUTH0_CLIENT_ID: 'test-client-id',
58
+ AUTH0_CLIENT_SECRET: 'test-client-secret',
59
+ },
60
+ }),
61
+ );
62
+
63
+ logSpy.mockRestore();
64
+ });
65
+
66
+ it('should export Auth0 configuration with custom tenant', async () => {
67
+ const customTenant = 'custom-tenant';
68
+ await exportConf(site, env, customTenant);
69
+
70
+ expect(dump).toHaveBeenCalledWith(
71
+ expect.objectContaining({
72
+ config: {
73
+ AUTH0_DOMAIN: 'custom-tenant.us.auth0.com',
74
+ AUTH0_CLIENT_ID: 'test-client-id',
75
+ AUTH0_CLIENT_SECRET: 'test-client-secret',
76
+ },
77
+ }),
78
+ );
79
+ });
80
+
81
+ it('should log error and exit process on failure', async () => {
82
+ getVaultSecrets.mockRejectedValue(new Error('Vault error'));
83
+
84
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
85
+ const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
86
+ throw new Error('process.exit');
87
+ });
88
+
89
+ await expect(exportConf(site, env, auth0Tenant)).rejects.toThrow(
90
+ 'process.exit',
91
+ );
92
+
93
+ expect(errorSpy).toHaveBeenCalledWith(
94
+ expect.stringContaining(
95
+ 'Error during Auth0 example configuration export:',
96
+ ),
97
+ 'Vault error',
98
+ );
99
+
100
+ errorSpy.mockRestore();
101
+ exitSpy.mockRestore();
102
+ });
103
+ });
@@ -1,16 +1,15 @@
1
1
  import fs from 'fs-extra';
2
2
  import axios from 'axios';
3
- import path from 'path';
3
+ import * as path from 'node:path'; // ESM-compatible import
4
4
  import { cloneRepository, pushDeployConfig } from '../next/deploy.js';
5
5
  import { getAuth0AuthToken } from './configure.js';
6
6
  import { getVaultSecrets } from './vault.js';
7
7
  import { getVaultSecretsConfigs } from '../next/config.js';
8
8
 
9
- export async function getAuth0Metadata(site, env) {
9
+ export async function getAuth0Metadata(site, env, auth0Tenant) {
10
10
  try {
11
11
  const connectDeployRepoPath = await cloneRepository('connect-deploy');
12
12
 
13
- // mapping of env to env
14
13
  const envMap = {
15
14
  dev: 'dev',
16
15
  preprod: 'qa',
@@ -18,14 +17,15 @@ export async function getAuth0Metadata(site, env) {
18
17
  uat: 'uat',
19
18
  'prod-www3': 'prod',
20
19
  };
20
+
21
21
  process.chdir(`${connectDeployRepoPath}/envs/${env}/${site}`);
22
22
 
23
23
  const configPath = path.join('conf', 'application.conf');
24
24
  const appConf = await fs.readFile(configPath, 'utf-8');
25
+
25
26
  const connectContext = appConf.match(
26
27
  /application\.context\s*=\s*"(.*?)"/,
27
28
  )?.[1];
28
-
29
29
  if (!connectContext) {
30
30
  console.error('connectContext not found in application.conf. Exiting...');
31
31
  process.exit(1);
@@ -34,7 +34,6 @@ export async function getAuth0Metadata(site, env) {
34
34
  const siteHost = appConf.match(
35
35
  /docupace\.baseUrlClient\s*=\s*"?https?:\/\/([^/]+)"?/,
36
36
  )?.[1];
37
-
38
37
  if (!siteHost) {
39
38
  console.error('siteHost not found in application.conf. Exiting...');
40
39
  process.exit(1);
@@ -47,10 +46,17 @@ export async function getAuth0Metadata(site, env) {
47
46
  const siteCommonSecretsPath =
48
47
  getVaultSecretsConfigs(site).siteCommonSecretsPath;
49
48
  console.log(`siteCommonSecretsPath => ${siteCommonSecretsPath}`);
49
+
50
50
  const siteCommonSecrets = await getVaultSecrets(siteCommonSecretsPath);
51
- const auth0Domain = `docupace-${envMap[env]}-${site}.us.auth0.com`;
51
+
52
+ const auth0Domain =
53
+ auth0Tenant === ''
54
+ ? `docupace-${envMap[env]}-${site}.us.auth0.com`
55
+ : `${auth0Tenant}.us.auth0.com`;
56
+
52
57
  const auth0ClientId = siteCommonSecrets.AUTH0_CLIENT_ID;
53
58
  const auth0ClientSecret = siteCommonSecrets.AUTH0_CLIENT_SECRET;
59
+
54
60
  const auth0Token = await getAuth0AuthToken(
55
61
  auth0Domain,
56
62
  auth0ClientId,
@@ -78,9 +84,7 @@ export async function getAuth0Metadata(site, env) {
78
84
 
79
85
  const metadataRes = await axios.get(
80
86
  `https://${auth0Domain}/samlp/metadata/${clientId}`,
81
- {
82
- responseType: 'text',
83
- },
87
+ { responseType: 'text' },
84
88
  );
85
89
 
86
90
  const metadata = metadataRes.data;
@@ -91,6 +95,7 @@ export async function getAuth0Metadata(site, env) {
91
95
  );
92
96
  process.exit(1);
93
97
  }
98
+
94
99
  await fs.ensureDir('conf/saml');
95
100
  await fs.writeFile('conf/saml/auth0.xml', metadata);
96
101
  console.log('SAML metadata written to conf/saml/auth0.xml');
@@ -121,6 +126,7 @@ export async function getAuth0Metadata(site, env) {
121
126
 
122
127
  await fs.writeFile(configPath, newConfig);
123
128
  console.log('The application.conf has been updated.');
129
+
124
130
  await pushDeployConfig(
125
131
  connectDeployRepoPath,
126
132
  `Auth0 SAML metadata update for site: (${site}) in env: (${env})`,
@@ -0,0 +1,186 @@
1
+ import { jest } from '@jest/globals';
2
+
3
+ // ----- Mocks (ESM-safe) -----
4
+ jest.unstable_mockModule('fs-extra', () => ({
5
+ default: {
6
+ readFile: jest.fn(),
7
+ writeFile: jest.fn(),
8
+ ensureDir: jest.fn(),
9
+ },
10
+ }));
11
+
12
+ jest.unstable_mockModule('axios', () => ({
13
+ default: {
14
+ get: jest.fn(),
15
+ },
16
+ }));
17
+
18
+ jest.unstable_mockModule('path', () => ({
19
+ join: jest.fn((...args) => args.join('/')),
20
+ }));
21
+
22
+ jest.unstable_mockModule('../next/deploy.js', () => ({
23
+ cloneRepository: jest.fn(),
24
+ pushDeployConfig: jest.fn(),
25
+ }));
26
+
27
+ jest.unstable_mockModule('./configure.js', () => ({
28
+ getAuth0AuthToken: jest.fn(),
29
+ }));
30
+
31
+ jest.unstable_mockModule('./vault.js', () => ({
32
+ getVaultSecrets: jest.fn(),
33
+ }));
34
+
35
+ jest.unstable_mockModule('../next/config.js', () => ({
36
+ getVaultSecretsConfigs: jest.fn(),
37
+ }));
38
+
39
+ // ----- Imports AFTER mocks -----
40
+ const { getAuth0Metadata } = await import('./metadata.js');
41
+ const fs = (await import('fs-extra')).default;
42
+ const axios = (await import('axios')).default;
43
+ const { cloneRepository, pushDeployConfig } = await import('../next/deploy.js');
44
+ const { getAuth0AuthToken } = await import('./configure.js');
45
+ const { getVaultSecrets } = await import('./vault.js');
46
+ const { getVaultSecretsConfigs } = await import('../next/config.js');
47
+
48
+ // ----- Shared Setup -----
49
+ beforeAll(() => {
50
+ jest.spyOn(console, 'log').mockImplementation(() => {});
51
+ jest.spyOn(console, 'error').mockImplementation(() => {});
52
+ });
53
+
54
+ afterAll(() => {
55
+ console.log.mockRestore();
56
+ console.error.mockRestore();
57
+ });
58
+
59
+ describe('getAuth0Metadata', () => {
60
+ const site = 'example';
61
+ const env = 'dev';
62
+ const auth0Tenant = '';
63
+ const fakeRepoPath = '/tmp/repo';
64
+ const fakeAppConf = `
65
+ application.context = "/connect"
66
+ docupace.baseUrlClient = "https://example.docupace.com"
67
+ `;
68
+
69
+ const mockSecrets = {
70
+ AUTH0_CLIENT_ID: 'client-id',
71
+ AUTH0_CLIENT_SECRET: 'client-secret',
72
+ };
73
+
74
+ const auth0Token = 'fake-token';
75
+ const clientId = 'abc123';
76
+ const fakeMetadata = '<xml>'.padEnd(2500, 'a') + '</xml>';
77
+
78
+ let originalChdir;
79
+ let exitSpy;
80
+
81
+ beforeEach(() => {
82
+ jest.clearAllMocks();
83
+ axios.get.mockReset();
84
+
85
+ cloneRepository.mockResolvedValue(fakeRepoPath);
86
+ getVaultSecretsConfigs.mockReturnValue({
87
+ siteCommonSecretsPath: 'vault/path',
88
+ });
89
+ getVaultSecrets.mockResolvedValue(mockSecrets);
90
+ getAuth0AuthToken.mockResolvedValue(auth0Token);
91
+ fs.readFile.mockResolvedValue(fakeAppConf);
92
+ fs.writeFile.mockResolvedValue();
93
+ fs.ensureDir.mockResolvedValue();
94
+ pushDeployConfig.mockResolvedValue();
95
+
96
+ originalChdir = process.chdir;
97
+ process.chdir = jest.fn();
98
+
99
+ exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {});
100
+ });
101
+
102
+ afterEach(() => {
103
+ process.chdir = originalChdir;
104
+ exitSpy.mockRestore();
105
+ });
106
+
107
+ it('should download and write metadata, update config, and push changes', async () => {
108
+ axios.get
109
+ .mockResolvedValueOnce({
110
+ data: [{ name: 'docupace-connect', client_id: clientId }],
111
+ }) // clients
112
+ .mockResolvedValueOnce({ data: fakeMetadata }); // metadata
113
+
114
+ await getAuth0Metadata(site, env, auth0Tenant);
115
+
116
+ expect(cloneRepository).toHaveBeenCalledWith('connect-deploy');
117
+ expect(fs.readFile).toHaveBeenCalledWith('conf/application.conf', 'utf-8');
118
+ expect(fs.ensureDir).toHaveBeenCalledWith('conf/saml');
119
+ expect(fs.writeFile).toHaveBeenCalledWith(
120
+ 'conf/saml/auth0.xml',
121
+ expect.stringContaining('<xml'),
122
+ );
123
+ expect(fs.writeFile).toHaveBeenCalledWith(
124
+ 'conf/application.conf',
125
+ expect.stringContaining('useSAML=true'),
126
+ );
127
+ expect(pushDeployConfig).toHaveBeenCalled();
128
+ });
129
+
130
+ it('should exit if docupace-connect client is not found', async () => {
131
+ axios.get.mockResolvedValueOnce({ data: [] }); // empty clients
132
+
133
+ await getAuth0Metadata(site, env, auth0Tenant);
134
+
135
+ expect(process.exit).toHaveBeenCalledWith(1);
136
+ expect(console.error).toHaveBeenCalledWith(
137
+ 'Client "docupace-connect" not found.',
138
+ );
139
+ });
140
+
141
+ it('should exit if metadata is too small', async () => {
142
+ axios.get
143
+ .mockResolvedValueOnce({
144
+ data: [{ name: 'docupace-connect', client_id: clientId }],
145
+ }) // clients
146
+ .mockResolvedValueOnce({ data: '<small>' }); // small metadata
147
+
148
+ await getAuth0Metadata(site, env, auth0Tenant);
149
+
150
+ expect(process.exit).toHaveBeenCalledWith(1);
151
+ expect(console.error).toHaveBeenCalledWith(
152
+ 'Downloaded metadata looks incorrect (too small). Exiting...',
153
+ );
154
+ });
155
+
156
+ it('should exit if connectContext is missing', async () => {
157
+ fs.readFile.mockResolvedValue(`docupace.baseUrlClient = "https://abc.com"`); // no context
158
+
159
+ await getAuth0Metadata(site, env, auth0Tenant);
160
+
161
+ expect(process.exit).toHaveBeenCalledWith(1);
162
+ expect(console.error).toHaveBeenCalledWith(
163
+ 'connectContext not found in application.conf. Exiting...',
164
+ );
165
+ });
166
+
167
+ it('should exit if siteHost is missing', async () => {
168
+ fs.readFile.mockResolvedValue(`application.context = "/abc"`); // no host
169
+
170
+ await getAuth0Metadata(site, env, auth0Tenant);
171
+
172
+ expect(process.exit).toHaveBeenCalledWith(1);
173
+ expect(console.error).toHaveBeenCalledWith(
174
+ 'siteHost not found in application.conf. Exiting...',
175
+ );
176
+ });
177
+
178
+ it('should exit on unknown error', async () => {
179
+ getVaultSecrets.mockRejectedValue(new Error('boom'));
180
+
181
+ await getAuth0Metadata(site, env, auth0Tenant);
182
+
183
+ expect(process.exit).toHaveBeenCalledWith(1);
184
+ expect(console.error).toHaveBeenCalledWith('Error:', 'boom');
185
+ });
186
+ });
@@ -1,107 +1,60 @@
1
+ // src/auth0/vault.js
1
2
  import axios from 'axios';
2
3
 
3
- /**
4
- * Fetch secrets from Vault.
5
- * @param {string} vaultPath - The path in Vault to fetch secrets from.
6
- * @returns {Promise<Object>} - The secret data as a JSON object.
7
- */
8
4
  export async function getVaultSecrets(vaultPath) {
9
- if (!vaultPath) {
10
- console.error('Error: VAULT_PATH is required.');
11
- throw new Error('VAULT_PATH is required.');
12
- }
13
-
14
- const vaultToken = process.env.VAULT_TOKEN;
15
- const vaultAddr = process.env.VAULT_ADDR;
5
+ if (!vaultPath) throw new Error('VAULT_PATH is required.');
16
6
 
17
- if (!vaultToken || !vaultAddr) {
18
- console.error('Error: VAULT_TOKEN and VAULT_ADDR must be set.');
7
+ const token = process.env.VAULT_TOKEN;
8
+ const addr = process.env.VAULT_ADDR;
9
+ if (!token || !addr)
19
10
  throw new Error('VAULT_TOKEN and VAULT_ADDR must be set.');
11
+
12
+ if (process.env.VAULT_SKIP_VERIFY === 'true') {
13
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
20
14
  }
21
15
 
22
16
  try {
23
- // Disable SSL verification globally by setting environment variable
24
- if (process.env.VAULT_SKIP_VERIFY === 'true') {
25
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
26
- }
27
-
28
- // Fetch the secret from Vault
29
- const response = await axios.get(`${vaultAddr}/v1/${vaultPath}`, {
30
- headers: {
31
- 'X-Vault-Token': vaultToken,
32
- },
17
+ const response = await axios.get(`${addr}/v1/${vaultPath}`, {
18
+ headers: { 'X-Vault-Token': token },
33
19
  });
34
20
 
35
- // Validate the response contains JSON data
36
21
  const data = response.data;
37
- if (data && typeof data === 'object') {
38
- // console.log(`Secrets fetched successfully from Vault at ${JSON.stringify(data.data.data)})`);
39
- return data.data.data;
40
- } else {
41
- console.error('Error: Invalid JSON received from Vault.');
42
- throw new Error('Invalid JSON received from Vault.');
43
- }
44
- } catch (error) {
45
- if (error.response) {
46
- console.error(
47
- `Error: Failed to connect to Vault server at ${vaultAddr}. Response: ${error.response.status} ${error.response.statusText}`,
48
- );
49
- } else {
50
- console.error(`Error: ${error.message}`);
51
- }
52
- // throw error;
22
+ if (data && typeof data === 'object') return data.data.data;
23
+ throw new Error('Invalid JSON received from Vault.');
24
+ } catch (err) {
25
+ console.error(
26
+ 'Vault GET error:',
27
+ err?.message || err?.response?.statusText || err,
28
+ );
53
29
  }
54
30
  }
55
31
 
56
- /**
57
- * Update a secret in Vault.
58
- * @param {string} vaultPath - The path in Vault where the secret should be updated.
59
- * @param {Object} secretData - The secret data to be stored (as a JSON object).
60
- * @returns {Promise<Object>} - The response from Vault as a JSON object.
61
- */
62
32
  export async function updateVaultSecrets(vaultPath, secretData) {
63
- if (!vaultPath || !secretData) {
64
- console.error('Usage: updateVaultSecret(vaultPath, secretData)');
33
+ if (!vaultPath || !secretData)
65
34
  throw new Error('Both vaultPath and secretData are required.');
66
- }
67
35
 
68
- const vaultToken = process.env.VAULT_TOKEN;
69
- const vaultAddr = process.env.VAULT_ADDR;
70
-
71
- if (!vaultToken || !vaultAddr) {
72
- console.error('Error: VAULT_TOKEN and VAULT_ADDR must be set.');
36
+ const token = process.env.VAULT_TOKEN;
37
+ const addr = process.env.VAULT_ADDR;
38
+ if (!token || !addr)
73
39
  throw new Error('VAULT_TOKEN and VAULT_ADDR must be set.');
40
+
41
+ if (process.env.VAULT_SKIP_VERIFY === 'true') {
42
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
74
43
  }
75
44
 
76
45
  try {
77
- // Disable SSL verification globally if VAULT_SKIP_VERIFY is set
78
- if (process.env.VAULT_SKIP_VERIFY === 'true') {
79
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
80
- }
81
-
82
- // Make the POST request to update the Vault secret
83
- const response = await axios.post(
84
- `${vaultAddr}/v1/${vaultPath}`,
85
- secretData,
86
- {
87
- headers: {
88
- 'X-Vault-Token': vaultToken,
89
- 'Content-Type': 'application/json',
90
- },
46
+ const response = await axios.post(`${addr}/v1/${vaultPath}`, secretData, {
47
+ headers: {
48
+ 'X-Vault-Token': token,
49
+ 'Content-Type': 'application/json',
91
50
  },
92
- );
51
+ });
93
52
 
94
- // Return the response from Vault
95
- console.log(response.data);
96
53
  return response.data;
97
- } catch (error) {
98
- if (error.response) {
99
- console.error(
100
- `Error: Failed to update secret at ${vaultPath}. Response: ${error.response.status} ${error.response.statusText}`,
101
- );
102
- } else {
103
- console.error(`Error: ${error.message}`);
104
- }
105
- // throw error;
54
+ } catch (err) {
55
+ console.error(
56
+ 'Vault POST error:',
57
+ err?.message || err?.response?.statusText || err,
58
+ );
106
59
  }
107
60
  }
@@ -0,0 +1,134 @@
1
+ // src/auth0/vault.test.js
2
+ import { jest } from '@jest/globals';
3
+
4
+ // ESM-compatible mocking
5
+ jest.unstable_mockModule('axios', () => ({
6
+ default: {
7
+ get: jest.fn(),
8
+ post: jest.fn(),
9
+ },
10
+ }));
11
+
12
+ describe('Vault Utilities', () => {
13
+ let axios;
14
+ let getVaultSecrets;
15
+ let updateVaultSecrets;
16
+ const OLD_ENV = process.env;
17
+
18
+ beforeAll(async () => {
19
+ // Dynamic import AFTER mocking
20
+ axios = (await import('axios')).default;
21
+ const vault = await import('./vault.js');
22
+ getVaultSecrets = vault.getVaultSecrets;
23
+ updateVaultSecrets = vault.updateVaultSecrets;
24
+ });
25
+
26
+ beforeEach(() => {
27
+ jest.resetModules();
28
+ process.env = { ...OLD_ENV };
29
+ jest.clearAllMocks();
30
+ });
31
+
32
+ afterAll(() => {
33
+ process.env = OLD_ENV;
34
+ });
35
+
36
+ describe('getVaultSecrets', () => {
37
+ it('should fetch secrets from Vault successfully', async () => {
38
+ process.env.VAULT_TOKEN = 'test-token';
39
+ process.env.VAULT_ADDR = 'https://vault.example.com';
40
+
41
+ axios.get.mockResolvedValueOnce({
42
+ data: {
43
+ data: {
44
+ data: {
45
+ username: 'admin',
46
+ password: 'secret',
47
+ },
48
+ },
49
+ },
50
+ });
51
+
52
+ const result = await getVaultSecrets('secret/data/myapp');
53
+ expect(result).toEqual({ username: 'admin', password: 'secret' });
54
+ });
55
+
56
+ it('should throw error if vaultPath is missing', async () => {
57
+ await expect(getVaultSecrets()).rejects.toThrow(
58
+ 'VAULT_PATH is required.',
59
+ );
60
+ });
61
+
62
+ it('should throw error if env vars are missing', async () => {
63
+ delete process.env.VAULT_TOKEN;
64
+ delete process.env.VAULT_ADDR;
65
+
66
+ await expect(getVaultSecrets('secret/data/myapp')).rejects.toThrow(
67
+ 'VAULT_TOKEN and VAULT_ADDR must be set.',
68
+ );
69
+ });
70
+
71
+ it('should handle Vault error response', async () => {
72
+ process.env.VAULT_TOKEN = 'token';
73
+ process.env.VAULT_ADDR = 'https://vault.example.com';
74
+
75
+ axios.get.mockRejectedValueOnce({
76
+ response: { status: 403, statusText: 'Forbidden' },
77
+ });
78
+
79
+ const result = await getVaultSecrets('secret/data/myapp');
80
+ expect(result).toBeUndefined();
81
+ });
82
+ });
83
+
84
+ describe('updateVaultSecrets', () => {
85
+ it('should update secrets in Vault successfully', async () => {
86
+ process.env.VAULT_TOKEN = 'test-token';
87
+ process.env.VAULT_ADDR = 'https://vault.example.com';
88
+
89
+ const secretData = {
90
+ data: {
91
+ apiKey: '123456',
92
+ },
93
+ };
94
+
95
+ axios.post.mockResolvedValueOnce({
96
+ data: {
97
+ success: true,
98
+ },
99
+ });
100
+
101
+ const result = await updateVaultSecrets('secret/data/myapp', secretData);
102
+ expect(result).toEqual({ success: true });
103
+ });
104
+
105
+ it('should throw error if vaultPath or secretData is missing', async () => {
106
+ await expect(updateVaultSecrets()).rejects.toThrow(
107
+ 'Both vaultPath and secretData are required.',
108
+ );
109
+ });
110
+
111
+ it('should throw error if env vars are missing', async () => {
112
+ delete process.env.VAULT_TOKEN;
113
+ delete process.env.VAULT_ADDR;
114
+
115
+ await expect(updateVaultSecrets('secret/data/myapp', {})).rejects.toThrow(
116
+ 'VAULT_TOKEN and VAULT_ADDR must be set.',
117
+ );
118
+ });
119
+
120
+ it('should handle Vault error response', async () => {
121
+ process.env.VAULT_TOKEN = 'token';
122
+ process.env.VAULT_ADDR = 'https://vault.example.com';
123
+
124
+ axios.post.mockRejectedValueOnce({
125
+ response: { status: 500, statusText: 'Internal Server Error' },
126
+ });
127
+
128
+ const result = await updateVaultSecrets('secret/data/myapp', {
129
+ data: {},
130
+ });
131
+ expect(result).toBeUndefined();
132
+ });
133
+ });
134
+ });