@docupace/pipeline 1.0.66 → 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/src/next/deploy.js +22 -17
- package/src/next/git-to-semver.js +79 -0
- package/src/next/jenkins.js +2 -2
- package/src/next/test.js +5 -0
|
@@ -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
|
+
});
|