diff --git a/.mocharc.json b/.mocharc.json index 8311bd3..51d77c1 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -1,8 +1,7 @@ { - "require": ["ts-node/register"], - "watch-extensions": "ts", - "recursive": true, - "reporter": "spec", - "timeout": 600000, - "node-option": ["loader=ts-node/esm"] + "require": ["ts-node/register"], + "watch-extensions": "ts", + "recursive": true, + "reporter": "spec", + "node-option": ["loader=ts-node/esm"] } diff --git a/test/unit/.eslintrc.cjs b/test/unit/.eslintrc.cjs index c4b54a5..1c7a35d 100644 --- a/test/unit/.eslintrc.cjs +++ b/test/unit/.eslintrc.cjs @@ -3,6 +3,7 @@ module.exports = { // Allow describe and it env: { mocha: true }, rules: { + 'class-methods-use-this': 'off', 'no-unused-expressions': 'off', // Allow assert style expressions. i.e. expect(true).to.be.true '@typescript-eslint/explicit-function-return-type': 'off', // Return types are defined by the source code. Allows for quick overwrites. '@typescript-eslint/no-empty-function': 'off', // Mocked out the methods that shouldn't do anything in the tests. diff --git a/test/unit/common/CommonUtils.test.ts b/test/unit/common/CommonUtils.test.ts index ca1aff7..72ed062 100644 --- a/test/unit/common/CommonUtils.test.ts +++ b/test/unit/common/CommonUtils.test.ts @@ -6,20 +6,72 @@ */ import os from 'node:os'; import fs from 'node:fs'; +import http from 'node:http'; +import https from 'node:https'; import path from 'node:path'; +import stream from 'node:stream'; import { Messages } from '@salesforce/core'; import { TestContext } from '@salesforce/core/testSetup'; import { stubMethod } from '@salesforce/ts-sinon'; import { expect } from 'chai'; +import sinon from 'sinon'; import { CommonUtils } from '../../../src/common/CommonUtils.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +// Custom Writable stream that writes to memory, for testing downloadFile +const data: Buffer[] = []; +class MemoryWriteStream extends stream.Writable { + public getData(): string { + return Buffer.concat(data).toString(); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public _write(chunk: Buffer, encoding: string, callback: (error?: Error | null) => void): void { + data.push(chunk); + callback(); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public close(callback?: ((err?: NodeJS.ErrnoException | null | undefined) => void) | undefined): void { + // no-op + } +} + describe('CommonUtils', () => { + const downloadDestinationFile = path.join(os.tmpdir(), 'ca.crt'); const messages = Messages.loadMessages('@salesforce/lwc-dev-mobile-core', 'common'); const $$ = new TestContext(); + let unlinkStub: sinon.SinonStub; + let httpGetStub: sinon.SinonStub; + let httpsGetStub: sinon.SinonStub; + let mockBadResponse: http.IncomingMessage; + let mockGoodResponse: http.IncomingMessage; + let writeStreamMock: MemoryWriteStream; + + beforeEach(() => { + stubMethod($$.SANDBOX, fs, 'createWriteStream').returns(writeStreamMock); + unlinkStub = stubMethod($$.SANDBOX, fs, 'unlink').callsFake((_, callback) => { + callback(); + }); + httpGetStub = stubMethod($$.SANDBOX, http, 'get'); + httpsGetStub = stubMethod($$.SANDBOX, https, 'get'); + + mockBadResponse = new stream.PassThrough() as unknown as http.IncomingMessage; + mockBadResponse.statusCode = 404; + mockBadResponse.statusMessage = 'Not Found'; + + mockGoodResponse = new stream.PassThrough() as unknown as http.IncomingMessage; + mockGoodResponse.statusCode = 200; + mockGoodResponse.statusMessage = 'Done'; + (mockGoodResponse as unknown as stream.PassThrough).write('This is a test'); + + writeStreamMock = new MemoryWriteStream(); + }); + afterEach(() => { + httpGetStub.reset(); $$.restore(); }); @@ -119,49 +171,46 @@ describe('CommonUtils', () => { } }); - it('downloadFile function', async () => { - const dest = path.join(os.tmpdir(), 'ca.crt'); - - // should fail and not create a destination file - try { - await CommonUtils.downloadFile('badurl', dest); - } catch (error) { - expect(error).to.be.an('error'); - expect(fs.existsSync(dest)).to.be.false; - } + it('downloadFile fails with bad relative url', async () => { + await verifyDownloadFails('badurl1', mockBadResponse); + }); - // should fail and not create a destination file - try { - await CommonUtils.downloadFile('http://badurl', dest); - } catch (error) { - expect(error).to.be.an('error'); - expect(fs.existsSync(dest)).to.be.false; - } + it('downloadFile fails with bad http absolute url', async () => { + await verifyDownloadFails('http://badurl2', mockBadResponse); + }); - // should fail and not create a destination file - try { - await CommonUtils.downloadFile('https://badurl', dest); - } catch (error) { - expect(error).to.be.an('error'); - expect(fs.existsSync(dest)).to.be.false; - } + it('downloadFile fails with bad https absolute url', async () => { + await verifyDownloadFails('https://badurl3', mockBadResponse); + }); - // should fail and not create a destination file - try { - await CommonUtils.downloadFile('https://www.google.com/badurl', dest); - } catch (error) { - expect(error).to.be.an('error'); - expect(fs.existsSync(dest)).to.be.false; - } + it('downloadFile fails when fetching url goes through but saving to file fails', async () => { + await verifyDownloadFails('https://www.salesforce.com', { + statusCode: 200, + pipe: (writeStream: fs.WriteStream) => { + writeStream.emit('error', new Error('failed to write to file')); + } + }); + }); - // For now don't run this part on Windows b/c our CI - // environment does not give file write permission. - if (process.platform !== 'win32') { - // should pass and create a destination file - await CommonUtils.downloadFile('https://www.google.com', dest); - expect(fs.existsSync(dest)).to.be.true; - } - }).timeout(20000); // increase timeout for this test + it('downloadFile passes and save the content', async () => { + httpsGetStub.callsFake((_, callback) => { + setTimeout( + () => + callback({ + statusCode: 200, + pipe: (writeStream: fs.WriteStream) => { + writeStream.write('Test Content'); + writeStream.emit('finish'); + } + }), + 100 + ); + return { on: () => {}, end: () => {} }; + }); + await CommonUtils.downloadFile('https://www.google.com', downloadDestinationFile); + expect(unlinkStub.called).to.be.false; + expect(writeStreamMock.getData()).to.equal('Test Content'); + }); it('read/write text file functions', async () => { const dest = path.join(os.tmpdir(), 'test_file.txt'); @@ -300,4 +349,26 @@ describe('CommonUtils', () => { parentPath: fname }; } + + async function verifyDownloadFails(url: string, mockResponse: any) { + if (url.startsWith('https:')) { + httpsGetStub.callsFake((_, callback) => { + setTimeout(() => callback(mockResponse), 100); + return { on: () => {}, end: () => {} }; + }); + } else { + httpGetStub.callsFake((_, callback) => { + setTimeout(() => callback(mockResponse), 100); + return { on: () => {}, end: () => {} }; + }); + } + + try { + await CommonUtils.downloadFile(url, downloadDestinationFile); + } catch (error) { + expect(error).to.be.an('error'); + expect(unlinkStub.calledOnce).to.be.true; + expect(unlinkStub.getCall(0).args[0]).to.be.equal(downloadDestinationFile); + } + } });