Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions api/ServiceEndpointApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

import VsoBaseInterfaces = require('./interfaces/common/VsoBaseInterfaces');
import serviceendpointbasem = require('./ServiceEndpointApiBase');

export interface IServiceEndpointApi extends serviceendpointbasem.IServiceEndpointApiBase {
}

export class ServiceEndpointApi extends serviceendpointbasem.ServiceEndpointApiBase implements IServiceEndpointApi {
constructor(baseUrl: string, handlers: VsoBaseInterfaces.IRequestHandler[], options?: VsoBaseInterfaces.IRequestOptions, userAgent?: string) {
super(baseUrl, handlers, options, userAgent);
}
}
1,395 changes: 1,395 additions & 0 deletions api/ServiceEndpointApiBase.ts

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions api/WebApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import profilem = require('./ProfileApi');
import projectm = require('./ProjectAnalysisApi');
import releasem = require('./ReleaseApi');
import securityrolesm = require('./SecurityRolesApi');
import serviceendpointm = require('./ServiceEndpointApi');
import taskagentm = require('./TaskAgentApi');
import taskm = require('./TaskApi');
import testm = require('./TestApi');
Expand Down Expand Up @@ -314,6 +315,12 @@ export class WebApi {
return new securityrolesm.SecurityRolesApi(serverUrl, handlers, this.options, this.userAgent);
}

public async getServiceEndpointApi(serverUrl?: string, handlers?: VsoBaseInterfaces.IRequestHandler[]): Promise<serviceendpointm.IServiceEndpointApi> {
serverUrl = await this._getResourceAreaUrl(serverUrl || this.serverUrl, "1814ab31-2f4f-4a9f-8761-f4d77dc5a5d7");
handlers = handlers || [this.authHandler];
return new serviceendpointm.ServiceEndpointApi(serverUrl, handlers, this.options, this.userAgent);
}

