vault/ui/tests/unit/services/api-test.js
Jordan Reimer 38396b5882
[UI] API Service Error Parsing (#30454)
* adds error parsing method to api service

* replaces apiErrorMessage util instances with api service parseError

* removes apiErrorMessage util and tests

* removes ApiError type

* fixes issue in isLocalStorageSupported error handling
2025-04-30 11:44:19 -06:00

232 lines
8.5 KiB
JavaScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import sinon from 'sinon';
import config from 'vault/config/environment';
import { ResponseError } from '@hashicorp/vault-client-typescript';
module('Unit | Service | api', function (hooks) {
setupTest(hooks);
hooks.beforeEach(function () {
this.apiService = this.owner.lookup('service:api');
const authService = this.owner.lookup('service:auth');
this.setLastFetch = sinon.spy(authService, 'setLastFetch');
this.currentToken = sinon.stub(authService, 'currentToken').value('foobar');
const namespaceService = this.owner.lookup('service:namespace');
this.namespace = sinon.stub(namespaceService, 'path').value('another-ns');
const controlGroupService = this.owner.lookup('service:control-group');
this.wrapInfo = { token: 'ctrl-group', accessor: '84tfdfd5pQ5vOOEMxC2o3Ymt' };
this.tokenForUrl = sinon.stub(controlGroupService, 'tokenForUrl').returns(this.wrapInfo);
this.deleteControlGroupToken = sinon.spy(controlGroupService, 'deleteControlGroupToken');
const flashMessageService = this.owner.lookup('service:flash-messages');
this.info = sinon.spy(flashMessageService, 'info');
this.url = '/v1/sys/capabilities-self';
});
test('it should set last fetch time', async function (assert) {
await this.apiService.setLastFetch({ url: '/v1/sys/health' });
assert.true(this.setLastFetch.notCalled, 'Last fetch is not set for polling url');
await this.apiService.setLastFetch({ url: '/v1/auth/token/lookup-self' });
assert.true(this.setLastFetch.calledOnce, 'Last fetch is set for non polling url');
});
test('it should get control group token', async function (assert) {
const context = {
url: this.url,
init: {
method: 'GET',
headers: { 'X-Vault-Token': 'root' },
},
};
this.tokenForUrl.returns(undefined);
const noTokenContext = await this.apiService.getControlGroupToken(context);
assert.true(this.tokenForUrl.calledWith(context.url), 'Url is passed to tokenForUrl method');
assert.deepEqual(context, noTokenContext, 'Original context is returned when no token is present');
this.tokenForUrl.returns(this.wrapInfo);
const { token } = this.wrapInfo;
const tokenContext = await this.apiService.getControlGroupToken(context);
const newContext = {
url: '/v1/sys/wrapping/unwrap',
init: {
method: 'POST',
headers: { 'X-Vault-Token': token },
body: JSON.stringify({ token }),
},
};
assert.deepEqual(tokenContext, newContext, 'New context is returned when token is present');
});
test('it should set default headers', async function (assert) {
const {
init: { headers },
} = await this.apiService.setHeaders({ init: { method: 'PATCH' } });
assert.strictEqual(
headers.get('X-Vault-Token'),
'foobar',
'Token header is set with value from auth service'
);
assert.strictEqual(
headers.get('X-Vault-Namespace'),
'another-ns',
'Namespace header is set with value from namespace service'
);
assert.strictEqual(
headers.get('Content-Type'),
'application/merge-patch+json',
'Content type header is set for PATCH method'
);
});
test('it should override default headers when set on request init', async function (assert) {
const initHeaders = {
'X-Vault-Token': 'root',
'X-Vault-Namespace': 'ns1',
};
const {
init: { headers },
} = await this.apiService.setHeaders({ init: { headers: initHeaders } });
assert.strictEqual(headers.get('X-Vault-Token'), 'root', 'Token header set on request init is preserved');
assert.strictEqual(
headers.get('X-Vault-Namespace'),
'ns1',
'Namespace header set on request init is preserved'
);
});
test('it should show warnings', async function (assert) {
const warnings = JSON.stringify({ warnings: ['warning1', 'warning2'] });
const response = new Response(warnings, { headers: { 'Content-Length': warnings.length } });
await this.apiService.showWarnings({ response });
assert.true(this.info.firstCall.calledWith('warning1'), 'First warning message is shown');
assert.true(this.info.secondCall.calledWith('warning2'), 'Second warning message is shown');
});
test('it should not attempt to set warnings for empty response', async function (assert) {
const response = new Response();
await this.apiService.showWarnings({ response });
assert.true(this.info.notCalled, 'No warning messages are shown');
});
test('it should delete control group token', async function (assert) {
await this.apiService.deleteControlGroupToken({ url: this.url });
assert.true(this.tokenForUrl.calledWith(this.url), 'Url is passed to tokenForUrl method');
assert.true(
this.deleteControlGroupToken.calledWith(this.wrapInfo.accessor),
'Control group token is deleted'
);
});
test('it should build headers', async function (assert) {
const headerMap = {
token: 'foobar',
namespace: 'ns1',
wrap: '10s',
};
const token = await this.apiService.buildHeaders({ token: headerMap.token });
assert.deepEqual(token.headers, { 'X-Vault-Token': headerMap.token }, 'Token header is set');
const namespace = await this.apiService.buildHeaders({ namespace: headerMap.namespace });
assert.deepEqual(
namespace.headers,
{ 'X-Vault-Namespace': headerMap.namespace },
'Namespace header is set'
);
const wrapTTL = await this.apiService.buildHeaders({ wrap: headerMap.wrap });
assert.deepEqual(wrapTTL.headers, { 'X-Vault-Wrap-TTL': '10s' }, 'Wrap TTL header is set');
const multi = await this.apiService.buildHeaders(headerMap);
assert.deepEqual(
multi.headers,
{
'X-Vault-Token': 'foobar',
'X-Vault-Namespace': 'ns1',
'X-Vault-Wrap-TTL': '10s',
},
'All supported headers are set'
);
});
module('Error parsing', function (hooks) {
hooks.beforeEach(function () {
this.response = {
errors: ['first error', 'second error'],
message: 'there were some errors',
};
this.getErrorResponse = () =>
new ResponseError({
status: 404,
url: `${document.location.origin}/v1/test/error/parsing`,
json: () => Promise.resolve(this.response),
});
});
test('it should correctly parse message from error', async function (assert) {
let e = await this.apiService.parseError(this.getErrorResponse());
assert.strictEqual(e.message, 'first error, second error', 'Builds message from errors');
this.response.errors = [];
e = await this.apiService.parseError(this.getErrorResponse());
assert.strictEqual(e.message, 'there were some errors', 'Returns message when errors are empty');
const error = new Error('some js type error');
e = await this.apiService.parseError(error);
assert.strictEqual(e.message, error.message, 'Returns message from generic Error');
e = await this.apiService.parseError('some random error');
assert.strictEqual(e.message, 'An error occurred, please try again', 'Returns default fallback');
const fallback = 'Everything is broken, sorry';
e = await this.apiService.parseError('some random error', fallback);
assert.strictEqual(e.message, fallback, 'Returns custom fallback');
});
test('it should return status', async function (assert) {
const { status } = await this.apiService.parseError(this.getErrorResponse());
assert.strictEqual(status, 404, 'Returns the status code from the response');
});
test('it should return path', async function (assert) {
const { path } = await this.apiService.parseError(this.getErrorResponse());
assert.strictEqual(path, '/v1/test/error/parsing', 'Returns the path from the request url');
});
test('it should return error response', async function (assert) {
const { response } = await this.apiService.parseError(this.getErrorResponse());
assert.deepEqual(response, this.response, 'Returns the original error response');
});
test('it should log out error in development environment', async function (assert) {
const consoleStub = sinon.stub(console, 'log');
sinon.stub(config, 'environment').value('development');
const error = new Error('some js type error');
await this.apiService.parseError(error);
assert.true(consoleStub.calledWith('API Error:', error));
sinon.restore();
});
});
});