@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.
- package/bitbucket-pipelines.yml +39 -7
- package/eslint.config.js +14 -3
- package/jest.config.js +15 -0
- package/package.json +4 -1
- package/src/auth0/alias.js +14 -7
- package/src/auth0/alias.test.js +172 -0
- package/src/auth0/cli.js +11 -9
- package/src/auth0/configure.js +2 -1
- package/src/auth0/configure.test.js +167 -0
- package/src/auth0/export.js +7 -2
- package/src/auth0/export.test.js +103 -0
- package/src/auth0/metadata.js +15 -9
- package/src/auth0/metadata.test.js +186 -0
- package/src/auth0/vault.js +34 -81
- package/src/auth0/vault.test.js +134 -0
package/bitbucket-pipelines.yml
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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",
|
package/src/auth0/alias.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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(
|
|
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);
|
package/src/auth0/configure.js
CHANGED
|
@@ -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
|
+
});
|
package/src/auth0/export.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
});
|
package/src/auth0/metadata.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
});
|
package/src/auth0/vault.js
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
69
|
-
const
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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 (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
+
});
|