public async getReleaseApi(serverUrl?: string, handlers?: VsoBaseInterfaces.IRequestHandler[]): Promise<releasem.IReleaseApi> {
// TODO: Load RESOURCE_AREA_ID correctly.
serverUrl = await this._getResourceAreaUrl(serverUrl || this.serverUrl, "efc2f575-36ef-48e9-b672-0c6fb4a48ac5");
Expand Down Expand Up @@ -477,16 +484,16 @@ export class WebApi {

let keyFile = Buffer.from(lookupInfo[0], 'base64').toString('utf8');
let keyAndIv = fs.readFileSync(keyFile, 'utf8');

let [keyBase64, ivBase64] = keyAndIv.split(':', 2);

if (!keyBase64 || !ivBase64) {
throw new Error(
'Invalid encryption key format. Expected "key:iv" format from azure-pipelines-task-lib 5.2.4+. ' +
'This version of azure-devops-node-api (15.2.0+) is not compatible with task-lib <5.2.4.'
);
}

Comment thread
ivanduplenskikh marked this conversation as resolved.
let encryptKey = Buffer.from(keyBase64, 'base64');
let iv = Buffer.from(ivBase64, 'base64');

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "azure-devops-node-api",
"description": "Node client for Azure DevOps and TFS REST APIs",
"version": "15.2.0",
"version": "16.0.0",
"main": "./WebApi.js",
"types": "./WebApi.d.ts",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion test/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

110 changes: 110 additions & 0 deletions test/units/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,116 @@ describe('WebApi Units', function () {
assert.equal(myWebApi.isNoProxyHost('https://my-tfs-instance.host/myproject'), true);
assert.equal(myWebApi.isNoProxyHost('https://my-other-tfs-instance.host/myproject'), false);
});

describe('getServiceEndpointApi', function () {
const baseUrl: string = 'https://dev.azure.com/';
const serviceEndpointResourceAreaId: string = '1814ab31-2f4f-4a9f-8761-f4d77dc5a5d7';
const resourceAreasLocationId: string = 'e81700f7-3be2-46de-8624-2eb35882fcaa';

afterEach(() => {
nock.cleanAll();
});

// Mocks the Location-area OPTIONS discovery call so the resourceAreas GET routes to `_apis/resourceAreas`.
function mockLocationOptions(server: string): void {
nock(server + '_apis/Location')
.options('')
.reply(200, {
value: [{
id: resourceAreasLocationId,
maxVersion: '7.2',
releasedVersion: '7.2',
routeTemplate: '_apis/resourceAreas',
area: 'Location',
resourceName: 'ResourceAreas',
resourceVersion: '1'
}]
});
}

it('returns a ServiceEndpointApi using the resolved resource area URL', async () => {
// Arrange
const resolvedUrl: string = 'https://serviceendpoint.dev.azure.com/';
mockLocationOptions(baseUrl);
nock(baseUrl)
.get('/_apis/resourceAreas')
.reply(200, {
value: [{
id: serviceEndpointResourceAreaId,
name: 'serviceendpoint',
locationUrl: resolvedUrl
}]
});
const myWebApi: WebApi.WebApi = new WebApi.WebApi(baseUrl, WebApi.getBasicHandler('user', 'password'));

// Act
const serviceEndpointApi = await myWebApi.getServiceEndpointApi();

// Assert
assert(serviceEndpointApi, 'ServiceEndpointApi should be created');
assert.equal(serviceEndpointApi.baseUrl, resolvedUrl, 'baseUrl should match the resolved resource area locationUrl');
});

it('falls back to the server URL when resource areas are empty (on-prem)', async () => {
// Arrange
const onPremUrl: string = 'https://my-tfs-instance.host/';
mockLocationOptions(onPremUrl);
nock(onPremUrl)
.get('/_apis/resourceAreas')
.reply(200, { count: 0, value: null });
const myWebApi: WebApi.WebApi = new WebApi.WebApi(onPremUrl, WebApi.getBasicHandler('user', 'password'));

// Act
const serviceEndpointApi = await myWebApi.getServiceEndpointApi();

// Assert
assert(serviceEndpointApi, 'ServiceEndpointApi should be created');
assert.equal(serviceEndpointApi.baseUrl, onPremUrl, 'baseUrl should fall back to the server URL on-prem');
});

it('uses provided handlers instead of the default auth handler', async () => {
// Arrange
const resolvedUrl: string = 'https://serviceendpoint.dev.azure.com/';
mockLocationOptions(baseUrl);
nock(baseUrl)
.get('/_apis/resourceAreas')
.reply(200, {
value: [{
id: serviceEndpointResourceAreaId,
name: 'serviceendpoint',
locationUrl: resolvedUrl
}]
});
const customHandler = WebApi.getBearerHandler('custom-token');
const myWebApi: WebApi.WebApi = new WebApi.WebApi(baseUrl, WebApi.getBasicHandler('user', 'password'));

// Act
const serviceEndpointApi = await myWebApi.getServiceEndpointApi(undefined, [customHandler]);

// Assert
assert(serviceEndpointApi, 'ServiceEndpointApi should be created when custom handlers are provided');
assert.equal(serviceEndpointApi.baseUrl, resolvedUrl, 'baseUrl should still resolve from resource areas');
});

it('uses the provided serverUrl as the fallback baseUrl when resource areas are empty', async () => {
// Arrange
const defaultUrl: string = baseUrl;
const customUrl: string = 'https://custom-tfs.contoso.com/';
// Resource area discovery happens against the WebApi's default serverUrl (via its own LocationsApi),
// but the custom serverUrl is used as the fallback when no resource areas are returned.
mockLocationOptions(defaultUrl);
nock(defaultUrl)
.get('/_apis/resourceAreas')
.reply(200, { count: 0, value: null });
const myWebApi: WebApi.WebApi = new WebApi.WebApi(defaultUrl, WebApi.getBasicHandler('user', 'password'));

// Act
const serviceEndpointApi = await myWebApi.getServiceEndpointApi(customUrl);

// Assert
assert.equal(serviceEndpointApi.baseUrl, customUrl, 'baseUrl should use the custom serverUrl fallback, not the WebApi default');
});
});
});

describe('Auth Handlers Units', function () {
Expand Down