mirror of
https://github.com/Keeper-Security/harness-integration.git
synced 2026-06-04 10:14:56 +08:00
unit testing
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
@@ -9,12 +9,7 @@ SECRETS_FILE=$(mktemp)
|
|||||||
node /app/src/index.js > "$SECRETS_FILE"
|
node /app/src/index.js > "$SECRETS_FILE"
|
||||||
|
|
||||||
# 3. Securely process the secrets
|
# 3. Securely process the secrets
|
||||||
# Parse lines in format: ENV:VAR_NAME='value', OUT:VAR_NAME='value', or VAR_NAME='value'
|
|
||||||
#
|
|
||||||
# SECURITY NOTE: All secrets are written to /harness/outputs/ and /harness/secrets/
|
# SECURITY NOTE: All secrets are written to /harness/outputs/ and /harness/secrets/
|
||||||
# These directories are mounted volumes scoped ONLY to the current pipeline execution.
|
|
||||||
# Harness CI automatically cleans up these volumes after pipeline completion.
|
|
||||||
# Secrets are NOT accessible outside the pipeline execution context.
|
|
||||||
mkdir -p /harness/outputs /harness/secrets
|
mkdir -p /harness/outputs /harness/secrets
|
||||||
|
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
@@ -53,9 +48,6 @@ while IFS= read -r line; do
|
|||||||
export "$name=$value"
|
export "$name=$value"
|
||||||
|
|
||||||
# Write to Harness CI Plugin Output (for output variables)
|
# Write to Harness CI Plugin Output (for output variables)
|
||||||
# These are scoped to the pipeline execution and accessible via:
|
|
||||||
# <+step.output.outputVariables.VAR_NAME> in subsequent steps
|
|
||||||
# Format: KEY=VALUE (one per line)
|
|
||||||
printf "%s=%s\n" "$name" "$value" >> /harness/outputs/outputs.txt
|
printf "%s=%s\n" "$name" "$value" >> /harness/outputs/outputs.txt
|
||||||
|
|
||||||
# For environment variables, also write to env_vars.txt for Harness to pick up
|
# For environment variables, also write to env_vars.txt for Harness to pick up
|
||||||
@@ -65,28 +57,13 @@ while IFS= read -r line; do
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Write to file for direct access (bypasses Harness truncation)
|
# Write to file for direct access (bypasses Harness truncation)
|
||||||
# SECURITY: Files in /harness/secrets/ are scoped to pipeline execution only
|
|
||||||
# Harness CI automatically cleans up these files after pipeline completion
|
|
||||||
echo -n "$value" > "/harness/secrets/${name}"
|
echo -n "$value" > "/harness/secrets/${name}"
|
||||||
chmod 600 "/harness/secrets/${name}" # Restrict permissions to owner only
|
chmod 600 "/harness/secrets/${name}" # Restrict permissions to owner only
|
||||||
|
|
||||||
# Debug: Log (removed to reduce log noise)
|
|
||||||
# value_length=${#value}
|
|
||||||
# if [ "$type" = "env" ]; then
|
|
||||||
# echo "INFO: Set environment variable: $name (length: $value_length)"
|
|
||||||
# else
|
|
||||||
# echo "INFO: Set output variable: $name (length: $value_length)"
|
|
||||||
# fi
|
|
||||||
# echo "INFO: Secret also written to /harness/secrets/${name} for direct file access"
|
|
||||||
|
|
||||||
done < "$SECRETS_FILE"
|
done < "$SECRETS_FILE"
|
||||||
|
|
||||||
# 4. Secure Clean up
|
# 4. Secure Clean up
|
||||||
# Remove the temporary file to ensure no sensitive data remains on disk
|
|
||||||
# Note: /harness/outputs/ and /harness/secrets/ are cleaned up by Harness CI
|
|
||||||
# after pipeline execution completes - they are scoped to the pipeline only
|
|
||||||
rm -f "$SECRETS_FILE"
|
rm -f "$SECRETS_FILE"
|
||||||
|
|
||||||
# 5. Hand over control to the Docker command (if any)
|
# 5. Hand over control to the Docker command (if any)
|
||||||
# This allows the container to be used as a wrapper for other commands
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
module.exports = {
|
||||||
|
testEnvironment: 'node',
|
||||||
|
collectCoverageFrom: [
|
||||||
|
'src/**/*.js',
|
||||||
|
'!src/**/*.test.js',
|
||||||
|
],
|
||||||
|
coverageThreshold: {
|
||||||
|
global: {
|
||||||
|
branches: 100,
|
||||||
|
functions: 100,
|
||||||
|
lines: 100,
|
||||||
|
statements: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testMatch: [
|
||||||
|
'**/__tests__/**/*.js',
|
||||||
|
'**/*.test.js',
|
||||||
|
],
|
||||||
|
verbose: true,
|
||||||
|
};
|
||||||
Generated
-22
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "harness_keeper_plugin",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "harness_keeper_plugin",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"@keeper-security/secrets-manager-core": "^17.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@keeper-security/secrets-manager-core": {
|
|
||||||
"version": "17.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@keeper-security/secrets-manager-core/-/secrets-manager-core-17.3.0.tgz",
|
|
||||||
"integrity": "sha512-9Wm5s4qbJJlOFmB1L1p2+TqWLdMg/IQMcKEXKw2ZTOQlHjGzECrX2HoeCEdCG8datt/n7uo7f84uby4GvJyOXA==",
|
|
||||||
"license": "MIT"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+5
-1
@@ -5,7 +5,8 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "jest --coverage",
|
||||||
|
"test:watch": "jest --watch"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -13,5 +14,8 @@
|
|||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@keeper-security/secrets-manager-core": "^17.3.0"
|
"@keeper-security/secrets-manager-core": "^17.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"jest": "^29.7.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -106,6 +106,7 @@ const setupStorage = async (token, isConfigJson, config) => {
|
|||||||
|
|
||||||
const runPlugin = async () => {
|
const runPlugin = async () => {
|
||||||
try {
|
try {
|
||||||
|
core.info('Starting Keeper Secrets Manager plugin');
|
||||||
fs.mkdirSync('/app', { recursive: true });
|
fs.mkdirSync('/app', { recursive: true });
|
||||||
|
|
||||||
const { token, isConfigJson, config } = processToken(process.env.KSM_CONFIG);
|
const { token, isConfigJson, config } = processToken(process.env.KSM_CONFIG);
|
||||||
@@ -130,8 +131,9 @@ const runPlugin = async () => {
|
|||||||
if (isFileReference) {
|
if (isFileReference) {
|
||||||
try {
|
try {
|
||||||
const fileData = await downloadFile(secret);
|
const fileData = await downloadFile(secret);
|
||||||
data = fileData instanceof Uint8Array ? Buffer.from(fileData) :
|
data = Buffer.isBuffer(fileData) ? fileData :
|
||||||
Buffer.isBuffer(fileData) ? fileData : Buffer.from(fileData);
|
fileData instanceof Uint8Array ? Buffer.from(fileData) :
|
||||||
|
Buffer.from(fileData);
|
||||||
} catch (downloadError) {
|
} catch (downloadError) {
|
||||||
core.error(`Failed to download file for notation ${input.notation}: ${downloadError.message}`);
|
core.error(`Failed to download file for notation ${input.notation}: ${downloadError.message}`);
|
||||||
continue;
|
continue;
|
||||||
@@ -153,7 +155,7 @@ const runPlugin = async () => {
|
|||||||
fs.chmodSync(secretFilePath, 0o600);
|
fs.chmodSync(secretFilePath, 0o600);
|
||||||
|
|
||||||
if (input.destinationType === 'environment') {
|
if (input.destinationType === 'environment') {
|
||||||
const outputValue = Buffer.isBuffer(data) ? data.toString('utf8') : String(data);
|
const outputValue = data.toString('utf8');
|
||||||
console.log(`ENV:${input.destination}='${outputValue}'`);
|
console.log(`ENV:${input.destination}='${outputValue}'`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,929 @@
|
|||||||
|
// Mock all external dependencies before requiring the module
|
||||||
|
jest.mock('@keeper-security/secrets-manager-core');
|
||||||
|
jest.mock('fs');
|
||||||
|
jest.mock('path');
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
describe('index.js - Complete Test Suite', () => {
|
||||||
|
let getSecrets, getValue, localConfigStorage, downloadFile, initializeStorage;
|
||||||
|
let mockExit, mockStderrWrite, mockConsoleLog;
|
||||||
|
let originalEnv;
|
||||||
|
|
||||||
|
// Helper to wait for async operations to complete
|
||||||
|
const waitForAsync = async () => {
|
||||||
|
// Wait multiple ticks to ensure all async operations complete
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
await new Promise(resolve => setImmediate(resolve));
|
||||||
|
}
|
||||||
|
// Also wait a small timeout to ensure all promises resolve
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10));
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset all mocks
|
||||||
|
jest.clearAllMocks();
|
||||||
|
jest.resetModules();
|
||||||
|
|
||||||
|
// Save original environment
|
||||||
|
originalEnv = { ...process.env };
|
||||||
|
|
||||||
|
// Mock process methods
|
||||||
|
mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
||||||
|
mockStderrWrite = jest.spyOn(process.stderr, 'write').mockImplementation(() => {});
|
||||||
|
mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
|
||||||
|
// Setup mocks for Keeper Secrets Manager
|
||||||
|
getSecrets = jest.fn();
|
||||||
|
getValue = jest.fn();
|
||||||
|
localConfigStorage = jest.fn();
|
||||||
|
downloadFile = jest.fn();
|
||||||
|
initializeStorage = jest.fn();
|
||||||
|
|
||||||
|
const secretsManagerCore = require('@keeper-security/secrets-manager-core');
|
||||||
|
secretsManagerCore.getSecrets = getSecrets;
|
||||||
|
secretsManagerCore.getValue = getValue;
|
||||||
|
secretsManagerCore.localConfigStorage = localConfigStorage;
|
||||||
|
secretsManagerCore.downloadFile = downloadFile;
|
||||||
|
secretsManagerCore.initializeStorage = initializeStorage;
|
||||||
|
|
||||||
|
// Setup fs mocks - need to get fresh references after resetModules
|
||||||
|
const fsModule = require('fs');
|
||||||
|
fs.mkdirSync = jest.fn();
|
||||||
|
fs.writeFileSync = jest.fn();
|
||||||
|
fs.chmodSync = jest.fn();
|
||||||
|
// Also set on the module itself
|
||||||
|
fsModule.mkdirSync = fs.mkdirSync;
|
||||||
|
fsModule.writeFileSync = fs.writeFileSync;
|
||||||
|
fsModule.chmodSync = fs.chmodSync;
|
||||||
|
|
||||||
|
// Setup path mocks
|
||||||
|
const pathModule = require('path');
|
||||||
|
path.resolve = jest.fn((p) => `/resolved/${p}`);
|
||||||
|
path.dirname = jest.fn((p) => {
|
||||||
|
const parts = p.split('/');
|
||||||
|
parts.pop();
|
||||||
|
return parts.join('/') || '/';
|
||||||
|
});
|
||||||
|
path.join = jest.fn((...args) => args.join('/'));
|
||||||
|
pathModule.resolve = path.resolve;
|
||||||
|
pathModule.dirname = path.dirname;
|
||||||
|
pathModule.join = path.join;
|
||||||
|
|
||||||
|
// Clear environment variables
|
||||||
|
delete process.env.KSM_CONFIG;
|
||||||
|
delete process.env.PLUGIN_SECRETS;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore original environment
|
||||||
|
process.env = originalEnv;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
mockExit?.mockRestore();
|
||||||
|
mockStderrWrite?.mockRestore();
|
||||||
|
mockConsoleLog?.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('processToken - Error Cases', () => {
|
||||||
|
test('should exit when rawToken is null', () => {
|
||||||
|
delete process.env.KSM_CONFIG;
|
||||||
|
require('../index');
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
expect(mockStderrWrite).toHaveBeenCalledWith(expect.stringContaining('KSM config is required'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should exit when rawToken is undefined', () => {
|
||||||
|
delete process.env.KSM_CONFIG;
|
||||||
|
require('../index');
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should exit when rawToken is empty string', () => {
|
||||||
|
process.env.KSM_CONFIG = '';
|
||||||
|
require('../index');
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should exit when token contains ${ Harness expression', () => {
|
||||||
|
process.env.KSM_CONFIG = '${harness.expression}';
|
||||||
|
require('../index');
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
expect(mockStderrWrite).toHaveBeenCalledWith(expect.stringContaining('Harness expression not resolved'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should exit when token contains <+ Harness expression', () => {
|
||||||
|
process.env.KSM_CONFIG = '<+expression>';
|
||||||
|
require('../index');
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should exit when token contains ngSecretManager', () => {
|
||||||
|
process.env.KSM_CONFIG = 'ngSecretManager.token';
|
||||||
|
require('../index');
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should exit when token is empty after trim', () => {
|
||||||
|
process.env.KSM_CONFIG = ' ';
|
||||||
|
require('../index');
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
expect(mockStderrWrite).toHaveBeenCalledWith(expect.stringContaining('Token is null or empty'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should exit when JSON config is missing hostname field', () => {
|
||||||
|
const jsonConfig = JSON.stringify({ clientId: 'client', privateKey: 'key' });
|
||||||
|
process.env.KSM_CONFIG = jsonConfig;
|
||||||
|
require('../index');
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
expect(mockStderrWrite).toHaveBeenCalledWith(expect.stringContaining('Missing required fields'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should exit when JSON config is missing clientId field', () => {
|
||||||
|
const jsonConfig = JSON.stringify({ hostname: 'test.com', privateKey: 'key' });
|
||||||
|
process.env.KSM_CONFIG = jsonConfig;
|
||||||
|
require('../index');
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should exit when JSON config is missing privateKey field', () => {
|
||||||
|
const jsonConfig = JSON.stringify({ hostname: 'test.com', clientId: 'client' });
|
||||||
|
process.env.KSM_CONFIG = jsonConfig;
|
||||||
|
require('../index');
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should exit when JSON config is missing multiple fields', () => {
|
||||||
|
const jsonConfig = JSON.stringify({ hostname: 'test.com' });
|
||||||
|
process.env.KSM_CONFIG = jsonConfig;
|
||||||
|
require('../index');
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should exit when JSON config is invalid', () => {
|
||||||
|
process.env.KSM_CONFIG = '{invalid json}';
|
||||||
|
require('../index');
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
expect(mockStderrWrite).toHaveBeenCalledWith(expect.stringContaining('Invalid JSON config'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should exit when token format is invalid (not US: or JSON)', () => {
|
||||||
|
process.env.KSM_CONFIG = 'INVALID:token';
|
||||||
|
require('../index');
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
expect(mockStderrWrite).toHaveBeenCalledWith(expect.stringContaining('Invalid token format'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('processToken - Success Cases', () => {
|
||||||
|
test('should decode base64 token successfully', async () => {
|
||||||
|
const base64Token = Buffer.from('US:decoded-token').toString('base64');
|
||||||
|
process.env.KSM_CONFIG = base64Token;
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('secret-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(initializeStorage).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle base64 decode error gracefully', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'invalid-base64!@#';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('secret-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
// Should continue execution despite decode error
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse valid JSON config', async () => {
|
||||||
|
const jsonConfig = JSON.stringify({ hostname: 'test.com', clientId: 'client', privateKey: 'key' });
|
||||||
|
process.env.KSM_CONFIG = jsonConfig;
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output';
|
||||||
|
const mockStorage = {};
|
||||||
|
localConfigStorage.mockReturnValue(mockStorage);
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('secret-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalledWith('/app/ksm-config.json', jsonConfig, 'utf8');
|
||||||
|
expect(localConfigStorage).toHaveBeenCalledWith('/app/ksm-config.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should accept US: token format', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test-token';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output';
|
||||||
|
const mockStorage = {};
|
||||||
|
localConfigStorage.mockReturnValue(mockStorage);
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('secret-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(initializeStorage).toHaveBeenCalledWith(mockStorage, 'US:test-token');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle token with whitespace', async () => {
|
||||||
|
process.env.KSM_CONFIG = ' US:test-token ';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('secret-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(initializeStorage).toHaveBeenCalledWith(expect.anything(), 'US:test-token');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call info function on plugin start', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('secret-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
// Verify info was called with the startup message
|
||||||
|
expect(mockStderrWrite).toHaveBeenCalledWith(expect.stringContaining('INFO: Starting Keeper Secrets Manager plugin'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should test console.log in base64 decode catch block', () => {
|
||||||
|
// This test ensures line 49 (console.log(e)) is covered
|
||||||
|
// Buffer.from doesn't throw for invalid base64, so we need to mock it to throw
|
||||||
|
const originalBufferFrom = Buffer.from;
|
||||||
|
Buffer.from = jest.fn(() => {
|
||||||
|
throw new Error('Base64 decode error');
|
||||||
|
});
|
||||||
|
|
||||||
|
process.env.KSM_CONFIG = 'not-us-format-token';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('secret-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
// The console.log in catch block should have been called
|
||||||
|
expect(mockConsoleLog).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Restore Buffer.from
|
||||||
|
Buffer.from = originalBufferFrom;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseSecretMappings', () => {
|
||||||
|
test('should parse env: destination type', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>env:VAR_NAME';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('secret-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(mockConsoleLog).toHaveBeenCalledWith("ENV:VAR_NAME='secret-value'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse file: destination type', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>file:/path/to/file';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('secret-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse output destination type (default)', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output_var';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('secret-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle input without > separator', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('secret-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle multiple > characters', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>part1>destination';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('secret-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle multiple secret mappings', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation1>env:VAR1\nnotation2>file:/path/file\nnotation3>output1';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('secret-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(getValue).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should filter empty lines from multiline input', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation1>output1\n\nnotation2>output2\n \nnotation3>output3';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('secret-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(getValue).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle empty secrets input', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = '';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(getValue).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('runPlugin - File Handling', () => {
|
||||||
|
test('should create /app directory', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output';
|
||||||
|
const mockStorage = {};
|
||||||
|
localConfigStorage.mockReturnValue(mockStorage);
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('secret-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
// Verify the async chain executed
|
||||||
|
expect(initializeStorage).toHaveBeenCalled();
|
||||||
|
expect(getSecrets).toHaveBeenCalled();
|
||||||
|
// Check that mkdirSync was called for /app (first call in runPlugin)
|
||||||
|
expect(fs.mkdirSync).toHaveBeenCalledWith('/app', { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle file destination type', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>file:/path/to/file.txt';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('file-content');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(path.resolve).toHaveBeenCalledWith('/path/to/file.txt');
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create directory for file destination', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>file:/path/to/file.txt';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('file-content');
|
||||||
|
path.dirname.mockReturnValue('/path/to');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(fs.mkdirSync).toHaveBeenCalledWith('/path/to', { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle file secret with fileId', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>file:/path/to/file.txt';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue({ fileId: 'file123' });
|
||||||
|
downloadFile.mockResolvedValue(Buffer.from('file-data'));
|
||||||
|
path.dirname.mockReturnValue('/path/to');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(downloadFile).toHaveBeenCalledWith({ fileId: 'file123' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle file secret with fileUid', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>file:/path/to/file.txt';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue({ fileUid: 'file123' });
|
||||||
|
downloadFile.mockResolvedValue(new Uint8Array([1, 2, 3]));
|
||||||
|
path.dirname.mockReturnValue('/path/to');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(downloadFile).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle file secret with url', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>file:/path/to/file.txt';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue({ url: 'https://example.com/file' });
|
||||||
|
downloadFile.mockResolvedValue(Buffer.from('file-data'));
|
||||||
|
path.dirname.mockReturnValue('/path/to');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(downloadFile).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle file secret with /file/ notation', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'record/field/file/file.txt>file:/path/to/file.txt';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
// Return an object so isFileReference check passes
|
||||||
|
// The notation includes '/file/' which will make notationIsFile true
|
||||||
|
getValue.mockReturnValue({ someProperty: 'value' });
|
||||||
|
downloadFile.mockResolvedValue(Buffer.from('file-data'));
|
||||||
|
path.dirname.mockReturnValue('/path/to');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(downloadFile).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle downloadFile error gracefully', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>file:/path/to/file.txt';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue({ fileId: 'file123' });
|
||||||
|
downloadFile.mockRejectedValue(new Error('Download failed'));
|
||||||
|
path.dirname.mockReturnValue('/path/to');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(mockStderrWrite).toHaveBeenCalledWith(expect.stringContaining('Failed to download file'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle Uint8Array file data', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>file:/path/to/file.txt';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue({ fileId: 'file123' });
|
||||||
|
const uint8Array = new Uint8Array([1, 2, 3, 4]);
|
||||||
|
downloadFile.mockResolvedValue(uint8Array);
|
||||||
|
path.dirname.mockReturnValue('/path/to');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle Buffer file data', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>file:/path/to/file.txt';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue({ fileId: 'file123' });
|
||||||
|
downloadFile.mockResolvedValue(Buffer.from('buffer-data'));
|
||||||
|
path.dirname.mockReturnValue('/path/to');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle other file data types', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>file:/path/to/file.txt';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue({ fileId: 'file123' });
|
||||||
|
// Return something that's not Uint8Array or Buffer to test the third branch
|
||||||
|
downloadFile.mockResolvedValue('string-data');
|
||||||
|
path.dirname.mockReturnValue('/path/to');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle file data that is neither Uint8Array nor Buffer (line 135 branch)', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>file:/path/to/file.txt';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue({ fileId: 'file123' });
|
||||||
|
// Return something that's not Uint8Array or Buffer to test the third branch of ternary
|
||||||
|
// Use an array to ensure it's not Uint8Array (regular array)
|
||||||
|
downloadFile.mockResolvedValue([1, 2, 3]);
|
||||||
|
path.dirname.mockReturnValue('/path/to');
|
||||||
|
path.resolve.mockReturnValue('/path/to/file.txt');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(downloadFile).toHaveBeenCalled();
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle file data that is Buffer (line 134 branch)', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>file:/path/to/file.txt';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue({ fileId: 'file123' });
|
||||||
|
// Return a Buffer (not Uint8Array) to test the Buffer.isBuffer branch
|
||||||
|
// Note: In Node.js, Buffer IS a Uint8Array, so this tests the first branch
|
||||||
|
const bufferData = Buffer.from('buffer-content');
|
||||||
|
downloadFile.mockResolvedValue(bufferData);
|
||||||
|
path.dirname.mockReturnValue('/path/to');
|
||||||
|
path.resolve.mockReturnValue('/resolved/path/to/file.txt');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(downloadFile).toHaveBeenCalled();
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining('file.txt'), bufferData);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle file data that is plain Uint8Array (not Buffer)', async () => {
|
||||||
|
// Test the second branch: Buffer.isBuffer is false but instanceof Uint8Array is true
|
||||||
|
// Use a plain Uint8Array (not a Buffer) to test the middle branch of the ternary
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>file:/path/to/file.txt';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue({ fileId: 'file123' });
|
||||||
|
|
||||||
|
// Create a plain Uint8Array (not a Buffer)
|
||||||
|
// Buffer.isBuffer will return false, but instanceof Uint8Array will be true
|
||||||
|
const uint8Array = new Uint8Array([1, 2, 3, 4, 5]);
|
||||||
|
downloadFile.mockResolvedValue(uint8Array);
|
||||||
|
path.dirname.mockReturnValue('/path/to');
|
||||||
|
path.resolve.mockReturnValue('/resolved/path/to/file.txt');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(downloadFile).toHaveBeenCalled();
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||||
|
// Should convert Uint8Array to Buffer
|
||||||
|
const writeCall = fs.writeFileSync.mock.calls.find(call => call[0].includes('file.txt'));
|
||||||
|
expect(writeCall).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('runPlugin - Secret Value Handling', () => {
|
||||||
|
test('should handle string secret value', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output_var';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('string-secret');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle Buffer secret value', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output_var';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue(Buffer.from('buffer-secret'));
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle non-string, non-buffer secret value', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output_var';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue(12345);
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should skip secret when value is not found', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output_var';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue(null);
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(mockStderrWrite).toHaveBeenCalledWith(expect.stringContaining('::warning::'));
|
||||||
|
expect(mockStderrWrite).toHaveBeenCalledWith(expect.stringContaining('Value not found'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should skip secret when value is undefined', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output_var';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue(undefined);
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(mockStderrWrite).toHaveBeenCalledWith(expect.stringContaining('::warning::'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('runPlugin - Destination Types', () => {
|
||||||
|
test('should handle environment destination type', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>env:VAR_NAME';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('env-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(mockConsoleLog).toHaveBeenCalledWith("ENV:VAR_NAME='env-value'");
|
||||||
|
expect(fs.mkdirSync).toHaveBeenCalledWith('/harness/secrets', { recursive: true });
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||||
|
expect(fs.chmodSync).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle environment destination with Buffer data', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>env:VAR_NAME';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue(Buffer.from('buffer-env-value'));
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining("ENV:VAR_NAME"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle environment destination with non-Buffer data (line 157 branch)', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>env:VAR_NAME';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
// Return a number to test the String(data) branch
|
||||||
|
getValue.mockReturnValue(12345);
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining("ENV:VAR_NAME"));
|
||||||
|
expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining("12345"));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('should create /harness/secrets directory for non-file destinations', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output_var';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('secret-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(fs.mkdirSync).toHaveBeenCalledWith('/harness/secrets', { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should set file permissions to 0o600 for secret files', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output_var';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('secret-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(fs.chmodSync).toHaveBeenCalledWith('/harness/secrets/output_var', 0o600);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not output ENV: for non-environment destinations', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output_var';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('secret-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(mockConsoleLog).not.toHaveBeenCalledWith(expect.stringContaining('ENV:'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('runPlugin - Error Handling', () => {
|
||||||
|
test('should handle storage initialization error', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockRejectedValue(new Error('Storage initialization failed'));
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(mockStderrWrite).toHaveBeenCalledWith(expect.stringContaining('Failed:'));
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle token-related error with specific message', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockRejectedValue(new Error('Invalid token'));
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(mockStderrWrite).toHaveBeenCalledWith(expect.stringContaining('One-time access tokens'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle expired token error', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockRejectedValue(new Error('Token expired'));
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(mockStderrWrite).toHaveBeenCalledWith(expect.stringContaining('One-time access tokens'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle invalid token error', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockRejectedValue(new Error('invalid'));
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(mockStderrWrite).toHaveBeenCalledWith(expect.stringContaining('One-time access tokens'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle getSecrets error', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockRejectedValue(new Error('Failed to get secrets'));
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(mockStderrWrite).toHaveBeenCalledWith(expect.stringContaining('Failed:'));
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle generic error without token message', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockRejectedValue(new Error('Generic error'));
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(mockStderrWrite).toHaveBeenCalledWith(expect.stringContaining('Failed:'));
|
||||||
|
expect(mockStderrWrite).not.toHaveBeenCalledWith(expect.stringContaining('One-time access tokens'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge Cases and Integration', () => {
|
||||||
|
test('should handle null secret object', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue(null);
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(mockStderrWrite).toHaveBeenCalledWith(expect.stringContaining('::warning::'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle object secret without file properties', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation>output';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue({ someProperty: 'value' });
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle object secret without file properties and notation without /file/', async () => {
|
||||||
|
// Test the branch where typeof secret === 'object' && secret !== null is true
|
||||||
|
// but (secret.fileId || secret.fileUid || secret.url || notationIsFile) is false
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'regular/notation>output';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
// Return an object without fileId, fileUid, url, and notation doesn't include '/file/'
|
||||||
|
getValue.mockReturnValue({ regularProperty: 'value', anotherProp: 123 });
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
// Should treat it as a regular secret, not a file reference
|
||||||
|
expect(downloadFile).not.toHaveBeenCalled();
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle complex multiline input with mixed types', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'notation1>env:VAR1\nnotation2>file:/path/file\nnotation3>output1\nnotation4>env:VAR2';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue('secret-value');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(getValue).toHaveBeenCalledTimes(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle file notation with multiple file references', async () => {
|
||||||
|
process.env.KSM_CONFIG = 'US:test';
|
||||||
|
process.env.PLUGIN_SECRETS = 'record1/field/file/file1.txt>file:/path/file1\nrecord2/field/file/file2.txt>file:/path/file2';
|
||||||
|
localConfigStorage.mockReturnValue({});
|
||||||
|
initializeStorage.mockResolvedValue();
|
||||||
|
getSecrets.mockResolvedValue({});
|
||||||
|
getValue.mockReturnValue({ fileId: 'file123' });
|
||||||
|
downloadFile.mockResolvedValue(Buffer.from('file-data'));
|
||||||
|
path.dirname.mockReturnValue('/path');
|
||||||
|
|
||||||
|
require('../index');
|
||||||
|
await waitForAsync();
|
||||||
|
expect(downloadFile).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user