Skip to content

Commit 7308337

Browse files
error handling in securefiles (#487)
* errror handling in securefiles * updating version of securefiles common package * adding test cases * changes in package-lock.json
1 parent 2b32ee8 commit 7308337

File tree

4 files changed

+128
-25
lines changed

4 files changed

+128
-25
lines changed

common-npm-packages/securefiles-common/Tests/L0.ts

Lines changed: 97 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,29 +23,59 @@ const tmAnswers = {
2323
}
2424
}
2525

26-
class AgentAPI {
27-
downloadSecureFile() {
28-
const rs = new Readable();
29-
rs._read = () => {};
30-
rs.push('data');
26+
function createMockStream(options: {
27+
statusCode?: number;
28+
statusMessage?: string;
29+
contentType?: string;
30+
data?: string;
31+
emitError?: Error;
32+
} = {}) {
33+
const {
34+
statusCode = 200,
35+
statusMessage = 'OK',
36+
contentType = 'application/octet-stream',
37+
data = 'data',
38+
emitError,
39+
} = options;
40+
41+
const rs = new Readable();
42+
rs._read = () => {};
43+
(rs as any).statusCode = statusCode;
44+
(rs as any).statusMessage = statusMessage;
45+
(rs as any).headers = { 'content-type': contentType };
46+
47+
if (emitError) {
48+
setTimeout(() => {
49+
rs.emit('error', emitError);
50+
}, 10);
51+
} else {
52+
if (data) rs.push(data);
3153
rs.push(null);
32-
return rs;
3354
}
55+
56+
return rs;
3457
}
3558

36-
class WebApi {
37-
getTaskAgentApi() {
38-
return new Promise((resolve) => {
39-
resolve(new AgentAPI());
40-
});
59+
// Helper function to create a complete node API mock
60+
function createNodeApiMock(streamOptions?: Parameters<typeof createMockStream>[0]) {
61+
class MockAgentAPI {
62+
downloadSecureFile() {
63+
return Promise.resolve(createMockStream(streamOptions));
64+
}
4165
}
42-
}
4366

44-
export const nodeapiMock = {
45-
WebApi,
46-
getPersonalAccessTokenHandler() {
47-
return {} as IRequestHandler;
67+
class MockWebApi {
68+
getTaskAgentApi() {
69+
return Promise.resolve(new MockAgentAPI());
70+
}
4871
}
72+
73+
return {
74+
WebApi: MockWebApi,
75+
getPersonalAccessTokenHandler() {
76+
return {} as IRequestHandler;
77+
}
78+
};
4979
}
5080

5181
export const fsMock = {
@@ -105,6 +135,12 @@ describe("securefiles-common package suites", function() {
105135
});
106136

107137
it("Check downloadSecureFile", async() => {
138+
const nodeapiMock = createNodeApiMock({
139+
statusCode: 200,
140+
statusMessage: 'OK',
141+
contentType: 'application/octet-stream'
142+
});
143+
108144
registerMock("azure-devops-node-api", nodeapiMock);
109145
registerMock("fs", fsMock);
110146
const secureFiles = require("../securefiles-common");
@@ -127,4 +163,49 @@ describe("securefiles-common package suites", function() {
127163
const pseudoResolvedPath = tlClone.resolve(tlClone.getVariable("Agent.TempDirectory"), tlClone.getSecureFileName(secureFileId));
128164
strictEqual(resolvedPath, pseudoResolvedPath, `Resolved path "${resolvedPath}" should be equal to "${pseudoResolvedPath}"`);
129165
});
166+
167+
it("Should handle HTTP error responses", async() => {
168+
const errorNodeapiMock = createNodeApiMock({
169+
statusCode: 500,
170+
statusMessage: 'Internal Server Error',
171+
contentType: 'application/json',
172+
data: '',
173+
});
174+
175+
registerMock("azure-devops-node-api", errorNodeapiMock);
176+
registerMock("fs", fsMock);
177+
const secureFiles = require("../securefiles-common");
178+
const secureFileHelpers = new secureFiles.SecureFileHelpers();
179+
180+
try {
181+
await secureFileHelpers.downloadSecureFile(secureFileId);
182+
throw new Error("Expected error was not thrown");
183+
} catch (error) {
184+
strictEqual(error.message.includes("HTTP 500"), true, "Should contain HTTP error status");
185+
strictEqual(error.message.includes("Internal Server Error"), true, "Should contain HTTP error message");
186+
}
187+
});
188+
189+
it("Should handle stream errors during download", async() => {
190+
const streamErrorNodeapiMock = createNodeApiMock({
191+
statusCode: 200,
192+
statusMessage: 'OK',
193+
contentType: 'application/octet-stream',
194+
emitError: new Error('Network connection lost')
195+
});
196+
197+
registerMock("azure-devops-node-api", streamErrorNodeapiMock);
198+
registerMock("fs", fsMock);
199+
200+
const secureFiles = require("../securefiles-common");
201+
const secureFileHelpers = new secureFiles.SecureFileHelpers();
202+
203+
try {
204+
await secureFileHelpers.downloadSecureFile(secureFileId);
205+
throw new Error("Expected error was not thrown");
206+
} catch (error) {
207+
strictEqual(error.message.includes("Failed to download secure file"), true, "Should handle stream errors");
208+
strictEqual(error.message.includes("Network connection lost"), true, "Should include original error message");
209+
}
210+
});
130211
});

common-npm-packages/securefiles-common/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.

common-npm-packages/securefiles-common/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "azure-pipelines-tasks-securefiles-common",
3-
"version": "2.262.0",
3+
"version": "2.263.0",
44
"description": "Azure Pipelines tasks SecureFiles Common",
55
"main": "securefiles-common.js",
66
"scripts": {

common-npm-packages/securefiles-common/securefiles-common.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,26 +34,48 @@ export class SecureFileHelpers {
3434
* @param secureFileId
3535
*/
3636
async downloadSecureFile(secureFileId: string): Promise<string> {
37-
const tempDownloadPath: string = this.getSecureFileTempDownloadPath(secureFileId);
3837

38+
const tempDownloadPath: string = this.getSecureFileTempDownloadPath(secureFileId);
3939
tl.debug('Downloading secure file contents to: ' + tempDownloadPath);
40-
const file: NodeJS.WritableStream = fs.createWriteStream(tempDownloadPath);
4140

4241
const agentApi = await this.serverConnection.getTaskAgentApi();
43-
4442
const ticket = tl.getSecureFileTicket(secureFileId);
4543
if (!ticket) {
4644
// Workaround bug #7491. tl.loc only works if the consuming tasks define the resource string.
4745
throw new Error(`Download ticket for SecureFileId ${secureFileId} not found.`);
4846
}
4947

50-
const stream = (await agentApi.downloadSecureFile(
51-
tl.getVariable('SYSTEM.TEAMPROJECT'), secureFileId, ticket, false)).pipe(file);
48+
tl.debug(`Starting secure file download for SecureFileId: ${secureFileId}`);
49+
const response = await agentApi.downloadSecureFile(tl.getVariable('SYSTEM.TEAMPROJECT'), secureFileId, ticket, false);
5250

53-
const defer = Q.defer();
51+
const defer = Q.defer<void>();
52+
response.on('error', (err: Error) => {
53+
defer.reject(new Error(`Failed to download secure file. ${err.message}`));
54+
});
55+
const httpResponse = response as any;
56+
tl.debug(`HTTP Status: ${httpResponse.statusCode} ${httpResponse.statusMessage}`);
57+
tl.debug(`Content-Type: ${httpResponse.headers['content-type'] || 'unknown'}`);
58+
59+
if (httpResponse.statusCode && httpResponse.statusCode != 200) {
60+
let errorBody = '';
61+
httpResponse.on('data', (chunk: Buffer) => {
62+
errorBody += chunk.toString();
63+
});
64+
httpResponse.on('end', () => {
65+
defer.reject(new Error(`Failed to download secure file. HTTP ${httpResponse.statusCode}: ${httpResponse.statusMessage}. Error content: ${errorBody}`));
66+
});
67+
}
68+
69+
70+
const file: NodeJS.WritableStream = fs.createWriteStream(tempDownloadPath);
71+
const stream = response.pipe(file);
72+
5473
stream.on('finish', () => {
5574
defer.resolve();
5675
});
76+
stream.on('error', (err: Error) => {
77+
defer.reject(new Error(`Error writing secure file to disk. ${err.message}`));
78+
});
5779
await defer.promise;
5880
tl.debug('Downloaded secure file contents to: ' + tempDownloadPath);
5981
return tempDownloadPath;

0 commit comments

Comments
 (0)