Skip to content

Commit 1912476

Browse files
committed
Minor refactoring for flow
1 parent e0cdbe0 commit 1912476

File tree

11 files changed

+323
-53
lines changed

11 files changed

+323
-53
lines changed

Tasks/UniversalPackagesV1/Strings/resources.resjson/en-US/resources.resjson

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,13 @@
3030
"loc.messages.Debug_OrgScopedFeed": "Organization-scoped feed detected - Feed: %s",
3131
"loc.messages.Debug_ParsedFeedInfo": "Parsed feed info - Service URI: %s, Feed: %s",
3232
"loc.messages.Debug_ProjectScopedFeed": "Project-scoped feed detected - Project: %s, Feed: %s",
33+
"loc.messages.Debug_ProvenanceApiUrl": "Creating provenance session at: %s",
34+
"loc.messages.Debug_ProvenanceDisabled": "Provenance disabled by Packaging.SavePublishMetadata variable",
35+
"loc.messages.Debug_ProvenanceResponseStatus": "Provenance API response status: %s",
3336
"loc.messages.Debug_PublishOperation": "Publish operation - Package: %s@%s, Source: %s",
3437
"loc.messages.Debug_Publishing": "Publishing package: %s, version: %s using feed id: %s, project: %s",
3538
"loc.messages.Debug_RetrievingArtifactToolUri": "Retrieving artifact tool from: %s",
36-
"loc.messages.Debug_SettingUpAuth": "Setting up authentication",
39+
"loc.messages.Debug_ResolvedPackagingUrl": "Resolved packaging service URL: %s",
3740
"loc.messages.Debug_TelemetryInitFailed": "Unable to log Universal Packages task init telemetry. Err:( %s )",
3841
"loc.messages.Debug_TelemetryResultFailed": "Unable to log telemetry result. Err:( %s )",
3942
"loc.messages.Debug_UsingArtifactToolDownload": "Using artifact tool to download the package",
@@ -60,7 +63,7 @@
6063
"loc.messages.Success_PackagesPublished": "Packages were published successfully",
6164
"loc.messages.Warning_BuildIdentityFeedHint": "Check feed '%s' permissions: %s",
6265
"loc.messages.Warning_BuildIdentityOperationHint": "Operation failed using identity '%s' (%s).",
63-
"loc.messages.Warning_FailedToGetProvenanceSession": "Failed to get provenance session for publishing: %s. Continuing with direct feed publish.",
66+
"loc.messages.Warning_ProvenanceSessionFailed": "Provenance session creation failed: %s",
6467
"loc.messages.Warning_WifAuthFailed": "WIF authentication failed for service connection %s, falling back to build service token: %s",
6568
"loc.messages.Warning_WifAuthNoToken": "WIF authentication returned no token for service connection %s, falling back to build service token"
6669
}

