| /** |
| * 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=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 \ |
| region=us-east-1`, |
| expected: { |
| method: 'write', |
| flagArray: [], |
| path: 'aws/config/root', |
| dataArray: [ |
| 'access_key=AKIAJWVN5Z4FOFT7NLNA', |
| 'secret_key=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', |
| '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=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', |
| 'region=us-east-1', |
| ], |
| flagInput: [], |
| expected: { |
| data: { |
| access_key: 'AKIAJWVN5Z4FOFT7NLNA', |
| secret_key: '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', |
| 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' |
| ); |
| }); |
| }); |
| }); |