appcenter-cli
Version:
Command line tool for Visual Studio App Center
475 lines (388 loc) • 19.5 kB
text/typescript
import * as JsZip from "jszip";
import { expect, use } from "chai";
import * as Fs from "fs";
import * as Pfs from "../../../../src/util/misc/promisfied-fs";
import * as _ from "lodash";
import * as Nock from "nock";
import * as Path from "path";
import * as Sinon from "sinon";
import * as Temp from "temp";
import * as ChaiAsPromised from "chai-as-promised";
import * as MockRequire from "mock-require";
use(ChaiAsPromised);
// Mocking AzureBlobUploadHelper
import AzureBlobUploadHelperMock from "./lib/azure-blob-uploader-helper-mock";
MockRequire("../../../../src/commands/crashes/lib/azure-blob-upload-helper", {
default: AzureBlobUploadHelperMock
});
import UploadSymbolsCommand from "../../../../src/commands/crashes/upload-symbols";
MockRequire.stopAll();
import { CommandArgs, CommandResult } from "../../../../src/util/commandline";
Temp.track();
describe("upload-symbols command", () => {
const fakeAppOwner = "fakeAppOwner";
const fakeAppName = "fakeAppName";
const fakeAppIdentifier = `${fakeAppOwner}/${fakeAppName}`;
const fakeToken = "c1o3d3e7";
const fakeSymbolUploadingId = "fakeSymbolUploadingId";
const fakeUploadUrl = "/upload/here";
/* tslint:disable-next-line:no-http-string */
const fakeHost = "http://localhost:1700";
const fakeFullUploadUrl = fakeHost + fakeUploadUrl;
const symbolsFile1Name = "symbolsFile1";
const symbolsFile2Name = "symbolsFile2";
const randomFileName = "randomFile";
const dSymFolder1Name = "dsymFolder1.dSYM";
const dSymFolder2Name = "dsymFolder2.dSYM";
const dSymParentFolderName = "hereBeTheDsyms";
const xcArchiveFolderName = "test.xcarchive";
const mappingsFileName = "testMappingsFile.map";
const symbolsFileContent = "Hello World!";
const randomFileContent = "Random File";
const mappingsFileContent = "Mappings";
const alternativeMappingsFileContent = "Alternative Mappings";
let tmpFolderPath: string;
let postSymbolSpy: Sinon.SinonSpy;
let patchSymbolSpy: Sinon.SinonSpy;
let abortSymbolUploadSpy: Sinon.SinonSpy;
let expectedRequestsScope: Nock.Scope;
let skippedRequestsScope: Nock.Scope;
before(() => {
Nock.disableNetConnect();
});
beforeEach(() => {
tmpFolderPath = Temp.mkdirSync("uploadSymbolsTest");
postSymbolSpy = Sinon.spy();
patchSymbolSpy = Sinon.spy();
abortSymbolUploadSpy = Sinon.spy();
});
describe("when Breakpad network requests are successful", () => {
beforeEach(() => {
expectedRequestsScope = _.flow(setupSuccessfulBreakpadPatchUploadResponse, setupSuccessfulBreakpadPostUploadResponse)(Nock(fakeHost));
skippedRequestsScope = setupSuccessfulBreakpadAbortUploadResponse(Nock(fakeHost));
});
it("uploads ZIP with symbols", async () => {
const zipPath = await createZipWithFile();
const result = await executeUploadCommand(["-b", zipPath]);
testCommandSuccess(result, expectedRequestsScope, skippedRequestsScope);
const [url, uploadedZipPath] = AzureBlobUploadHelperMock.getUploadedZipUrlAndPath();
expect(url).to.eql(fakeFullUploadUrl, `ZIP file should be uploaded to ${fakeFullUploadUrl}`);
expect(uploadedZipPath).to.eql(zipPath, "Zip file should be passed as it is");
});
});
describe("when Apple network requests are successful", () => {
beforeEach(() => {
expectedRequestsScope = _.flow(setupSuccessfulApplePatchUploadResponse, setupSuccessfulApplePostUploadResponse)(Nock(fakeHost));
skippedRequestsScope = setupSuccessfulAppleAbortUploadResponse(Nock(fakeHost));
});
it("uploads ZIP with symbols", async () => {
// Arrange
const zipPath = await createZipWithFile();
// Act
const result = await executeUploadCommand(["-s", zipPath]);
// Assert
testCommandSuccess(result, expectedRequestsScope, skippedRequestsScope);
const [url, uploadedZipPath] = AzureBlobUploadHelperMock.getUploadedZipUrlAndPath();
expect(url).to.eql(fakeFullUploadUrl, `ZIP file should be uploaded to ${fakeFullUploadUrl}`);
expect(uploadedZipPath).to.eql(zipPath, "Zip file should be passed as it is");
});
it("uploads dSYM folder", async () => {
// Arrange
const dsymFolder = createFolderWithSymbolsFile(tmpFolderPath, dSymFolder1Name, symbolsFile1Name, symbolsFileContent);
// Act
const result = await executeUploadCommand(["-s", dsymFolder]);
// Assert
testCommandSuccess(result, expectedRequestsScope, skippedRequestsScope);
const [url, zipPath] = AzureBlobUploadHelperMock.getUploadedZipUrlAndPath();
expect(url).to.eql(fakeFullUploadUrl, `ZIP file should be uploaded to ${fakeFullUploadUrl}`);
const uploadedZipEntries = getEntitiesList(await getUploadedZip(zipPath));
expect(uploadedZipEntries.length).to.eql(2, "Only two entries are expected to be in the ZIP");
expect(uploadedZipEntries).to.contain(Path.join(dSymFolder1Name, symbolsFile1Name), "Test file should be inside the uploaded ZIP");
expect(uploadedZipEntries).to.contain(dSymFolder1Name + Path.sep, ".dSYM folder should be inside the uploaded ZIP");
});
it("uploads dSYM parent folder", async () => {
// Arrange
const dsymParentFolder = createDsymParentFolder(tmpFolderPath, dSymParentFolderName);
// Act
const result = await executeUploadCommand(["-s", dsymParentFolder]);
// Assert
testCommandSuccess(result, expectedRequestsScope, skippedRequestsScope);
const [url, zipPath] = AzureBlobUploadHelperMock.getUploadedZipUrlAndPath();
expect(url).to.eql(fakeFullUploadUrl, `ZIP file should be uploaded to ${fakeFullUploadUrl}`);
const uploadedZipEntries = getEntitiesList(await getUploadedZip(zipPath));
expect(uploadedZipEntries.length).to.eql(4, "Only four entries are expected to be in the ZIP");
expect(uploadedZipEntries).to.contain(dSymFolder1Name + Path.sep, "First .dSYM folder should be inside the uploaded ZIP");
expect(uploadedZipEntries).to.contain(dSymFolder2Name + Path.sep, "Second .dSYM folder should be inside the uploaded ZIP");
expect(uploadedZipEntries).to.contain(Path.join(dSymFolder1Name, symbolsFile1Name), "Symbols file 1 should be inside the first dsym folder of ZIP");
expect(uploadedZipEntries).to.contain(Path.join(dSymFolder2Name, symbolsFile2Name), "Symbols file 2 should be inside the second dsym folder of ZIP");
});
it("uploads xcarchive folder", async () => {
// Arrange
const xcarchiveFolder = createXcArchiveFolder();
// Act
const result = await executeUploadCommand(["-x", xcarchiveFolder]);
// Assert
testCommandSuccess(result, expectedRequestsScope, skippedRequestsScope);
const [url, zipPath] = AzureBlobUploadHelperMock.getUploadedZipUrlAndPath();
expect(url).to.eql(fakeFullUploadUrl, `ZIP file should be uploaded to ${fakeFullUploadUrl}`);
const uploadedZipEntries = getEntitiesList(await getUploadedZip(zipPath));
expect(uploadedZipEntries.length).to.eql(4, "Only four entries are expected to be in the ZIP");
expect(uploadedZipEntries).to.contain(dSymFolder1Name + Path.sep, "First .dSYM folder should be inside the uploaded ZIP");
expect(uploadedZipEntries).to.contain(dSymFolder2Name + Path.sep, "Second .dSYM folder should be inside the uploaded ZIP");
expect(uploadedZipEntries).to.contain(Path.join(dSymFolder1Name, symbolsFile1Name), "Symbols file 1 should be inside the first dsym folder of ZIP");
expect(uploadedZipEntries).to.contain(Path.join(dSymFolder2Name, symbolsFile2Name), "Symbols file 2 should be inside the second dsym folder of ZIP");
});
it("uploads ZIP with added sourcemap file", async () => {
// Arrange
const zipPath = await createZipWithFile();
const mappingsFilePath = createMappingsFile(mappingsFileContent);
// Act
const result = await executeUploadCommand(["-s", zipPath, "-m", mappingsFilePath]);
// Assert
testCommandSuccess(result, expectedRequestsScope, skippedRequestsScope);
const [url, uploadedZipPath] = AzureBlobUploadHelperMock.getUploadedZipUrlAndPath();
expect(url).to.eql(fakeFullUploadUrl, `ZIP file should be uploaded to ${fakeFullUploadUrl}`);
expect(uploadedZipPath).not.to.eql(zipPath, "Uploaded ZIP path should be different from original ZIP path");
const uploadedZipEntries = getEntitiesList(await getUploadedZip(uploadedZipPath));
expect(uploadedZipEntries.length).to.eql(2, "Only two entries are expected to be in the ZIP");
expect(uploadedZipEntries).to.contain(symbolsFile1Name, "Test file should be inside the uploaded ZIP");
expect(uploadedZipEntries).to.contain(mappingsFileName, "Mappings file should be inside the uploaded ZIP");
});
it("uploads ZIP with updated sourcemap file", async () => {
// Arrange
const zipPath = await createZipWithFileAndMappings();
const newMappingsFilePath = createMappingsFile(alternativeMappingsFileContent);
// Act
const result = await executeUploadCommand(["-s", zipPath, "-m", newMappingsFilePath]);
// Assert
testCommandSuccess(result, expectedRequestsScope, skippedRequestsScope);
const [url, uploadedZipPath] = AzureBlobUploadHelperMock.getUploadedZipUrlAndPath();
expect(url).to.eql(fakeFullUploadUrl, `ZIP file should be uploaded to ${fakeFullUploadUrl}`);
expect(uploadedZipPath).not.to.eql(zipPath, "Uploaded ZIP path should be different from original ZIP path");
const uploadedZip = await getUploadedZip(uploadedZipPath);
const mappingsFileEntry = uploadedZip.file(mappingsFileName);
expect(mappingsFileEntry).to.not.eql(null, "Mappings file should exist in the uploaded ZIP");
const content = await mappingsFileEntry.async("text");
expect(content).eql(alternativeMappingsFileContent, "Mappings should have updated content: " + alternativeMappingsFileContent);
});
});
describe("when Apple upload fails", () => {
before(() => {
AzureBlobUploadHelperMock.throwOnUpload = true;
});
beforeEach(() => {
expectedRequestsScope = _.flow(setupSuccessfulAppleAbortUploadResponse, setupSuccessfulApplePostUploadResponse)(Nock(fakeHost));
skippedRequestsScope = setupSuccessfulApplePatchUploadResponse(Nock(fakeHost));
});
it("aborts the symbol uploading", async () => {
// Arrange
const zipPath = await createZipWithFile();
// Act
const result = await expect(executeUploadCommand(["-s", zipPath])).to.eventually.be.rejected;
// Assert
testUploadFailure(result, expectedRequestsScope, skippedRequestsScope);
});
after(() => {
AzureBlobUploadHelperMock.throwOnUpload = false;
});
});
describe("when Breakpad upload fails", () => {
before(() => {
AzureBlobUploadHelperMock.throwOnUpload = true;
});
beforeEach(() => {
expectedRequestsScope = _.flow(setupSuccessfulBreakpadAbortUploadResponse, setupSuccessfulBreakpadPostUploadResponse)(Nock(fakeHost));
skippedRequestsScope = setupSuccessfulBreakpadPatchUploadResponse(Nock(fakeHost));
});
it("aborts the symbol uploading", async () => {
const zipPath = await createZipWithFile();
const result = await expect(executeUploadCommand(["-b", zipPath])).to.eventually.be.rejected;
testUploadFailure(result, expectedRequestsScope, skippedRequestsScope);
});
after(() => {
AzureBlobUploadHelperMock.throwOnUpload = false;
});
});
afterEach(() => {
Nock.cleanAll();
});
after(() => {
Nock.enableNetConnect();
});
async function executeUploadCommand(args: string[]): Promise<CommandResult> {
const uploadSymbolsCommand = new UploadSymbolsCommand(getCommandArgs(args));
return await uploadSymbolsCommand.execute();
}
function testCommandSuccess(result: CommandResult, executionScope: Nock.Scope, abortScope: Nock.Scope) {
expect(result.succeeded).to.eql(true, "Command should be successfully completed");
expect(abortScope.isDone()).to.eql(false, "Upload should not be aborted");
executionScope.done(); // All normal API calls are executed
}
function testUploadFailure(result: CommandResult, executionScope: Nock.Scope, patchScope: Nock.Scope) {
expect(result.succeeded).to.eql(false, "Command should fail");
expect(patchScope.isDone()).to.eql(false, "Upload should not be completed");
executionScope.done(); // All normal API calls are executed
}
async function createZipWithFile(): Promise<string> {
// creating temp contents file for zip
const symbolsFilePath = createSymbolsFileInsideFolder(tmpFolderPath, symbolsFile1Name, symbolsFileContent);
// packing temp contents file to the zip
const testZipFile = "testZip.zip";
const testZipFilePath = Path.join(tmpFolderPath, testZipFile);
const zip = new JsZip();
const testFileContent = await Pfs.readFile(symbolsFilePath);
zip.file(symbolsFile1Name, testFileContent);
// writing zip file
await Pfs.writeFile(testZipFilePath, await zip.generateAsync({
type: "nodebuffer"
}));
return testZipFilePath;
}
async function createZipWithFileAndMappings(): Promise<string> {
// creating usual zip file for uploading
const zipFilePath = await createZipWithFile();
// adding mappings file to the created zip and writing it back
const zipContentBuffer = await Pfs.readFile(zipFilePath);
const zip = await new JsZip().loadAsync(zipContentBuffer);
zip.file(mappingsFileName, mappingsFileContent);
const updatedZipContentBuffer = await zip.generateAsync({
type: "nodebuffer"
});
await Pfs.writeFile(zipFilePath, updatedZipContentBuffer);
return zipFilePath;
}
function createFolderWithSymbolsFile(path: string, folderName: string, fileName: string, fileContent: string): string {
// creating folder
const testFolderPath = Path.join(path, folderName);
Fs.mkdirSync(testFolderPath);
// creating symbols file inside folder
createSymbolsFileInsideFolder(testFolderPath, fileName, fileContent);
return testFolderPath;
}
function createDsymParentFolder(path: string, parentFolderName: string): string {
// creating dsym parent folder
const dsymParentFolderPath = Path.join(path, parentFolderName);
Fs.mkdirSync(dsymParentFolderPath);
// creating first dsymFolder with file
createFolderWithSymbolsFile(dsymParentFolderPath, dSymFolder1Name, symbolsFile1Name, symbolsFileContent);
// creating second dsymFolder with file
createFolderWithSymbolsFile(dsymParentFolderPath, dSymFolder2Name, symbolsFile2Name, symbolsFileContent);
// adding random file to the root which should not be added
Fs.writeFileSync(Path.join(dsymParentFolderPath, randomFileName), randomFileContent);
return dsymParentFolderPath;
}
function createXcArchiveFolder(): string {
// creating .xcarchive folder
const xcArchiveFolderPath = Path.join(tmpFolderPath, xcArchiveFolderName);
Fs.mkdirSync(xcArchiveFolderPath);
// creating dSyms folder inside with inner folders
createDsymParentFolder(xcArchiveFolderPath, "dSYMs");
return xcArchiveFolderPath;
}
function getCommandArgs(additionalArgs: string[]): CommandArgs {
const args: string[] = ["-a", fakeAppIdentifier, "--token", fakeToken, "--env", "local"].concat(additionalArgs);
return {
args,
command: ["crashes", "upload-symbols"],
commandPath: "FAKE"
};
}
async function getUploadedZip(zip: string): Promise<JsZip> {
const zipContent = await Pfs.readFile(zip);
return await new JsZip().loadAsync(zipContent);
}
function getEntitiesList(zip: JsZip): string[] {
return Object.getOwnPropertyNames(zip.files).map((entity) => Path.normalize(entity));
}
function createSymbolsFileInsideFolder(folderPath: string, fileName: string, fileContent: string) {
const symbolsFilePath = Path.join(folderPath, fileName);
Fs.writeFileSync(symbolsFilePath, fileContent);
return symbolsFilePath;
}
function createMappingsFile(content: string): string {
// creating a test mappings file
const mappingsFilePath = Path.join(tmpFolderPath, mappingsFileName);
Fs.writeFileSync(mappingsFilePath, content);
return mappingsFilePath;
}
function setupSuccessfulApplePostUploadResponse(nockScope: Nock.Scope): Nock.Scope {
return nockScope.post(`/v0.1/apps/${fakeAppOwner}/${fakeAppName}/symbol_uploads`, {
symbol_type: "Apple"
}).reply(200, ((uri: any, requestBody: any) => {
postSymbolSpy(requestBody);
return {
expiration_date: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
symbol_upload_id: fakeSymbolUploadingId,
upload_url: fakeHost + fakeUploadUrl
};
}));
}
function setupSuccessfulBreakpadPostUploadResponse(nockScope: Nock.Scope): Nock.Scope {
return nockScope.post(`/v0.1/apps/${fakeAppOwner}/${fakeAppName}/symbol_uploads`, {
symbol_type: "Breakpad"
}).reply(200, ((uri: any, requestBody: any) => {
postSymbolSpy(requestBody);
return {
expiration_date: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
symbol_upload_id: fakeSymbolUploadingId,
upload_url: fakeHost + fakeUploadUrl
};
}));
}
function setupSuccessfulApplePatchUploadResponse(nockScope: Nock.Scope): Nock.Scope {
return nockScope.patch(`/v0.1/apps/${fakeAppOwner}/${fakeAppName}/symbol_uploads/${fakeSymbolUploadingId}`, {
status: "committed"
}).reply(200, ((uri: any, requestBody: any) => {
patchSymbolSpy(requestBody);
return {
origin: "User",
status: "committed",
symbol_type: "Apple",
symbol_upload_id: fakeSymbolUploadingId,
symbols: new Array()
};
}));
}
function setupSuccessfulBreakpadPatchUploadResponse(nockScope: Nock.Scope): Nock.Scope {
return nockScope.patch(`/v0.1/apps/${fakeAppOwner}/${fakeAppName}/symbol_uploads/${fakeSymbolUploadingId}`, {
status: "committed"
}).reply(200, ((uri: any, requestBody: any) => {
patchSymbolSpy(requestBody);
return {
origin: "User",
status: "committed",
symbol_type: "Breakpad",
symbol_upload_id: fakeSymbolUploadingId,
symbols: new Array()
};
}));
}
function setupSuccessfulAppleAbortUploadResponse(nockScope: Nock.Scope): Nock.Scope {
return nockScope.patch(`/v0.1/apps/${fakeAppOwner}/${fakeAppName}/symbol_uploads/${fakeSymbolUploadingId}`, {
status: "aborted"
}).reply(200, ((uri: any, requestBody: any) => {
abortSymbolUploadSpy(requestBody);
return {
origin: "User",
status: "aborted",
symbol_type: "Apple",
symbol_upload_id: fakeSymbolUploadingId,
symbols: new Array()
};
}));
}
function setupSuccessfulBreakpadAbortUploadResponse(nockScope: Nock.Scope): Nock.Scope {
return nockScope.patch(`/v0.1/apps/${fakeAppOwner}/${fakeAppName}/symbol_uploads/${fakeSymbolUploadingId}`, {
status: "aborted"
}).reply(200, ((uri: any, requestBody: any) => {
abortSymbolUploadSpy(requestBody);
return {
origin: "User",
status: "aborted",
symbol_type: "Breakpad",
symbol_upload_id: fakeSymbolUploadingId,
symbols: new Array()
};
}));
}
});