@microfocus/alm-octane-js-rest-sdk
Version:
NodeJS wrapper for the OpenText Core Software Delivery Platform API
728 lines (671 loc) • 25.3 kB
text/typescript
/*
* Copyright 2020-2025 Open Text.
*
* The only warranties for products and services of Open Text and
* its affiliates and licensors (“Open Text”) are as may be set forth
* in the express warranty statements accompanying such products and services.
* Nothing herein should be construed as constituting an additional warranty.
* Open Text shall not be liable for technical or editorial errors or
* omissions contained herein. The information contained herein is subject
* to change without notice.
*
* Except as specifically indicated otherwise, this document contains
* confidential information and a valid license is required for possession,
* use or copying. If this work is provided to the U.S. Government,
* consistent with FAR 12.211 and 12.212, Commercial Computer Software,
* Computer Software Documentation, and Technical Data for Commercial Items are
* licensed to the U.S. Government under vendor's standard commercial license.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env mocha */
import assert from 'assert';
import { AxiosInstance } from 'axios';
import MockAdapter from 'axios-mock-adapter';
import RequestHandler from '../../lib/root/requestHandler';
const testServerUrl = 'http://localhost';
const uri = '/some/uri';
const user = 'MyUser';
const password = 'MyPassword';
const token = 'TestToken123';
const UNAUTHORIZED_ERROR = {
name: 'Error',
response: {
status: 401,
},
};
export class BuildServerResponses {
scope: MockAdapter;
authenticatedCookie: string;
loginCookieName: string;
constructor(requester: AxiosInstance) {
this.scope = new MockAdapter(requester);
this.authenticatedCookie = 'Authenticated';
this.loginCookieName = 'LWSSO_COOKIE_KEY';
}
withSuccessfulAuthentication(user?: string, password?: string) {
this.scope
.onPost('/authentication/sign_in', {
asymmetricMatch(actual: { user: string; password: string }) {
if (user) {
assert.strictEqual(actual.user, user);
}
if (password) {
assert.strictEqual(actual.password, password);
}
return true;
},
})
.reply(200, undefined, {
'Set-Cookie': this.loginCookieName + '=' + this.authenticatedCookie,
});
return this;
}
withUnsuccessfulAuthentication() {
this.scope
.onPost('/authentication/sign_in')
.reply(401, undefined, { 'Set-Cookie': this.loginCookieName + '=' });
return this;
}
withUnsuccessfulGetRequest(uri: string, errorBody: object) {
this.scope.onGet(uri).reply(400, errorBody);
return this;
}
withUnsuccessfulDeleteRequest(uri: string, errorBody: object) {
this.scope.onDelete(uri).reply(400, errorBody);
return this;
}
withUnsuccessfulUpdateRequest(uri: string, errorBody: object) {
this.scope.onPut(uri).reply(400, errorBody);
return this;
}
withUnsuccessfulCreateRequest(uri: string, errorBody: object) {
this.scope.onPost(uri).reply(400, errorBody);
return this;
}
withGetRequest(uri: string, statusCode: number, replyData?: any) {
this.scope.onGet(uri).reply(statusCode, replyData);
return this;
}
withDeleteRequest(uri: string, statusCode: number) {
this.scope.onDelete(uri).reply(statusCode);
return this;
}
withUpdateRequest(
uri: string,
statusCode: number,
body?: object,
replyData?: any
) {
this.scope.onPut(uri, body).reply(statusCode, replyData);
return this;
}
withCreateRequest(
uri: string,
statusCode: number,
body?: object,
replyData?: any
) {
this.scope.onPost(uri, body).reply(statusCode, replyData);
return this;
}
withUnauthorizedUpdateRequest(uri: string, body?: object, replyData?: any) {
return this.withUpdateRequest(uri, 401, body, replyData);
}
withUnauthorizedGetRequest(uri: string) {
return this.withGetRequest(uri, 401);
}
withUnauthorizedDeleteRequest(uri: string) {
return this.withDeleteRequest(uri, 401);
}
withUnauthorizedCreateRequest(uri: string, body?: object, replyData?: any) {
return this.withCreateRequest(uri, 401, body, replyData);
}
withSuccessfulSignOut() {
this.scope
.onPost('/authentication/sign_out')
.reply(200, undefined, { 'Set-Cookie': this.loginCookieName + '=' });
return this;
}
withUnsuccessfulSignOut() {
this.scope
.onPost('/authentication/sign_out')
.reply(401, undefined, { 'Set-Cookie': 'LWSSO_COOKIE_KEY=' });
return this;
}
withTokenGetRequest(uri: string, token: string, statusCode: number, replyData?: any) {
this.scope
.onGet(uri)
.reply(function(config) {
// Verify the Authorization header contains the token
if (config.headers?.Authorization === `Bearer ${token}`) {
return [statusCode, replyData];
}
return [401, { error: 'Invalid token' }];
});
return this;
}
withUnauthorizedTokenGetRequest(uri: string, token: string) {
return this.withTokenGetRequest(uri, token, 401);
}
withTokenDeleteRequest(uri: string, token: string, statusCode: number) {
this.scope
.onDelete(uri)
.reply(function(config) {
if (config.headers?.Authorization === `Bearer ${token}`) {
return [statusCode];
}
return [401, { error: 'Invalid token' }];
});
return this;
}
withUnauthorizedTokenDeleteRequest(uri: string, token: string) {
return this.withTokenDeleteRequest(uri, token, 401);
}
withTokenUpdateRequest(uri: string, token: string, statusCode: number, body?: object, replyData?: any) {
this.scope
.onPut(uri, body)
.reply(function(config) {
if (config.headers?.Authorization === `Bearer ${token}`) {
return [statusCode, replyData];
}
return [401, { error: 'Invalid token' }];
});
return this;
}
withTokenCreateRequest(uri: string, token: string, statusCode: number, body?: object, replyData?: any) {
this.scope
.onPost(uri, body)
.reply(function(config) {
if (config.headers?.Authorization === `Bearer ${token}`) {
return [statusCode, replyData];
}
return [401, { error: 'Invalid token' }];
});
return this;
}
build() {
return this.scope;
}
}
describe('requestHandler', function () {
// @ts-ignore
this.slow(350);
let requestHandler: RequestHandler;
beforeEach(() => {
const requestHandlerInitialization = {
user: user,
password: password,
server: testServerUrl,
sharedSpace: 1001,
workspace: 1002,
};
requestHandler = new RequestHandler(requestHandlerInitialization);
});
async function throwsUnauthorizedException(promise: Promise<any>) {
await throwsException(promise, 401, undefined);
}
async function throwsException(
promise: Promise<any>,
statusCode?: number,
errorBody?: any
) {
try {
await promise;
assert.fail();
} catch (e: any) {
if (errorBody) {
assert.strictEqual(
JSON.stringify(e.response.data),
JSON.stringify(errorBody)
);
}
if (statusCode) {
assert.strictEqual(e.response.status, statusCode);
}
}
}
describe('#authenticate', () => {
it('makes a successful authentication request with the given username and password', async () => {
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withSuccessfulAuthentication(user, password)
.build();
const shouldBeUndefined = await requestHandler.authenticate();
await assert.strictEqual(shouldBeUndefined.data, undefined);
scope.reset();
});
it('throws an error after failed authentication', async () => {
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnsuccessfulAuthentication()
.build();
await throwsUnauthorizedException(requestHandler.authenticate());
scope.reset();
});
});
describe('#get', () => {
it('throws an error in case of an error response different from 401', async () => {
const error = { error: 'Failed as intended' };
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnsuccessfulGetRequest(uri, error)
.build();
await throwsException(requestHandler.get(uri), 400, error);
scope.reset();
});
it('throws an error if request was answered with status code 401 two times', async () => {
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnauthorizedGetRequest(uri)
.withSuccessfulAuthentication()
.withUnauthorizedGetRequest(uri)
.build();
await throwsUnauthorizedException(requestHandler.get(uri));
scope.reset();
});
it('throws an error if request was answered with status code 401 and authentication fails', async () => {
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnauthorizedGetRequest(uri)
.withUnsuccessfulAuthentication()
.build();
await throwsUnauthorizedException(requestHandler.get(uri));
scope.reset();
});
it('returns the data even if the 1st request was with the status code 401', async function () {
const objectToGet = 'Request was successful';
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnauthorizedGetRequest(uri)
.withSuccessfulAuthentication()
.withGetRequest(uri, 200, objectToGet)
.build();
const objectGot = await requestHandler.get(uri);
assert.strictEqual(objectGot.data, objectToGet);
scope.reset();
});
it('makes a successful get request', async function () {
const objectToGet = 'Request was successful';
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withGetRequest(uri, 200, objectToGet)
.build();
const objectGot = await requestHandler.get(uri);
assert.strictEqual(objectGot.data, objectToGet);
scope.reset();
});
});
describe('#delete', () => {
it('throws an error in case of an error response different from 401', async () => {
const error = { error: 'Failed as intended' };
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnsuccessfulDeleteRequest(uri, error)
.build();
await throwsException(requestHandler.delete(uri), 400, error);
scope.reset();
});
it('throws an error if request was answered with status code 401 two times', async function () {
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnauthorizedDeleteRequest(uri)
.withSuccessfulAuthentication()
.withUnauthorizedDeleteRequest(uri)
.build();
await throwsUnauthorizedException(requestHandler.delete(uri));
scope.reset();
});
it('throws an error if request was answered with status code 401 and authentication fails', async () => {
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnauthorizedDeleteRequest(uri)
.withUnsuccessfulAuthentication()
.build();
await throwsUnauthorizedException(requestHandler.delete(uri));
scope.reset();
});
it('request is made even if 1st request was with the status code 401', async function () {
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnauthorizedDeleteRequest(uri)
.withSuccessfulAuthentication()
.withDeleteRequest(uri, 200)
.build();
const shouldBeUndefined = await requestHandler.delete(uri);
assert.strictEqual(shouldBeUndefined?.data, undefined);
scope.reset();
});
it('makes a successful delete request', async function () {
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withDeleteRequest(uri, 200)
.build();
const shouldBeUndefined = await requestHandler.delete(uri);
assert.strictEqual(shouldBeUndefined.data, undefined);
scope.reset();
});
});
describe('#update', () => {
it('throws an error in case of an error response different from 401', async () => {
const error = { error: 'Failed as intended' };
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnsuccessfulUpdateRequest(uri, error)
.build();
await throwsException(requestHandler.update(uri), 400, error);
scope.reset();
});
it('throws an error if request was answered with status code 401 two times', async function () {
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnauthorizedUpdateRequest(uri)
.withSuccessfulAuthentication()
.withUnauthorizedUpdateRequest(uri)
.build();
await throwsUnauthorizedException(requestHandler.update(uri));
scope.reset();
});
it('throws an error if request was answered with status code 401 and authentication fails', async () => {
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnauthorizedUpdateRequest(uri)
.withUnsuccessfulAuthentication()
.build();
await throwsUnauthorizedException(requestHandler.update(uri));
scope.reset();
});
it('request is made even if 1st request was with the status code 401', async function () {
const updatedObject = 'Success';
const updateBody = {
parameter1: 'para 1',
parameter2: 2,
};
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnauthorizedUpdateRequest(uri, updateBody, updatedObject)
.withSuccessfulAuthentication()
.withUpdateRequest(uri, 200, updateBody, updatedObject)
.build();
const responseObject = await requestHandler.update(uri, updateBody);
assert.strictEqual(responseObject.data, updatedObject);
scope.reset();
});
it('makes a successful update request', async function () {
const updatedObject = 'Success';
const updateBody = {
parameter1: 'para 1',
parameter2: 2,
};
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUpdateRequest(uri, 200, updateBody, updatedObject)
.build();
const responseObject = await requestHandler.update(uri, updateBody);
assert.strictEqual(responseObject.data, updatedObject);
scope.reset();
});
});
describe('#create', () => {
it('throws an error in case of an error response different from 401', async () => {
const error = { error: 'Failed as intended' };
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnsuccessfulCreateRequest(uri, error)
.build();
await throwsException(requestHandler.create(uri), 400, error);
scope.reset();
});
it('throws an error if request was answered with status code 401 two times', async function () {
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnauthorizedCreateRequest(uri)
.withSuccessfulAuthentication()
.withUnauthorizedCreateRequest(uri)
.build();
await throwsUnauthorizedException(requestHandler.create(uri));
scope.reset();
});
it('throws an error if request was answered with status code 401 and authentication fails', async () => {
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnauthorizedCreateRequest(uri)
.withUnsuccessfulAuthentication()
.build();
await throwsUnauthorizedException(requestHandler.create(uri));
scope.reset();
});
it('request is made even if 1st request was with the status code 401', async function () {
const createObject = 'Success';
const createBody = {
parameter1: 'para 1',
parameter2: 2,
};
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnauthorizedCreateRequest(uri, createBody, createObject)
.withSuccessfulAuthentication()
.withCreateRequest(uri, 200, createBody, createObject)
.build();
const responseObject = await requestHandler.create(uri, createBody);
assert.strictEqual(responseObject.data, createObject);
scope.reset();
});
it('makes a successful update request', async function () {
const createObject = { data: 'Success' };
const createBody = {
parameter1: 'para 1',
parameter2: 2,
};
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withCreateRequest(uri, 200, createBody, createObject)
.build();
const responseObject = await requestHandler.create(uri, createBody);
assert.strictEqual(
JSON.stringify(responseObject.data),
JSON.stringify(createObject)
);
scope.reset();
});
});
describe('#reauthenicate', () => {
it('throws the error given if it does not have the status code 401', async () => {
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnsuccessfulAuthentication()
.build();
const errorWithoutStatusCode = { response: { data: 'Error' } };
const errorWithStatusCode = { response: { status: 400, data: 'Error' } };
await throwsException(
// @ts-ignore
requestHandler._reauthenticate(errorWithoutStatusCode),
undefined,
errorWithoutStatusCode.response.data
);
await throwsException(
// @ts-ignore
requestHandler._reauthenticate(errorWithStatusCode),
400,
errorWithStatusCode.response.data
);
scope.reset();
});
it('throws an error if the error received has status code 401 and the authentication fails ', async function () {
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnsuccessfulAuthentication()
.build();
await throwsUnauthorizedException(
// @ts-ignore
requestHandler._reauthenticate(UNAUTHORIZED_ERROR)
);
scope.reset();
});
it('authenticates again if the received error has the status code 401', async function () {
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withSuccessfulAuthentication()
.build();
// @ts-ignore
const shouldBeUndefined = await requestHandler._reauthenticate(
UNAUTHORIZED_ERROR
);
assert.strictEqual(shouldBeUndefined?.data, undefined);
scope.reset();
});
});
describe('#signOut', () => {
it('throws an error if the request fails', async function () {
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withUnsuccessfulSignOut()
.build();
await throwsUnauthorizedException(requestHandler.signOut());
scope.reset();
});
it('throws an error if the request fails', async function () {
// @ts-ignore
const scope = new BuildServerResponses(requestHandler._requestor)
.withSuccessfulSignOut()
.build();
const shouldBeUndefined = await requestHandler.signOut();
assert.strictEqual(shouldBeUndefined.data, undefined);
scope.reset();
});
});
});
describe('requestHandler with token authentication', function () {
let requestHandlerWithToken: RequestHandler;
beforeEach(() => {
requestHandlerWithToken = new RequestHandler({
token: token,
server: testServerUrl,
sharedSpace: 1001,
workspace: 1002,
});
});
describe('#get with token', () => {
it('makes a successful get request using token authentication', async function () {
const objectToGet = 'Request was successful with token';
// @ts-ignore
const scope = new BuildServerResponses(requestHandlerWithToken._requestor)
.withTokenGetRequest(uri, token, 200, objectToGet)
.build();
const objectGot = await requestHandlerWithToken.get(uri);
assert.strictEqual(objectGot.data, objectToGet);
scope.reset();
});
it('throws an error if token authentication fails', async function () {
// @ts-ignore
const scope = new BuildServerResponses(requestHandlerWithToken._requestor)
.withUnauthorizedTokenGetRequest(uri, token)
.build();
try {
await requestHandlerWithToken.get(uri);
assert.fail('Expected an error to be thrown');
} catch (err: any) {
assert.ok(
err.message.includes('Token authentication failed'),
`Expected error message about token failure, but got: ${err.message}`
);
}
scope.reset();
});
});
describe('#delete with token', () => {
it('makes a successful delete request using token authentication', async function () {
// @ts-ignore
const scope = new BuildServerResponses(requestHandlerWithToken._requestor)
.withTokenDeleteRequest(uri, token, 200)
.build();
const shouldBeUndefined = await requestHandlerWithToken.delete(uri);
assert.strictEqual(shouldBeUndefined.data, undefined);
scope.reset();
});
it('throws an error if token authentication fails', async function () {
// @ts-ignore
const scope = new BuildServerResponses(requestHandlerWithToken._requestor)
.withUnauthorizedTokenDeleteRequest(uri, token)
.build();
try {
await requestHandlerWithToken.delete(uri);
assert.fail('Expected an error to be thrown');
} catch (err: any) {
assert.ok(
err.message.includes('Token authentication failed'),
`Expected error message about token failure, but got: ${err.message}`
);
}
scope.reset();
});
});
describe('#update with token', () => {
it('makes a successful update request using token authentication', async function () {
const updatedObject = 'Success';
const updateBody = {
parameter1: 'parameter1',
parameter2: 2,
};
// @ts-ignore
const scope = new BuildServerResponses(requestHandlerWithToken._requestor)
.withTokenUpdateRequest(uri, token, 200, updateBody, updatedObject)
.build();
const responseObject = await requestHandlerWithToken.update(uri, updateBody);
assert.strictEqual(responseObject.data, updatedObject);
scope.reset();
});
});
describe('#create with token', () => {
it('makes a successful create request using token authentication', async function () {
const createObject = { data: 'Success' };
const createBody = {
parameter1: 'parameter1',
parameter2: 2,
};
// @ts-ignore
const scope = new BuildServerResponses(requestHandlerWithToken._requestor)
.withTokenCreateRequest(uri, token, 200, createBody, createObject)
.build();
const responseObject = await requestHandlerWithToken.create(uri, createBody);
assert.strictEqual(
JSON.stringify(responseObject.data),
JSON.stringify(createObject)
);
scope.reset();
});
});
});
// Test the hybrid flow (credentials + token)
describe('requestHandler with credentials and token (hybrid)', function () {
let requestHandlerHybrid: RequestHandler;
beforeEach(() => {
requestHandlerHybrid = new RequestHandler({
user: user,
password: password,
token: token,
server: testServerUrl,
sharedSpace: 1001,
workspace: 1002,
});
});
it('check if credentials were specifically used when having hybrid setup', async function () {
const objectToGet = 'Request was successful';
// @ts-ignore
const scope = new BuildServerResponses(requestHandlerHybrid._requestor)
.withSuccessfulAuthentication()
.withGetRequest(uri, 200, objectToGet)
.build();
const objectGot = await requestHandlerHybrid.get(uri);
assert.strictEqual(objectGot.data, objectToGet);
scope.reset();
});
});