Skip to content

Commit 1cd5f14

Browse files
Add ServiceEndpointApi integration to WebApi (#671)
* Add ServiceEndpointApi integration to WebApi - Imported ServiceEndpointApi module. - Updated _resourceAreas type to allow undefined. - Implemented getServiceEndpointApi method to retrieve ServiceEndpointApi instance. - Cleaned up whitespace in the encryption key handling section. * Bump version to 15.3.0 in package.json * Bump version to 16.0.0 in package.json and package-lock.json * Update package-lock.json to version 16.0.0 and add unit tests for ServiceEndpointApi
1 parent ed19aa8 commit 1cd5f14

7 files changed

Lines changed: 1533 additions & 7 deletions

File tree

api/ServiceEndpointApi.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
import VsoBaseInterfaces = require('./interfaces/common/VsoBaseInterfaces');
5+
import serviceendpointbasem = require('./ServiceEndpointApiBase');
6+
7+
export interface IServiceEndpointApi extends serviceendpointbasem.IServiceEndpointApiBase {
8+
}
9+
10+
export class ServiceEndpointApi extends serviceendpointbasem.ServiceEndpointApiBase implements IServiceEndpointApi {
11+
constructor(baseUrl: string, handlers: VsoBaseInterfaces.IRequestHandler[], options?: VsoBaseInterfaces.IRequestOptions, userAgent?: string) {
12+
super(baseUrl, handlers, options, userAgent);
13+
}
14+
}

api/ServiceEndpointApiBase.ts

Lines changed: 1395 additions & 0 deletions
Large diffs are not rendered by default.

api/WebApi.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import profilem = require('./ProfileApi');
2020
import projectm = require('./ProjectAnalysisApi');
2121
import releasem = require('./ReleaseApi');
2222
import securityrolesm = require('./SecurityRolesApi');
23+
import serviceendpointm = require('./ServiceEndpointApi');
2324
import taskagentm = require('./TaskAgentApi');
2425
import taskm = require('./TaskApi');
2526
import testm = require('./TestApi');
@@ -314,6 +315,12 @@ export class WebApi {
314315
return new securityrolesm.SecurityRolesApi(serverUrl, handlers, this.options, this.userAgent);
315316
}
316317

318+
public async getServiceEndpointApi(serverUrl?: string, handlers?: VsoBaseInterfaces.IRequestHandler[]): Promise<serviceendpointm.IServiceEndpointApi> {
319+
serverUrl = await this._getResourceAreaUrl(serverUrl || this.serverUrl, "1814ab31-2f4f-4a9f-8761-f4d77dc5a5d7");
320+
handlers = handlers || [this.authHandler];
321+
return new serviceendpointm.ServiceEndpointApi(serverUrl, handlers, this.options, this.userAgent);
322+
}
323+
317324
public async getReleaseApi(serverUrl?: string, handlers?: VsoBaseInterfaces.IRequestHandler[]): Promise<releasem.IReleaseApi> {
318325
// TODO: Load RESOURCE_AREA_ID correctly.
319326
serverUrl = await this._getResourceAreaUrl(serverUrl || this.serverUrl, "efc2f575-36ef-48e9-b672-0c6fb4a48ac5");
@@ -477,16 +484,16 @@ export class WebApi {
477484

478485
let keyFile = Buffer.from(lookupInfo[0], 'base64').toString('utf8');
479486
let keyAndIv = fs.readFileSync(keyFile, 'utf8');
480-
487+
481488
let [keyBase64, ivBase64] = keyAndIv.split(':', 2);
482-
489+
483490
if (!keyBase64 || !ivBase64) {
484491
throw new Error(
485492
'Invalid encryption key format. Expected "key:iv" format from azure-pipelines-task-lib 5.2.4+. ' +
486493
'This version of azure-devops-node-api (15.2.0+) is not compatible with task-lib <5.2.4.'
487494
);
488495
}
489-
496+
490497
let encryptKey = Buffer.from(keyBase64, 'base64');
491498
let iv = Buffer.from(ivBase64, 'base64');
492499

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "azure-devops-node-api",
33
"description": "Node client for Azure DevOps and TFS REST APIs",
4-
"version": "15.2.0",
4+
"version": "16.0.0",
55
"main": "./WebApi.js",
66
"types": "./WebApi.d.ts",
77
"scripts": {

test/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/units/tests.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,116 @@ describe('WebApi Units', function () {
467467
assert.equal(myWebApi.isNoProxyHost('https://my-tfs-instance.host/myproject'), true);
468468
assert.equal(myWebApi.isNoProxyHost('https://my-other-tfs-instance.host/myproject'), false);
469469
});
470+
471+
describe('getServiceEndpointApi', function () {
472+
const baseUrl: string = 'https://dev.azure.com/';
473+
const serviceEndpointResourceAreaId: string = '1814ab31-2f4f-4a9f-8761-f4d77dc5a5d7';
474+
const resourceAreasLocationId: string = 'e81700f7-3be2-46de-8624-2eb35882fcaa';
475+
476+
afterEach(() => {
477+
nock.cleanAll();
478+
});
479+
480+
// Mocks the Location-area OPTIONS discovery call so the resourceAreas GET routes to `_apis/resourceAreas`.
481+
function mockLocationOptions(server: string): void {
482+
nock(server + '_apis/Location')
483+
.options('')
484+
.reply(200, {
485+
value: [{
486+
id: resourceAreasLocationId,
487+
maxVersion: '7.2',
488+
releasedVersion: '7.2',
489+
routeTemplate: '_apis/resourceAreas',
490+
area: 'Location',
491+
resourceName: 'ResourceAreas',
492+
resourceVersion: '1'
493+
}]
494+
});
495+
}
496+
497+
it('returns a ServiceEndpointApi using the resolved resource area URL', async () => {
498+
// Arrange
499+
const resolvedUrl: string = 'https://serviceendpoint.dev.azure.com/';
500+
mockLocationOptions(baseUrl);
501+
nock(baseUrl)
502+
.get('/_apis/resourceAreas')
503+
.reply(200, {
504+
value: [{
505+
id: serviceEndpointResourceAreaId,
506+
name: 'serviceendpoint',
507+
locationUrl: resolvedUrl
508+
}]
509+
});
510+
const myWebApi: WebApi.WebApi = new WebApi.WebApi(baseUrl, WebApi.getBasicHandler('user', 'password'));
511+
512+
// Act
513+
const serviceEndpointApi = await myWebApi.getServiceEndpointApi();
514+
515+
// Assert
516+
assert(serviceEndpointApi, 'ServiceEndpointApi should be created');
517+
assert.equal(serviceEndpointApi.baseUrl, resolvedUrl, 'baseUrl should match the resolved resource area locationUrl');
518+
});
519+
520+
it('falls back to the server URL when resource areas are empty (on-prem)', async () => {
521+
// Arrange
522+
const onPremUrl: string = 'https://my-tfs-instance.host/';
523+
mockLocationOptions(onPremUrl);
524+
nock(onPremUrl)
525+
.get('/_apis/resourceAreas')
526+
.reply(200, { count: 0, value: null });
527+
const myWebApi: WebApi.WebApi = new WebApi.WebApi(onPremUrl, WebApi.getBasicHandler('user', 'password'));
528+
529+
// Act
530+
const serviceEndpointApi = await myWebApi.getServiceEndpointApi();
531+
532+
// Assert
533+
assert(serviceEndpointApi, 'ServiceEndpointApi should be created');
534+
assert.equal(serviceEndpointApi.baseUrl, onPremUrl, 'baseUrl should fall back to the server URL on-prem');
535+
});
536+
537+
it('uses provided handlers instead of the default auth handler', async () => {
538+
// Arrange
539+
const resolvedUrl: string = 'https://serviceendpoint.dev.azure.com/';
540+
mockLocationOptions(baseUrl);
541+
nock(baseUrl)
542+
.get('/_apis/resourceAreas')
543+
.reply(200, {
544+
value: [{
545+
id: serviceEndpointResourceAreaId,
546+
name: 'serviceendpoint',
547+
locationUrl: resolvedUrl
548+
}]
549+
});
550+
const customHandler = WebApi.getBearerHandler('custom-token');
551+
const myWebApi: WebApi.WebApi = new WebApi.WebApi(baseUrl, WebApi.getBasicHandler('user', 'password'));
552+
553+
// Act
554+
const serviceEndpointApi = await myWebApi.getServiceEndpointApi(undefined, [customHandler]);
555+
556+
// Assert
557+
assert(serviceEndpointApi, 'ServiceEndpointApi should be created when custom handlers are provided');
558+
assert.equal(serviceEndpointApi.baseUrl, resolvedUrl, 'baseUrl should still resolve from resource areas');
559+
});
560+
561+
it('uses the provided serverUrl as the fallback baseUrl when resource areas are empty', async () => {
562+
// Arrange
563+
const defaultUrl: string = baseUrl;
564+
const customUrl: string = 'https://custom-tfs.contoso.com/';
565+
// Resource area discovery happens against the WebApi's default serverUrl (via its own LocationsApi),
566+
// but the custom serverUrl is used as the fallback when no resource areas are returned.
567+
mockLocationOptions(defaultUrl);
568+
nock(defaultUrl)
569+
.get('/_apis/resourceAreas')
570+
.reply(200, { count: 0, value: null });
571+
const myWebApi: WebApi.WebApi = new WebApi.WebApi(defaultUrl, WebApi.getBasicHandler('user', 'password'));
572+
573+
// Act
574+
const serviceEndpointApi = await myWebApi.getServiceEndpointApi(customUrl);
575+
576+
// Assert
577+
assert.equal(serviceEndpointApi.baseUrl, customUrl, 'baseUrl should use the custom serverUrl fallback, not the WebApi default');
578+
});
579+
});
470580
});
471581

472582
describe('Auth Handlers Units', function () {

0 commit comments

Comments
 (0)