From fae41c3cae5ceba11f349d3d16df967443476923 Mon Sep 17 00:00:00 2001
From: Hugo Montero <hugo.montero.ventura@particle.io>
Date: Mon, 20 May 2024 14:55:05 -0600
Subject: [PATCH] add update cli coverage

---
 src/cmd/update-cli.js      |  12 +-
 src/cmd/update-cli.test.js | 279 +++++++++++++++++++++++++++++++++++++
 2 files changed, 285 insertions(+), 6 deletions(-)
 create mode 100644 src/cmd/update-cli.test.js

diff --git a/src/cmd/update-cli.js b/src/cmd/update-cli.js
index 7988aed34..3b177303b 100644
--- a/src/cmd/update-cli.js
+++ b/src/cmd/update-cli.js
@@ -88,13 +88,13 @@ class UpdateCliCommand {
 	logAndReject(error, reject, version) {
 		const baseMessage = 'We were unable to check for updates';
 		const message = version ? `${baseMessage}: Version ${version} not found` : `${baseMessage} Please try again later`;
-		log.error(error);
+		log.error(error.message || error);
 		reject(message);
 	}
 
-	async downloadCLI(manifest) {
+	async downloadCLI(manifest, _os = os) {
 		try {
-			const { url, sha256: expectedHash } = this.getBuildDetailsFromManifest(manifest);
+			const { url, sha256: expectedHash } = this.getBuildDetailsFromManifest(manifest, _os);
 			const fileName = url.split('/').pop();
 			const fileNameWithoutLastExtension = path.basename(fileName, path.extname(fileName));
 			const filePath = path.join(os.tmpdir(), fileNameWithoutLastExtension);
@@ -153,9 +153,9 @@ class UpdateCliCommand {
 		});
 	}
 
-	getBuildDetailsFromManifest(manifest) {
-		const platform = os.platform();
-		let arch = os.arch();
+	getBuildDetailsFromManifest(manifest, _os = os) {
+		const platform = _os.platform();
+		let arch = _os.arch();
 		const platformKey = platform;
 		const archKey = arch;
 		const platformManifest = manifest.builds && manifest.builds[platformKey];
diff --git a/src/cmd/update-cli.test.js b/src/cmd/update-cli.test.js
new file mode 100644
index 000000000..b91a70aa9
--- /dev/null
+++ b/src/cmd/update-cli.test.js
@@ -0,0 +1,279 @@
+const { expect, sinon } = require('../../test/setup');
+const UpdateCLI = require('./update-cli');
+const settings = require('../../settings');
+const fs = require('fs-extra');
+const path = require('path');
+const nock = require('nock');
+const log = require('../lib/log');
+const pkg = require('../../package');
+const os = require('os');
+
+describe('Update CLI Command', () => {
+	beforeEach(() => {
+		sinon.stub(settings, 'saveProfileData');
+		settings.profile_json = { enableUpdates: false };
+	});
+
+	afterEach(() => {
+		sinon.restore();
+	});
+
+	describe('updateCli', () => {
+		beforeEach(() => {
+			sinon.stub(log, 'info');
+		});
+		afterEach(() => {
+			sinon.restore();
+		});
+		it('skip update if the CLI is up to date', async () => {
+			const update = new UpdateCLI();
+			const manifest = { version: '1.2.3' };
+			sinon.stub(update, 'downloadManifest').resolves(manifest);
+			sinon.stub(pkg, 'version').value('1.2.3');
+			await update.updateCli();
+			expect(log.info).to.have.been.calledWith('CLI is already up to date');
+		});
+
+		it('update the CLI to the latest version', async () => {
+			const update = new UpdateCLI();
+			const manifest = { version: '1.2.3' };
+			sinon.stub(update, 'downloadManifest').resolves(manifest);
+			sinon.stub(pkg, 'version').value('1.2.2');
+			sinon.stub(update, 'downloadCLI').resolves('/path/to/cli/particle-cli-1.2.3-linux-x64.gz');
+			sinon.stub(update, 'replaceCLI').resolves();
+			await update.updateCli();
+			expect(log.info).to.have.been.calledWith('CLI updated successfully');
+			expect(settings.saveProfileData).to.have.property('callCount', 1);
+			expect(settings.profile_json.last_version_check).to.gt(0);
+			expect(update.downloadCLI).to.have.been.calledWith(manifest);
+			expect(update.replaceCLI).to.have.been.calledWith('/path/to/cli/particle-cli-1.2.3-linux-x64.gz');
+		});
+	});
+	describe('enable/disable updates', () => {
+		it('should enable updates', async () => {
+			const update = new UpdateCLI();
+			await update.update({ 'enable-updates': true });
+			expect(settings.saveProfileData).to.have.property('callCount', 1);
+			expect(settings.profile_json.enableUpdates).to.equal(true);
+		});
+
+		it('should disable updates', async () => {
+			settings.profile_json.enableUpdates = true;
+			const update = new UpdateCLI();
+			await update.update({ 'disable-updates': true });
+			expect(settings.saveProfileData).to.have.property('callCount', 1);
+			expect(settings.profile_json.enableUpdates).to.equal(false);
+		});
+	});
+
+	describe('downloadManifest', () => {
+		const manifestData = JSON.stringify({
+			'version': '4.1.0',
+			'builds': {
+				'linux': { 'x64': { 'url': 'https://binaries.particle.io/cli/manifest/4.1.0/particle-cli-4.1.0-linux-x64.tar.gz' }
+				}
+			}
+		});
+		beforeEach(() => {
+			sinon.stub(log, 'info');
+			sinon.stub(log, 'error');
+		});
+		afterEach(() => {
+			sinon.restore();
+		});
+		it('downloads the latest manifest version', async () => {
+			nock('https://binaries.particle.io')
+				.intercept('/particle-cli/manifest.json', 'GET')
+				.reply(200, manifestData);
+			const update = new UpdateCLI();
+			const manifest = await update.downloadManifest();
+			expect(manifest).to.deep.equal(JSON.parse(manifestData));
+		});
+
+		it('downloads a specific manifest version', async () => {
+			nock('https://binaries.particle.io')
+				.intercept('/particle-cli/manifest-4.1.0.json', 'GET')
+				.reply(200, manifestData);
+			const update = new UpdateCLI();
+			const manifest = await update.downloadManifest('4.1.0');
+			expect(manifest).to.deep.equal(JSON.parse(manifestData));
+		});
+		it('throws an error if the manifest download fails', async () => {
+			nock('https://binaries.particle.io')
+				.intercept('/particle-cli/manifest.json', 'GET')
+				.reply(404);
+			const update = new UpdateCLI();
+			await expect(update.downloadManifest()).to.be.rejectedWith('We were unable to check for updates Please try again later');
+			expect(log.error).to.have.been.calledWith('Failed to download manifest: Status Code 404');
+		});
+		it('throws an error if the manifest is invalid', async () => {
+			nock('https://binaries.particle.io')
+				.intercept('/particle-cli/manifest.json', 'GET')
+				.reply(200, 'invalid json');
+			const update = new UpdateCLI();
+			await expect(update.downloadManifest()).to.be.rejectedWith('We were unable to check for updates Please try again later');
+			expect(log.error).to.have.been.calledWith('Unexpected token i in JSON at position 0');
+		});
+	});
+
+	describe('downloadCLI', () => {
+		beforeEach(() => {
+			sinon.stub(log, 'error');
+			sinon.stub(log, 'debug');
+		});
+		afterEach(() => {
+			sinon.restore();
+		});
+		it('downloads the CLI binary', async () => {
+			const update = new UpdateCLI();
+			const buildDetails = {
+				url: 'https://binaries.particle.io/cli/manifest/1.2.3/particle-cli-1.2.3-linux-x64.gz',
+				sha256: '9cb63cb779e8c571db3199b783a36cc43cd9e7c076beeb496c39e9cc06196dc5'
+			};
+			sinon.stub(update, 'getBuildDetailsFromManifest').returns(buildDetails);
+			sinon.stub(update, 'unzipFile').resolves('/path/to/cli/particle-cli-1.2.3-linux-x64.gz');
+
+			nock('https://binaries.particle.io')
+				.intercept('/cli/manifest/1.2.3/particle-cli-1.2.3-linux-x64.gz', 'GET')
+				.reply(200, 'binary data');
+			const cliPath = await update.downloadCLI(buildDetails);
+			expect(cliPath).to.contain('particle-cli-1.2.3-linux-x64.gz');
+			expect(log.error).to.not.have.been.called;
+		});
+
+		it('throws an error if the CLI download fails', async () => {
+			const update = new UpdateCLI();
+			const buildDetails = {
+				url: 'https://binaries.particle.io/cli/manifest/1.2.3/particle-cli-1.2.3-linux-x64.gz',
+				sha256: '9cb63cb779e8c571db3199b783a36cc43cd9e7c076beeb496c39e9cc06196dc5'
+			};
+			sinon.stub(update, 'getBuildDetailsFromManifest').returns(buildDetails);
+			sinon.stub(update, 'unzipFile').resolves('/path/to/cli/particle-cli-1.2.3-linux-x64.gz');
+
+			nock('https://binaries.particle.io')
+				.intercept('/cli/manifest/1.2.3/particle-cli-1.2.3-linux-x64.gz', 'GET')
+				.reply(404);
+			await expect(update.downloadCLI(buildDetails)).to.be.rejectedWith('Failed to download or verify the CLI, please try again later');
+			expect(log.debug).to.have.been.calledWith('Failed during download or verification: Error: No file found to download');
+		});
+
+		it('throws an error if the CLI hash does not match', async () => {
+			const update = new UpdateCLI();
+			const buildDetails = {
+				url: 'https://binaries.particle.io/cli/manifest/1.2.3/particle-cli-1.2.3-linux-x64.gz',
+				sha256: 'invalid-hash'
+			};
+			sinon.stub(update, 'getBuildDetailsFromManifest').returns(buildDetails);
+			sinon.stub(update, 'unzipFile').resolves('/path/to/cli/particle-cli-1.2.3-linux-x64.gz');
+
+			nock('https://binaries.particle.io')
+				.intercept('/cli/manifest/1.2.3/particle-cli-1.2.3-linux-x64.gz', 'GET')
+				.reply(200, 'binary data');
+			await expect(update.downloadCLI(buildDetails)).to.be.rejectedWith('Failed to download or verify the CLI, please try again later');
+			expect(log.debug).to.have.been.calledWith('Failed during download or verification: Error: Hash mismatch');
+		});
+	});
+
+	describe('getBuildDetailsFromManifest', () => {
+		const manifest = {
+			builds: {
+				linux: {
+					x64: {
+						url: 'https://binaries.particle.io/cli/manifest/1.2.3/particle-cli-1.2.3-linux-x64.gz'
+					}
+				},
+				darwin: {
+					x64: {
+						url: 'https://binaries.particle.io/cli/manifest/1.2.3/particle-cli-1.2.3-darwin-x64.gz'
+					},
+					arm64: {
+						url: 'https://binaries.particle.io/cli/manifest/1.2.3/particle-cli-1.2.3-darwin-arm64.gz'
+					}
+				},
+				win32: {
+					x64: {
+						url: 'https://binaries.particle.io/cli/manifest/1.2.3/particle-cli-1.2.3-win32-x64.gz'
+					}
+				}
+			}
+		};
+		const _os = {
+			platform: sinon.stub().returns('darwin'),
+			arch: sinon.stub().returns('unknown')
+		};
+		afterEach(() => {
+			sinon.restore();
+		});
+		it('returns the manifest details from current platform/architecture', () => {
+			const update = new UpdateCLI();
+			const buildDetails = update.getBuildDetailsFromManifest(manifest);
+			expect(buildDetails).to.equal(manifest.builds[os.platform()][os.arch()]);
+		});
+
+		it('throws an error if the platform is not found in the manifest', () => {
+			const update = new UpdateCLI();
+			expect(() => update.getBuildDetailsFromManifest(manifest, _os)).to.throw('No CLI build found for darwin unknown');
+		});
+
+		it('throws an error if the platform is not found in the manifest', () => {
+			const update = new UpdateCLI();
+			expect(() => update.getBuildDetailsFromManifest({})).to.throw(`No CLI build found for ${os.platform()} ${os.arch()}`);
+		});
+
+	});
+
+	describe('replaceCLI', () => {
+		let processExecPathTmp;
+		beforeEach(() => {
+			processExecPathTmp = process.execPath;
+			process.execPath = '/usr/bin/particle';
+		});
+
+		afterEach(() => {
+			process.execPath = processExecPathTmp;
+		});
+
+
+		it('replace the cli for a new one', async () => {
+			const update = new UpdateCLI();
+			const newCliPath = path.join('path', 'to', 'new', 'cli');
+			const execPath = path.join(path.sep,'usr', 'bin', 'particle');
+			const binPath = path.join(path.sep,'usr', 'bin');
+			const fileName = 'particle';
+
+
+			const getBinaryPathStub = sinon.spy(update, 'getBinaryPath');
+			const moveStub = sinon.stub(fs, 'move').resolves();
+			const chmodStub = sinon.stub(fs, 'chmod').resolves();
+			const cliPath = execPath;
+			const oldCliPath = path.join(binPath, `${fileName}.old`);
+			await update.replaceCLI(newCliPath);
+			expect(getBinaryPathStub).to.have.been.calledOnce;
+			expect(moveStub).to.have.been.calledWith(execPath, oldCliPath, { overwrite: true });
+			expect(moveStub).to.have.been.calledWith(newCliPath, cliPath);
+			expect(chmodStub).to.have.been.calledWith(cliPath, 0o755);
+		});
+	});
+
+	describe('configureProfileSettings', () => {
+		it ('disable updates if version is provided', async() => {
+			const update = new UpdateCLI();
+			settings.profile_json.last_version_check = 0;
+			settings.profile_json.enableUpdates = true;
+			await update.configureProfileSettings('1.2.3');
+			expect(settings.profile_json.last_version_check).to.gt(0);
+			expect(settings.profile_json.enableUpdates).to.equal(false);
+			expect(settings.saveProfileData).to.have.property('callCount', 2);
+		});
+
+		it ('update last_version_check but will not change enableUpdates data if version is undefined', async () => {
+			const update = new UpdateCLI();
+			settings.profile_json.last_version_check = 0;
+			settings.profile_json.enableUpdates = true;
+			await update.configureProfileSettings();
+			expect(settings.profile_json.last_version_check).to.gt(0);
+			expect(settings.profile_json.enableUpdates).to.equal(true);
+			expect(settings.saveProfileData).to.have.property('callCount', 1);
+		});
+	});
+});