510 lines
14 KiB
JavaScript
510 lines
14 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: MPL-2.0
|
|
*/
|
|
|
|
import { module, test } from 'qunit';
|
|
import {
|
|
parseCommand,
|
|
logFromResponse,
|
|
logFromError,
|
|
formattedErrorFromInput,
|
|
extractFlagsFromStrings,
|
|
extractDataFromStrings,
|
|
} from 'vault/lib/console-helpers';
|
|
|
|
module('Unit | Lib | console helpers', function () {
|
|
const testCommands = [
|
|
{
|
|
name: 'write with data',
|
|
command: `vault write aws/config/root \
|
|
access_key=AKIAJWVN5Z4FOFT7NLNA \
|
|
secret_key=R4nm063hgMVo4BTT5xOs5nHLeLXA6lar7ZJ3Nt0i \
|
|
region=us-east-1`,
|
|
expected: {
|
|
method: 'write',
|
|
flagArray: [],
|
|
path: 'aws/config/root',
|
|
dataArray: [
|
|
'access_key=AKIAJWVN5Z4FOFT7NLNA',
|
|
'secret_key=R4nm063hgMVo4BTT5xOs5nHLeLXA6lar7ZJ3Nt0i',
|
|
'region=us-east-1',
|
|
],
|
|
},
|
|
},
|
|
{
|
|
name: 'write with space in a value',
|
|
command: `vault write \
|
|
auth/ldap/config \
|
|
url=ldap://ldap.example.com:3268 \
|
|
binddn="CN=ServiceViewDev,OU=Service Accounts,DC=example,DC=com" \
|
|
bindpass="xxxxxxxxxxxxxxxxxxxxxxxxxx" \
|
|
userdn="DC=example,DC=com" \
|
|
groupdn="DC=example,DC=com" \
|
|
insecure_tls=true \
|
|
starttls=false
|
|
`,
|
|
expected: {
|
|
method: 'write',
|
|
flagArray: [],
|
|
path: 'auth/ldap/config',
|
|
dataArray: [
|
|
'url=ldap://ldap.example.com:3268',
|
|
'binddn=CN=ServiceViewDev,OU=Service Accounts,DC=example,DC=com',
|
|
'bindpass=xxxxxxxxxxxxxxxxxxxxxxxxxx',
|
|
'userdn=DC=example,DC=com',
|
|
'groupdn=DC=example,DC=com',
|
|
'insecure_tls=true',
|
|
'starttls=false',
|
|
],
|
|
},
|
|
},
|
|
{
|
|
name: 'write with double quotes',
|
|
command: `vault write \
|
|
auth/token/create \
|
|
policies="foo"
|
|
`,
|
|
expected: { method: 'write', flagArray: [], path: 'auth/token/create', dataArray: ['policies=foo'] },
|
|
},
|
|
{
|
|
name: 'write with single quotes',
|
|
command: `vault write \
|
|
auth/token/create \
|
|
policies='foo'
|
|
`,
|
|
expected: { method: 'write', flagArray: [], path: 'auth/token/create', dataArray: ['policies=foo'] },
|
|
},
|
|
{
|
|
name: 'write with unmatched quotes',
|
|
command: `vault write \
|
|
auth/token/create \
|
|
policies="'foo"
|
|
`,
|
|
expected: { method: 'write', flagArray: [], path: 'auth/token/create', dataArray: ["policies='foo"] },
|
|
},
|
|
{
|
|
name: 'write with shell characters',
|
|
/* eslint-disable no-useless-escape */
|
|
command: `vault write database/roles/api-prod db_name=apiprod creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" default_ttl=1h max_ttl=24h
|
|
`,
|
|
expected: {
|
|
method: 'write',
|
|
flagArray: [],
|
|
path: 'database/roles/api-prod',
|
|
dataArray: [
|
|
'db_name=apiprod',
|
|
`creation_statements=CREATE ROLE {{name}} WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO {{name}};`,
|
|
'default_ttl=1h',
|
|
'max_ttl=24h',
|
|
],
|
|
},
|
|
},
|
|
|
|
{
|
|
name: 'read with field',
|
|
command: `vault read -field=access_key aws/creds/my-role`,
|
|
expected: {
|
|
method: 'read',
|
|
flagArray: ['-field=access_key'],
|
|
path: 'aws/creds/my-role',
|
|
dataArray: [],
|
|
},
|
|
},
|
|
];
|
|
|
|
testCommands.forEach(function (testCase) {
|
|
test(`#parseCommand: ${testCase.name}`, function (assert) {
|
|
const result = parseCommand(testCase.command);
|
|
assert.deepEqual(result, testCase.expected);
|
|
});
|
|
});
|
|
|
|
test('#parseCommand: invalid commands', function (assert) {
|
|
assert.expect(1);
|
|
const command = 'vault kv get foo';
|
|
assert.throws(
|
|
() => {
|
|
parseCommand(command);
|
|
},
|
|
/invalid command/,
|
|
'throws on invalid command'
|
|
);
|
|
});
|
|
|
|
const testExtractCases = [
|
|
{
|
|
method: 'read',
|
|
name: 'data fields',
|
|
dataInput: [
|
|
'access_key=AKIAJWVN5Z4FOFT7NLNA',
|
|
'secret_key=R4nm063hgMVo4BTT5xOs5nHLeLXA6lar7ZJ3Nt0i',
|
|
'region=us-east-1',
|
|
],
|
|
flagInput: [],
|
|
expected: {
|
|
data: {
|
|
access_key: 'AKIAJWVN5Z4FOFT7NLNA',
|
|
secret_key: 'R4nm063hgMVo4BTT5xOs5nHLeLXA6lar7ZJ3Nt0i',
|
|
region: 'us-east-1',
|
|
},
|
|
flags: {},
|
|
},
|
|
},
|
|
{
|
|
method: 'read',
|
|
name: 'repeated data and a flag',
|
|
dataInput: ['allowed_domains=example.com', 'allowed_domains=foo.example.com'],
|
|
flagInput: ['-wrap-ttl=2h'],
|
|
expected: {
|
|
data: {
|
|
allowed_domains: ['example.com', 'foo.example.com'],
|
|
},
|
|
flags: {
|
|
wrapTTL: '2h',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
method: 'read',
|
|
name: 'triple data',
|
|
dataInput: [
|
|
'allowed_domains=example.com',
|
|
'allowed_domains=foo.example.com',
|
|
'allowed_domains=dev.example.com',
|
|
],
|
|
flagInput: [],
|
|
expected: {
|
|
data: {
|
|
allowed_domains: ['example.com', 'foo.example.com', 'dev.example.com'],
|
|
},
|
|
flags: {},
|
|
},
|
|
},
|
|
{
|
|
method: 'read',
|
|
name: 'data with more than one equals sign',
|
|
dataInput: ['foo=bar=baz', 'foo=baz=bop', 'some=value=val'],
|
|
flagInput: [],
|
|
expected: {
|
|
data: {
|
|
foo: ['bar=baz', 'baz=bop'],
|
|
some: 'value=val',
|
|
},
|
|
flags: {},
|
|
},
|
|
},
|
|
{
|
|
method: 'read',
|
|
name: 'data with empty values',
|
|
dataInput: [`foo=`, 'some=thing'],
|
|
flagInput: [],
|
|
expected: {
|
|
data: {
|
|
foo: '',
|
|
some: 'thing',
|
|
},
|
|
flags: {},
|
|
},
|
|
},
|
|
{
|
|
method: 'write',
|
|
name: 'write with force flag',
|
|
dataInput: [],
|
|
flagInput: ['-force'],
|
|
expected: {
|
|
data: {},
|
|
flags: {
|
|
force: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
method: 'write',
|
|
name: 'write with force short flag',
|
|
dataInput: [],
|
|
flagInput: ['-f'],
|
|
expected: {
|
|
data: {},
|
|
flags: {
|
|
force: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
method: 'write',
|
|
name: 'write with GNU style force flag',
|
|
dataInput: [],
|
|
flagInput: ['--force'],
|
|
expected: {
|
|
data: {},
|
|
flags: {
|
|
force: true,
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
testExtractCases.forEach(function (testCase) {
|
|
test(`#extractDataFromStrings: ${testCase.name}`, function (assert) {
|
|
const data = extractDataFromStrings(testCase.dataInput);
|
|
assert.deepEqual(data, testCase.expected.data, 'has expected data');
|
|
});
|
|
test(`#extractFlagsFromStrings: ${testCase.name}`, function (assert) {
|
|
const flags = extractFlagsFromStrings(testCase.flagInput, testCase.method);
|
|
assert.deepEqual(flags, testCase.expected.flags, 'has expected flags');
|
|
});
|
|
});
|
|
|
|
const testResponseCases = [
|
|
{
|
|
name: 'write response, no content',
|
|
args: [null, 'foo/bar', 'write', {}],
|
|
expectedData: {
|
|
type: 'success',
|
|
content: 'Success! Data written to: foo/bar',
|
|
},
|
|
},
|
|
{
|
|
name: 'delete response, no content',
|
|
args: [null, 'foo/bar', 'delete', {}],
|
|
expectedData: {
|
|
type: 'success',
|
|
content: 'Success! Data deleted (if it existed) at: foo/bar',
|
|
},
|
|
},
|
|
{
|
|
name: 'read, no data, auth, wrap_info',
|
|
args: [{ foo: 'bar', one: 'two' }, 'foo/bar', 'read', {}],
|
|
expectedData: {
|
|
type: 'object',
|
|
content: { foo: 'bar', one: 'two' },
|
|
},
|
|
},
|
|
{
|
|
name: 'read with -format=json flag, no data, auth, wrap_info',
|
|
args: [{ foo: 'bar', one: 'two' }, 'foo/bar', 'read', { format: 'json' }],
|
|
expectedData: {
|
|
type: 'json',
|
|
content: { foo: 'bar', one: 'two' },
|
|
},
|
|
},
|
|
{
|
|
name: 'read with -field flag, no data, auth, wrap_info',
|
|
args: [{ foo: 'bar', one: 'two' }, 'foo/bar', 'read', { field: 'one' }],
|
|
expectedData: {
|
|
type: 'text',
|
|
content: 'two',
|
|
},
|
|
},
|
|
{
|
|
name: 'write, with content',
|
|
args: [{ data: { one: 'two' } }, 'foo/bar', 'write', {}],
|
|
expectedData: {
|
|
type: 'object',
|
|
content: { one: 'two' },
|
|
},
|
|
},
|
|
{
|
|
name: 'with wrap-ttl flag',
|
|
args: [{ wrap_info: { one: 'two' } }, 'foo/bar', 'read', { wrapTTL: '1h' }],
|
|
expectedData: {
|
|
type: 'object',
|
|
content: { one: 'two' },
|
|
},
|
|
},
|
|
{
|
|
name: 'with -format=json flag and wrap-ttl flag',
|
|
args: [{ foo: 'bar', wrap_info: { one: 'two' } }, 'foo/bar', 'read', { format: 'json', wrapTTL: '1h' }],
|
|
expectedData: {
|
|
type: 'json',
|
|
content: { foo: 'bar', wrap_info: { one: 'two' } },
|
|
},
|
|
},
|
|
{
|
|
name: 'with -format=json and -field flags',
|
|
args: [{ foo: 'bar', data: { one: 'two' } }, 'foo/bar', 'read', { format: 'json', field: 'one' }],
|
|
expectedData: {
|
|
type: 'json',
|
|
content: 'two',
|
|
},
|
|
},
|
|
{
|
|
name: 'with -format=json and -field, and -wrap-ttl flags',
|
|
args: [
|
|
{ foo: 'bar', wrap_info: { one: 'two' } },
|
|
'foo/bar',
|
|
'read',
|
|
{ format: 'json', wrapTTL: '1h', field: 'one' },
|
|
],
|
|
expectedData: {
|
|
type: 'json',
|
|
content: 'two',
|
|
},
|
|
},
|
|
{
|
|
name: 'with string field flag and wrap-ttl flag',
|
|
args: [{ foo: 'bar', wrap_info: { one: 'two' } }, 'foo/bar', 'read', { field: 'one', wrapTTL: '1h' }],
|
|
expectedData: {
|
|
type: 'text',
|
|
content: 'two',
|
|
},
|
|
},
|
|
{
|
|
name: 'with object field flag and wrap-ttl flag',
|
|
args: [
|
|
{ foo: 'bar', wrap_info: { one: { two: 'three' } } },
|
|
'foo/bar',
|
|
'read',
|
|
{ field: 'one', wrapTTL: '1h' },
|
|
],
|
|
expectedData: {
|
|
type: 'object',
|
|
content: { two: 'three' },
|
|
},
|
|
},
|
|
{
|
|
name: 'with response data and string field flag',
|
|
args: [{ foo: 'bar', data: { one: 'two' } }, 'foo/bar', 'read', { field: 'one', wrapTTL: '1h' }],
|
|
expectedData: {
|
|
type: 'text',
|
|
content: 'two',
|
|
},
|
|
},
|
|
{
|
|
name: 'with response data and object field flag ',
|
|
args: [
|
|
{ foo: 'bar', data: { one: { two: 'three' } } },
|
|
'foo/bar',
|
|
'read',
|
|
{ field: 'one', wrapTTL: '1h' },
|
|
],
|
|
expectedData: {
|
|
type: 'object',
|
|
content: { two: 'three' },
|
|
},
|
|
},
|
|
{
|
|
name: 'response with data',
|
|
args: [{ foo: 'bar', data: { one: 'two' } }, 'foo/bar', 'read', {}],
|
|
expectedData: {
|
|
type: 'object',
|
|
content: { one: 'two' },
|
|
},
|
|
},
|
|
{
|
|
name: 'with response data, field flag, and field missing',
|
|
args: [{ foo: 'bar', data: { one: 'two' } }, 'foo/bar', 'read', { field: 'foo' }],
|
|
expectedData: {
|
|
type: 'error',
|
|
content: 'Field "foo" not present in secret',
|
|
},
|
|
},
|
|
{
|
|
name: 'with response data and auth block',
|
|
args: [{ data: { one: 'two' }, auth: { three: 'four' } }, 'auth/token/create', 'write', {}],
|
|
expectedData: {
|
|
type: 'object',
|
|
content: { three: 'four' },
|
|
},
|
|
},
|
|
{
|
|
name: 'with -field and -format with an object field',
|
|
args: [{ data: { one: { three: 'two' } } }, 'sys/mounts', 'read', { field: 'one', format: 'json' }],
|
|
expectedData: {
|
|
type: 'json',
|
|
content: { three: 'two' },
|
|
},
|
|
},
|
|
{
|
|
name: 'with -field and -format with a string field',
|
|
args: [{ data: { one: 'two' } }, 'sys/mounts', 'read', { field: 'one', format: 'json' }],
|
|
expectedData: {
|
|
type: 'json',
|
|
content: 'two',
|
|
},
|
|
},
|
|
];
|
|
|
|
testResponseCases.forEach(function (testCase) {
|
|
test(`#logFromResponse: ${testCase.name}`, function (assert) {
|
|
const data = logFromResponse(...testCase.args);
|
|
assert.deepEqual(data, testCase.expectedData);
|
|
});
|
|
});
|
|
|
|
const testErrorCases = [
|
|
{
|
|
name: 'AdapterError write',
|
|
args: [{ httpStatus: 404, path: 'v1/sys/foo', errors: [{}] }, 'sys/foo', 'write'],
|
|
expectedContent: 'Error writing to: sys/foo.\nURL: v1/sys/foo\nCode: 404',
|
|
},
|
|
{
|
|
name: 'AdapterError read',
|
|
args: [{ httpStatus: 404, path: 'v1/sys/foo', errors: [{}] }, 'sys/foo', 'read'],
|
|
expectedContent: 'Error reading from: sys/foo.\nURL: v1/sys/foo\nCode: 404',
|
|
},
|
|
{
|
|
name: 'AdapterError list',
|
|
args: [{ httpStatus: 404, path: 'v1/sys/foo', errors: [{}] }, 'sys/foo', 'list'],
|
|
expectedContent: 'Error listing: sys/foo.\nURL: v1/sys/foo\nCode: 404',
|
|
},
|
|
{
|
|
name: 'AdapterError delete',
|
|
args: [{ httpStatus: 404, path: 'v1/sys/foo', errors: [{}] }, 'sys/foo', 'delete'],
|
|
expectedContent: 'Error deleting at: sys/foo.\nURL: v1/sys/foo\nCode: 404',
|
|
},
|
|
{
|
|
name: 'VaultError single error',
|
|
args: [{ httpStatus: 404, path: 'v1/sys/foo', errors: ['no client token'] }, 'sys/foo', 'delete'],
|
|
expectedContent: 'Error deleting at: sys/foo.\nURL: v1/sys/foo\nCode: 404\nErrors:\n no client token',
|
|
},
|
|
{
|
|
name: 'VaultErrors multiple errors',
|
|
args: [
|
|
{ httpStatus: 404, path: 'v1/sys/foo', errors: ['no client token', 'this is an error'] },
|
|
'sys/foo',
|
|
'delete',
|
|
],
|
|
expectedContent:
|
|
'Error deleting at: sys/foo.\nURL: v1/sys/foo\nCode: 404\nErrors:\n no client token\n this is an error',
|
|
},
|
|
];
|
|
|
|
testErrorCases.forEach(function (testCase) {
|
|
test(`#logFromError: ${testCase.name}`, function (assert) {
|
|
const data = logFromError(...testCase.args);
|
|
assert.deepEqual(
|
|
data,
|
|
{ type: 'error', content: testCase.expectedContent },
|
|
'returns the expected data'
|
|
);
|
|
});
|
|
});
|
|
|
|
const testCommandCases = [
|
|
{
|
|
name: 'errors when command does not include a path',
|
|
args: [],
|
|
expectedContent: 'A path is required to make a request.',
|
|
},
|
|
{
|
|
name: 'errors when write command does not include data and does not have force tag',
|
|
args: ['foo/bar', 'write', {}, []],
|
|
expectedContent: 'Must supply data or use -force',
|
|
},
|
|
];
|
|
|
|
testCommandCases.forEach(function (testCase) {
|
|
test(`#formattedErrorFromInput: ${testCase.name}`, function (assert) {
|
|
const data = formattedErrorFromInput(...testCase.args);
|
|
|
|
assert.deepEqual(
|
|
data,
|
|
{ type: 'error', content: testCase.expectedContent },
|
|
'returns the pcorrect data'
|
|
);
|
|
});
|
|
});
|
|
});
|