Tasks/UniversalPackagesV1/Tests/L0.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ describe('UniversalPackages Suite', function () {
141141
...getDefaultEnvVars(),
142142
'INPUT_COMMAND': 'publish',
143143
'INPUT_FEED': TEST_CONSTANTS.FEED_NAME,
144+
'INPUT_ORGANIZATION': TEST_CONSTANTS.ORGANIZATION_NAME,
145+
'INPUT_ADOSERVICECONNECTION': TEST_CONSTANTS.SERVICE_CONNECTION_NAME,
144146
'PROVENANCE_PROVIDES_SESSION_ID': 'true',
145147
'EXPECTED_COMMAND_STRING': expectedCommandString
146148
});
@@ -177,6 +179,8 @@ describe('UniversalPackages Suite', function () {
177179
...getDefaultEnvVars(),
178180
'INPUT_COMMAND': 'publish',
179181
'INPUT_FEED': TEST_CONSTANTS.PROJECT_SCOPED_FEED_NAME,
182+
'INPUT_ORGANIZATION': TEST_CONSTANTS.ORGANIZATION_NAME,
183+
'INPUT_ADOSERVICECONNECTION': TEST_CONSTANTS.SERVICE_CONNECTION_NAME,
180184
'PROVENANCE_PROVIDES_SESSION_ID': 'true',
181185
'EXPECTED_COMMAND_STRING': expectedCommandString
182186
});
@@ -198,6 +202,7 @@ describe('UniversalPackages Suite', function () {
198202
let tr = await runTestWithEnv('./testRunner.js', {
199203
...getDefaultEnvVars(),
200204
'INPUT_COMMAND': 'download', // download and publish use the same path for auth
205+
'INPUT_ORGANIZATION': TEST_CONSTANTS.ORGANIZATION_NAME,
201206
'INPUT_ADOSERVICECONNECTION': TEST_CONSTANTS.SERVICE_CONNECTION_NAME,
202207
'WIF_AUTH_BEHAVIOR': 'success',
203208
'EXPECTED_COMMAND_STRING': expectedCommandString
@@ -216,6 +221,7 @@ describe('UniversalPackages Suite', function () {
216221
let tr = await runTestWithEnv('./testRunner.js', {
217222
...getDefaultEnvVars(),
218223
'INPUT_COMMAND': 'download',
224+
'INPUT_ORGANIZATION': TEST_CONSTANTS.ORGANIZATION_NAME,
219225
'INPUT_ADOSERVICECONNECTION': TEST_CONSTANTS.SERVICE_CONNECTION_NAME,
220226
'WIF_AUTH_BEHAVIOR': 'throws',
221227
'SYSTEM_TOKEN_AVAILABLE': 'true',
@@ -235,6 +241,7 @@ describe('UniversalPackages Suite', function () {
235241
let tr = await runTestWithEnv('./testRunner.js', {
236242
...getDefaultEnvVars(),
237243
'INPUT_COMMAND': 'download',
244+
'INPUT_ORGANIZATION': TEST_CONSTANTS.ORGANIZATION_NAME,
238245
'INPUT_ADOSERVICECONNECTION': TEST_CONSTANTS.SERVICE_CONNECTION_NAME,
239246
'WIF_AUTH_BEHAVIOR': 'returns-null',
240247
'SYSTEM_TOKEN_AVAILABLE': 'true',
@@ -248,6 +255,46 @@ describe('UniversalPackages Suite', function () {
248255
expectedMessage: TEST_CONSTANTS.SUCCESS_OUTPUT
249256
});
250257
});
258+
259+
it('uses pipeline identity when no service connection is specified', async function() {
260+
const expectedCommandString = buildCommandString({ command: 'download', feed: TEST_CONSTANTS.FEED_NAME });
261+
let tr = await runTestWithEnv('./testRunner.js', {
262+
...getDefaultEnvVars(),
263+
'INPUT_COMMAND': 'download',
264+
'EXPECTED_COMMAND_STRING': expectedCommandString
265+
});
266+
assertArtifactToolCommand({
267+
tr,
268+
command: 'download',
269+
shouldSucceed: true,
270+
expectedCommandString,
271+
expectedMessage: TEST_CONSTANTS.SUCCESS_OUTPUT
272+
});
273+
});
274+
275+
it('uses cross-org service URL when organization is specified with service connection', async function() {
276+
const crossOrgCommandString = buildCommandString({
277+
command: 'download',
278+
feed: TEST_CONSTANTS.FEED_NAME,
279+
serviceUrl: TEST_CONSTANTS.CROSS_ORG_SERVICE_URL
280+
});
281+
let tr = await runTestWithEnv('./testRunner.js', {
282+
...getDefaultEnvVars(),
283+
'INPUT_COMMAND': 'download',
284+
'INPUT_ORGANIZATION': 'other-org',
285+
'INPUT_ADOSERVICECONNECTION': TEST_CONSTANTS.SERVICE_CONNECTION_NAME,
286+
'WIF_AUTH_BEHAVIOR': 'success',
287+
'MOCK_SERVICE_URL': TEST_CONSTANTS.CROSS_ORG_SERVICE_URL,
288+
'EXPECTED_COMMAND_STRING': crossOrgCommandString
289+
});
290+
assertArtifactToolCommand({
291+
tr,
292+
command: 'download',
293+
shouldSucceed: true,
294+
expectedCommandString: crossOrgCommandString,
295+
expectedMessage: TEST_CONSTANTS.SUCCESS_OUTPUT
296+
});
297+
});
251298
});
252299

253300
describe('Error Handling', function() {
@@ -258,6 +305,7 @@ describe('UniversalPackages Suite', function () {
258305
let tr = await runTestWithEnv('./testRunner.js', {
259306
...getDefaultEnvVars(),
260307
'INPUT_COMMAND': 'download',
308+
'INPUT_ORGANIZATION': TEST_CONSTANTS.ORGANIZATION_NAME,
261309
'INPUT_ADOSERVICECONNECTION': TEST_CONSTANTS.SERVICE_CONNECTION_NAME,
262310
'WIF_AUTH_BEHAVIOR': 'returns-null',
263311
'SYSTEM_TOKEN_AVAILABLE': 'false',
@@ -272,6 +320,7 @@ describe('UniversalPackages Suite', function () {
272320
let tr = await runTestWithEnv('./testRunner.js', {
273321
...getDefaultEnvVars(),
274322
'INPUT_COMMAND': 'publish',
323+
'INPUT_ORGANIZATION': TEST_CONSTANTS.ORGANIZATION_NAME,
275324
'INPUT_ADOSERVICECONNECTION': TEST_CONSTANTS.SERVICE_CONNECTION_NAME,
276325
'WIF_AUTH_BEHAVIOR': 'returns-null',
277326
'SYSTEM_TOKEN_AVAILABLE': 'false',
@@ -281,6 +330,19 @@ describe('UniversalPackages Suite', function () {
281330
assertTaskFailedBeforeToolExecution(tr, tl.loc('Error_AuthenticationFailed'));
282331
});
283332

333+
it('fails when organization is not specified with service connection', async function() {
334+
const expectedCommandString = buildCommandString({ command: 'download', feed: TEST_CONSTANTS.FEED_NAME });
335+
let tr = await runTestWithEnv('./testRunner.js', {
336+
...getDefaultEnvVars(),
337+
'INPUT_COMMAND': 'download',
338+
'INPUT_ADOSERVICECONNECTION': TEST_CONSTANTS.SERVICE_CONNECTION_NAME,
339+
'WIF_AUTH_BEHAVIOR': 'success',
340+
'EXPECTED_COMMAND_STRING': expectedCommandString
341+
});
342+
343+
assertTaskFailedBeforeToolExecution(tr, tl.loc('Error_OrganizationRequired'));
344+
});
345+
284346
it('fails when running against on-premises server', async function() {
285347
let tr = await runTestWithEnv('./testRunner.js', {
286348
...getDefaultEnvVars(),

Tasks/UniversalPackagesV1/Tests/TestConstants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ export const TEST_CONSTANTS = {
55
PACKAGE_NAME: 'TestPackage',
66
PACKAGE_VERSION: '1.0.0',
77
DOWNLOAD_PATH: 'c:\\temp',
8+
ORGANIZATION_NAME: 'example',
89
SERVICE_URL: 'https://dev.azure.com/example',
10+
CROSS_ORG_SERVICE_URL: 'https://dev.azure.com/other-org',
911
ARTIFACT_TOOL_PATH: 'c:\\mock\\location\\ArtifactTool.exe',
1012

1113
// Test output messages

Tasks/UniversalPackagesV1/Tests/TestHelpers.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ export function buildCommandString(params: {
2424
feed: string;
2525
projectName?: string;
2626
description?: string;
27+
serviceUrl?: string;
2728
}): string {
28-
const { command, feed, projectName, description } = params;
29-
let commandString = `${UniversalMockHelper.getArtifactToolPath()} universal ${command} --feed ${feed} --service ${TEST_CONSTANTS.SERVICE_URL} --package-name ${TEST_CONSTANTS.PACKAGE_NAME} --package-version ${TEST_CONSTANTS.PACKAGE_VERSION} --path ${TEST_CONSTANTS.DOWNLOAD_PATH} --patvar UNIVERSAL_AUTH_TOKEN --verbosity verbose`;
29+
const { command, feed, projectName, description, serviceUrl = TEST_CONSTANTS.SERVICE_URL } = params;
30+
let commandString = `${UniversalMockHelper.getArtifactToolPath()} universal ${command} --feed ${feed} --service ${serviceUrl} --package-name ${TEST_CONSTANTS.PACKAGE_NAME} --package-version ${TEST_CONSTANTS.PACKAGE_VERSION} --path ${TEST_CONSTANTS.DOWNLOAD_PATH} --patvar UNIVERSAL_AUTH_TOKEN --verbosity verbose`;
3031

3132
if (projectName) {
3233
commandString += ` --project ${projectName}`;

Tasks/UniversalPackagesV1/Tests/UniversalMockHelper.ts

Lines changed: 143 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export interface MockConfig {
1010
inputs: {
1111
command: string;
1212
directory: string;
13-
organization: string;
13+
organization?: string;
1414
feed: string;
1515
packageName: string;
1616
packageVersion: string;
@@ -25,6 +25,8 @@ export interface MockConfig {
2525
wifAuthBehavior?: string;
2626
systemTokenAvailable: boolean;
2727
providesSessionId?: string;
28+
serviceUrl: string;
29+
feedValidationBehavior?: string;
2830
}
2931

3032
export class UniversalMockHelper {
@@ -55,6 +57,8 @@ export class UniversalMockHelper {
5557
clientMock.registerClientToolRunnerMock(tmr);
5658
pkgMock.registerLocationHelpersMock(tmr);
5759
this.registerConnectionDataUtilsMock();
60+
this.registerLocationUtilitiesMock();
61+
this.registerRetryUtilitiesMock();
5862
this.registerAuthenticationMocks();
5963

6064
// Only register provenance mock if test explicitly sets providesSessionId
@@ -114,16 +118,103 @@ export class UniversalMockHelper {
114118
this.tmr.registerMock('azure-pipelines-tasks-artifacts-common/connectionDataUtils', connectionDataMock);
115119
}
116120

121+
private registerLocationUtilitiesMock() {
122+
const locationUtilitiesMock = {
123+
getFeedUriFromBaseServiceUri: async (serviceUri: string, accessToken: string) => {
124+
// Return the packaging API URL based on the service URI
125+
return `${serviceUri}/_apis/packaging`;
126+
}
127+
};
128+
129+
this.tmr.registerMock('azure-pipelines-tasks-packaging-common/locationUtilities', locationUtilitiesMock);
130+
}
131+
132+
private registerRetryUtilitiesMock() {
133+
const retryUtilitiesMock = {
134+
retryOnException: async <T>(operation: () => Promise<T>, maxRetries: number, delayMs: number): Promise<T> => {
135+
// For tests, just execute once without retries
136+
return await operation();
137+
}
138+
};
139+
140+
this.tmr.registerMock('azure-pipelines-tasks-artifacts-common/retryUtils', retryUtilitiesMock);
141+
}
142+
117143
private registerProvenanceHelperMock() {
118144
const sessionId = this.provenanceSessionId;
119145

120146
const provenanceMock = {
121147
ProvenanceHelper: {
122-
GetSessionId: async () => sessionId
148+
CreateSessionRequest: (feedId: string) => ({
149+
feed: feedId,
150+
source: "InternalBuild",
151+
data: {
152+
"Build.BuildId": "12345"
153+
}
154+
})
123155
}
124156
};
125157

126158
this.tmr.registerMock('azure-pipelines-tasks-packaging-common/provenance', provenanceMock);
159+
160+
// Mock the REST client for provenance API calls
161+
const restClientMock = {
162+
RestClient: class MockRestClient {
163+
constructor(userAgent: string, baseUrl?: string, handlers?: any[], options?: any) {}
164+
165+
async create<T>(resource: string, body: any, options?: any): Promise<{result: T, statusCode: number}> {
166+
if (sessionId) {
167+
return { result: { sessionId: sessionId } as T, statusCode: 200 };
168+
}
169+
// Return null result if session creation failed
170+
return { result: null as T, statusCode: 404 };
171+
}
172+
}
173+
};
174+
175+
this.tmr.registerMock('typed-rest-client/RestClient', restClientMock);
176+
177+
// Mock ClientApiBase infrastructure for ProvenanceApi
178+
const clientApiBasesMock = {
179+
ClientApiBase: class MockClientApiBase {
180+
public baseUrl: string;
181+
public rest: any;
182+
public vsoClient: any;
183+
184+
constructor(baseUrl: string, handlers: any[], userAgent: string, options?: any) {
185+
this.baseUrl = baseUrl;
186+
this.rest = new restClientMock.RestClient(userAgent, baseUrl, handlers, options);
187+
this.vsoClient = {
188+
getVersioningData: async (apiVersion: string, area: string, locationId: string, routeValues?: any) => {
189+
// Mock successful versioning data response
190+
const protocol = routeValues?.protocol || 'upack';
191+
const project = routeValues?.project;
192+
const projectSegment = project ? `/${project}` : '';
193+
return {
194+
apiVersion: apiVersion,
195+
requestUrl: `${baseUrl}${projectSegment}/_apis/Provenance/${protocol}/CreateSession?api-version=${apiVersion}`
196+
};
197+
}
198+
};
199+
}
200+
201+
createRequestOptions(contentType: string, apiVersion: string): any {
202+
return {
203+
acceptHeader: contentType,
204+
additionalHeaders: {
205+
'Content-Type': contentType,
206+
'X-TFS-FedAuthRedirect': 'Suppress'
207+
}
208+
};
209+
}
210+
211+
formatResponse(data: any, responseTypeMetadata: any, isCollection: boolean): any {
212+
return data;
213+
}
214+
}
215+
};
216+
217+
this.tmr.registerMock('azure-devops-node-api/ClientApiBases', clientApiBasesMock);
127218
}
128219

129220
private registerAuthenticationMocks() {
@@ -142,7 +233,8 @@ export class UniversalMockHelper {
142233
}
143234
};
144235

145-
// Mock getSystemAccessToken
236+
// Mock getSystemAccessToken and getWebApiWithProxy for feed validation and provenance
237+
const provenanceSessionId = this.provenanceSessionId;
146238
const webapiMock = {
147239
getSystemAccessToken: () => {
148240
if (this.config.systemTokenAvailable) {
@@ -151,10 +243,57 @@ export class UniversalMockHelper {
151243
return undefined;
152244
},
153245
getWebApiWithProxy: (serviceUri: string, accessToken: string) => {
246+
if (this.config.feedValidationBehavior === 'fail') {
247+
return {
248+
serverUrl: serviceUri,
249+
authHandler: {},
250+
options: {},
251+
vsoClient: {
252+
getVersioningData: async () => {
253+
throw new Error('Feed validation failed: 401 Unauthorized');
254+
}
255+
},
256+
getLocationsApi: async () => ({
257+
getResourceArea: async (areaId: string) => {
258+
throw new Error('Feed validation failed: 401 Unauthorized');
259+
}
260+
})
261+
};
262+
}
154263
return {
155264
serverUrl: serviceUri,
156265
authHandler: {},
157-
options: {}
266+
options: {},
267+
rest: {
268+
create: async <T>(resource: string, body: any, options?: any): Promise<{result: T, statusCode: number}> => {
269+
if (provenanceSessionId) {
270+
return { result: { sessionId: provenanceSessionId } as T, statusCode: 200 };
271+
}
272+
return { result: null as T, statusCode: 404 };
273+
}
274+
},
275+
vsoClient: {
276+
getVersioningData: async (apiVersion?: string, area?: string, locationId?: string, routeValues?: any) => {
277+
// For provenance API calls
278+
if (area === 'Provenance') {
279+
const protocol = routeValues?.protocol || 'upack';
280+
const project = routeValues?.project;
281+
const projectSegment = project ? `/${project}` : '';
282+
return {
283+
apiVersion: apiVersion,
284+
requestUrl: `${serviceUri}${projectSegment}/_apis/Provenance/${protocol}/CreateSession?api-version=${apiVersion}`
285+
};
286+
}
287+
// Default for feed validation
288+
return { requestUrl: `${serviceUri}/_apis/packaging/feeds` };
289+
}
290+
},
291+
getLocationsApi: async () => ({
292+
getResourceArea: async (areaId: string) => {
293+
// Mock the UPack area ID returning a packaging service URL
294+
return { locationUrl: `${serviceUri}/_apis/packaging` };
295+
}
296+
})
158297
};
159298
}
160299
};

0 commit comments

Comments
 (0)