From 9e810b445aabfe8c779e16b76b1ed57fa15cd03d Mon Sep 17 00:00:00 2001 From: Brian Clifton Date: Mon, 13 Mar 2017 17:37:00 -0700 Subject: [PATCH] Prevent session from being destroyed when errors are raised Taking a step towards https://github.com/brave/browser-laptop/issues/5512 - Take most of the session loading logic out of the massive try/catch - This adds 13+ tests to ensure behavior works as expected Before % Stmts: 52.65 % Branch: 45.77 % Funcs: 54.05 % Lines: 53.21 After % Stmts: 55.33 % Branch: 48.59 % Funcs: 56.41 % Lines: 56.41 - If file fails JSON.parse, a backup is saved. This is written synchronously Auditors: @bbondy, @bridiver Test Plan: `npm run unittest -- --grep="sessionStore unit tests"` --- app/sessionStore.js | 216 ++++++++++------ package.json | 1 + test/unit/app/sessionStoreTest.js | 412 +++++++++++++++++++++++++++--- test/unit/lib/fakeElectron.js | 3 +- 4 files changed, 514 insertions(+), 118 deletions(-) diff --git a/app/sessionStore.js b/app/sessionStore.js index 083c2247b62..1509500675f 100644 --- a/app/sessionStore.js +++ b/app/sessionStore.js @@ -12,7 +12,7 @@ // - NODE_ENV of ‘test’ bypassing session state or else they all fail. const Immutable = require('immutable') -const fs = require('fs') +const fs = require('fs-extra') const path = require('path') const electron = require('electron') const app = electron.app @@ -50,6 +50,14 @@ const getTopSiteMap = () => { return {} } +const getTempStoragePath = (filename) => { + const epochTimestamp = (new Date()).getTime().toString() + filename = filename || 'tmp' + return process.env.NODE_ENV !== 'test' + ? path.join(app.getPath('userData'), 'session-store-' + filename + '-' + epochTimestamp) + : path.join(process.env.HOME, '.brave-test-session-store-' + filename + '-' + epochTimestamp) +} + const getStoragePath = () => { return path.join(app.getPath('userData'), sessionStorageName) } @@ -91,10 +99,7 @@ module.exports.saveAppState = (payload, isShutdown) => { } payload.lastAppVersion = app.getVersion() - const epochTimestamp = (new Date()).getTime().toString() - const tmpStoragePath = process.env.NODE_ENV !== 'test' - ? path.join(app.getPath('userData'), 'session-store-tmp-' + epochTimestamp) - : path.join(process.env.HOME, '.brave-test-session-store-tmp-' + epochTimestamp) + const tmpStoragePath = getTempStoragePath() let p = promisify(fs.writeFile, tmpStoragePath, JSON.stringify(payload)) .then(() => promisify(fs.rename, tmpStoragePath, getStoragePath())) @@ -266,7 +271,11 @@ module.exports.cleanAppData = (data, isShutdown) => { } const clearAutocompleteData = isShutdown && getSetting(settings.SHUTDOWN_CLEAR_AUTOCOMPLETE_DATA) === true if (clearAutocompleteData) { - autofill.clearAutocompleteData() + try { + autofill.clearAutocompleteData() + } catch (e) { + console.log('cleanAppData: error calling autofill.clearAutocompleteData: ', e) + } } const clearAutofillData = isShutdown && getSetting(settings.SHUTDOWN_CLEAR_AUTOFILL_DATA) === true if (clearAutofillData) { @@ -337,8 +346,21 @@ module.exports.cleanAppData = (data, isShutdown) => { }) } } - data = tabState.getPersistentState(data).toJS() - data = windowState.getPersistentState(data).toJS() + + try { + data = tabState.getPersistentState(data).toJS() + } catch (e) { + delete data.tabs + console.log('cleanAppData: error calling tabState.getPersistentState: ', e) + } + + try { + data = windowState.getPersistentState(data).toJS() + } catch (e) { + delete data.windows + console.log('cleanAppData: error calling windowState.getPersistentState: ', e) + } + if (data.extensions) { Object.keys(data.extensions).forEach((extensionId) => { delete data.extensions[extensionId].tabs @@ -408,6 +430,77 @@ const setVersionInformation = (data) => { return data } +module.exports.runPreMigrations = (data) => { + // autofill data migration + if (data.autofill) { + if (Array.isArray(data.autofill.addresses)) { + let addresses = exports.defaultAppState().autofill.addresses + data.autofill.addresses.forEach((guid) => { + addresses.guid.push(guid) + addresses.timestamp = new Date().getTime() + }) + data.autofill.addresses = addresses + } + if (Array.isArray(data.autofill.creditCards)) { + let creditCards = exports.defaultAppState().autofill.creditCards + data.autofill.creditCards.forEach((guid) => { + creditCards.guid.push(guid) + creditCards.timestamp = new Date().getTime() + }) + data.autofill.creditCards = creditCards + } + if (data.autofill.addresses.guid) { + let guids = [] + data.autofill.addresses.guid.forEach((guid) => { + if (typeof guid === 'object') { + guids.push(guid['persist:default']) + } else { + guids.push(guid) + } + }) + data.autofill.addresses.guid = guids + } + if (data.autofill.creditCards.guid) { + let guids = [] + data.autofill.creditCards.guid.forEach((guid) => { + if (typeof guid === 'object') { + guids.push(guid['persist:default']) + } else { + guids.push(guid) + } + }) + data.autofill.creditCards.guid = guids + } + } + // xml migration + if (data.settings) { + if (data.settings[settings.DEFAULT_SEARCH_ENGINE] === 'content/search/google.xml') { + data.settings[settings.DEFAULT_SEARCH_ENGINE] = 'Google' + } + if (data.settings[settings.DEFAULT_SEARCH_ENGINE] === 'content/search/duckduckgo.xml') { + data.settings[settings.DEFAULT_SEARCH_ENGINE] = 'DuckDuckGo' + } + } + + return data +} + +module.exports.runPostMigrations = (data) => { + // sites refactoring migration + if (Array.isArray(data.sites) && data.sites.length) { + let sites = {} + let order = 0 + data.sites.forEach((site) => { + let key = siteUtil.getSiteKey(Immutable.fromJS(site)) + site.order = order++ + sites[key] = site + }) + data.sites = sites + } + + return data +} + /** * Loads the browser state from storage. * @@ -421,64 +514,30 @@ module.exports.loadAppState = () => { data = fs.readFileSync(getStoragePath()) } catch (e) {} + let loaded = false try { data = JSON.parse(data) - // autofill data migration - if (data.autofill) { - if (Array.isArray(data.autofill.addresses)) { - let addresses = exports.defaultAppState().autofill.addresses - data.autofill.addresses.forEach((guid) => { - addresses.guid.push(guid) - addresses.timestamp = new Date().getTime() - }) - data.autofill.addresses = addresses - } - if (Array.isArray(data.autofill.creditCards)) { - let creditCards = exports.defaultAppState().autofill.creditCards - data.autofill.creditCards.forEach((guid) => { - creditCards.guid.push(guid) - creditCards.timestamp = new Date().getTime() - }) - data.autofill.creditCards = creditCards - } - if (data.autofill.addresses.guid) { - let guids = [] - data.autofill.addresses.guid.forEach((guid) => { - if (typeof guid === 'object') { - guids.push(guid['persist:default']) - } else { - guids.push(guid) - } - }) - data.autofill.addresses.guid = guids - } - if (data.autofill.creditCards.guid) { - let guids = [] - data.autofill.creditCards.guid.forEach((guid) => { - if (typeof guid === 'object') { - guids.push(guid['persist:default']) - } else { - guids.push(guid) - } - }) - data.autofill.creditCards.guid = guids - } - } - // xml migration - if (data.settings) { - if (data.settings[settings.DEFAULT_SEARCH_ENGINE] === 'content/search/google.xml') { - data.settings[settings.DEFAULT_SEARCH_ENGINE] = 'Google' - } - if (data.settings[settings.DEFAULT_SEARCH_ENGINE] === 'content/search/duckduckgo.xml') { - data.settings[settings.DEFAULT_SEARCH_ENGINE] = 'DuckDuckGo' - } + loaded = true + } catch (e) { + // Session state might be corrupted; let's backup this + // corrupted value for people to report into support. + module.exports.backupSession() + if (data) { + console.log('could not parse data: ', data, e) } + data = exports.defaultAppState() + } + + if (loaded) { + data = module.exports.runPreMigrations(data) + // Clean app data here if it wasn't cleared on shutdown if (data.cleanedOnShutdown !== true || data.lastAppVersion !== app.getVersion()) { data = module.exports.cleanAppData(data, false) } data = Object.assign(module.exports.defaultAppState(), data) data.cleanedOnShutdown = false + // Always recalculate the update status if (data.updates) { const updateStatus = data.updates.status @@ -494,28 +553,12 @@ module.exports.loadAppState = () => { return } } - data = setVersionInformation(data) - - // sites refactoring migration - if (Array.isArray(data.sites) && data.sites.length) { - let sites = {} - let order = 0 - data.sites.forEach((site) => { - let key = siteUtil.getSiteKey(Immutable.fromJS(site)) - site.order = order++ - sites[key] = site - }) - data.sites = sites - } - } catch (e) { - // TODO: Session state is corrupted, maybe we should backup this - // corrupted value for people to report into support. - if (data) { - console.log('could not parse data: ', data, e) - } - data = exports.defaultAppState() - data = setVersionInformation(data) + + data = module.exports.runPostMigrations(data) } + + data = setVersionInformation(data) + locale.init(data.settings[settings.LANGUAGE]).then((locale) => { app.setLocale(locale) resolve(data) @@ -523,6 +566,23 @@ module.exports.loadAppState = () => { }) } +/** + * Called when session is suspected for corruption; this will move it out of the way + */ +module.exports.backupSession = () => { + const src = getStoragePath() + const dest = getTempStoragePath('backup') + + if (fs.existsSync(src)) { + try { + fs.copySync(src, dest) + console.log('An error occurred. For support purposes, file "' + src + '" has been copied to "' + dest + '".') + } catch (e) { + console.log('backupSession: error making copy of session file: ', e) + } + } +} + /** * Obtains the default application level state */ diff --git a/package.json b/package.json index 2444696350a..3befff8a73d 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "file-loader": "^0.8.5", "font-awesome": "^4.5.0", "font-awesome-webpack": "0.0.4", + "fs-extra": "^2.1.2", "immutable": "^3.7.5", "immutablediff": "^0.4.2", "immutablepatch": "brave/immutable-js-patch", diff --git a/test/unit/app/sessionStoreTest.js b/test/unit/app/sessionStoreTest.js index abd3b2faab0..3c2b7b7fcc3 100644 --- a/test/unit/app/sessionStoreTest.js +++ b/test/unit/app/sessionStoreTest.js @@ -11,6 +11,7 @@ require('../braveUnit') describe('sessionStore unit tests', function () { let sessionStore + const fakeElectron = require('../lib/fakeElectron') const fakeAutofill = { init: () => {}, addAutofillAddress: () => {}, @@ -28,7 +29,9 @@ describe('sessionStore unit tests', function () { } const fakeFileSystem = { readFileSync: (path) => { - return '{"cleanedOnShutdown": false}' + return JSON.stringify({ + cleanedOnShutdown: false + }) }, writeFile: (path, options, callback) => { console.log('calling mocked fs.writeFile') @@ -37,6 +40,13 @@ describe('sessionStore unit tests', function () { rename: (oldPath, newPath, callback) => { console.log('calling mocked fs.rename') callback() + }, + copySync: (oldPath, newPath) => { + console.log('calling mocked fs.copySync') + }, + existsSync: (path) => { + console.log('calling mocked fs.existsSync') + return true } } const mockSiteUtil = { @@ -61,8 +71,8 @@ describe('sessionStore unit tests', function () { warnOnUnregistered: false, useCleanCache: true }) - mockery.registerMock('fs', fakeFileSystem) - mockery.registerMock('electron', require('../lib/fakeElectron')) + mockery.registerMock('fs-extra', fakeFileSystem) + mockery.registerMock('electron', fakeElectron) mockery.registerMock('./locale', fakeLocale) mockery.registerMock('../js/state/siteUtil', mockSiteUtil) mockery.registerMock('./autofill', fakeAutofill) @@ -97,35 +107,32 @@ describe('sessionStore unit tests', function () { it('calls cleanAppData', function () { cleanAppDataStub.reset() return sessionStore.saveAppState({}) - .then(function (result) { - assert.equal(cleanAppDataStub.calledOnce, true) - }, function (result) { - console.log('failed: ', result) - assert.fail() - }) + .then(function (result) { + assert.equal(cleanAppDataStub.calledOnce, true) + }, function (result) { + assert.ok(false, 'promise was rejected: ' + JSON.stringify(result)) + }) }) describe('with isShutdown', function () { it('calls cleanSessionDataOnShutdown if true', function () { cleanSessionDataOnShutdownStub.reset() return sessionStore.saveAppState({}, true) - .then(() => { - assert.equal(cleanSessionDataOnShutdownStub.calledOnce, true) - }, function (result) { - console.log('failed: ', result) - assert.fail() - }) + .then(() => { + assert.equal(cleanSessionDataOnShutdownStub.calledOnce, true) + }, function (result) { + assert.ok(false, 'promise was rejected: ' + JSON.stringify(result)) + }) }) it('does not call cleanSessionDataOnShutdown if false', function () { cleanSessionDataOnShutdownStub.reset() return sessionStore.saveAppState({}, false) - .then(() => { - assert.equal(cleanSessionDataOnShutdownStub.notCalled, true) - }, function (result) { - console.log('failed: ', result) - assert.fail() - }) + .then(() => { + assert.equal(cleanSessionDataOnShutdownStub.notCalled, true) + }, function (result) { + assert.ok(false, 'promise was rejected: ' + JSON.stringify(result)) + }) }) }) }) @@ -202,6 +209,22 @@ describe('sessionStore unit tests', function () { assert.equal(clearAutocompleteDataSpy.calledOnce, true) clearAutocompleteDataSpy.restore() }) + + describe('exception is thrown', function () { + let clearAutocompleteDataStub + before(function () { + clearAutocompleteDataStub = sinon.stub(fakeAutofill, 'clearAutocompleteData').throws('lame error') + }) + after(function () { + clearAutocompleteDataStub.restore() + }) + + it('swallows exception', function () { + const data = {} + sessionStore.cleanAppData(data, true) + assert.ok(true) + }) + }) }) describe('when clearAutofillData is true', function () { @@ -437,20 +460,40 @@ describe('sessionStore unit tests', function () { }) }) - it('calls tabState.getPersistentState', function () { - const getPersistentStateSpy = sinon.spy(fakeTabState, 'getPersistentState') - const data = {} - sessionStore.cleanAppData(data) - assert.equal(getPersistentStateSpy.calledOnce, true) - getPersistentStateSpy.restore() + describe('with tabState', function () { + it('calls getPersistentState', function () { + const getPersistentStateSpy = sinon.spy(fakeTabState, 'getPersistentState') + const data = {} + sessionStore.cleanAppData(data) + assert.equal(getPersistentStateSpy.calledOnce, true) + getPersistentStateSpy.restore() + }) + + it('deletes tabState if an exception is thrown', function () { + const getPersistentStateSpy = sinon.stub(fakeTabState, 'getPersistentState').throws('oh noes') + const data = {tabs: true} + const result = sessionStore.cleanAppData(data) + assert.equal(result.tabs, undefined) + getPersistentStateSpy.restore() + }) }) - it('calls windowState.getPersistentState', function () { - const getPersistentStateSpy = sinon.spy(fakeWindowState, 'getPersistentState') - const data = {} - sessionStore.cleanAppData(data) - assert.equal(getPersistentStateSpy.calledOnce, true) - getPersistentStateSpy.restore() + describe('with windowState', function () { + it('calls getPersistentState', function () { + const getPersistentStateSpy = sinon.spy(fakeWindowState, 'getPersistentState') + const data = {} + sessionStore.cleanAppData(data) + assert.equal(getPersistentStateSpy.calledOnce, true) + getPersistentStateSpy.restore() + }) + + it('deletes windowState if an exception is thrown', function () { + const getPersistentStateSpy = sinon.stub(fakeWindowState, 'getPersistentState').throws('oh noes') + const data = {windows: true} + const result = sessionStore.cleanAppData(data) + assert.equal(result.windows, undefined) + getPersistentStateSpy.restore() + }) }) describe('with data.extensions', function () { @@ -461,22 +504,313 @@ describe('sessionStore unit tests', function () { }) describe('loadAppState', function () { + let runPreMigrationsSpy let cleanAppDataStub + let defaultAppStateSpy + let runPostMigrationsSpy + let localeInitSpy + let backupSessionStub + before(function () { + runPreMigrationsSpy = sinon.spy(sessionStore, 'runPreMigrations') cleanAppDataStub = sinon.stub(sessionStore, 'cleanAppData') + defaultAppStateSpy = sinon.spy(sessionStore, 'defaultAppState') + runPostMigrationsSpy = sinon.spy(sessionStore, 'runPostMigrations') + localeInitSpy = sinon.spy(fakeLocale, 'init') + backupSessionStub = sinon.stub(sessionStore, 'backupSession') }) after(function () { cleanAppDataStub.restore() + runPreMigrationsSpy.restore() + defaultAppStateSpy.restore() + runPostMigrationsSpy.restore() + localeInitSpy.restore() + backupSessionStub.restore() + }) + + describe('when reading the session file', function () { + describe('happy path', function () { + let readFileSyncSpy + + before(function () { + readFileSyncSpy = sinon.spy(fakeFileSystem, 'readFileSync') + }) + + after(function () { + readFileSyncSpy.restore() + }) + + it('calls fs.readFileSync', function () { + return sessionStore.loadAppState() + .then(function (result) { + assert.equal(readFileSyncSpy.calledOnce, true) + }, function (result) { + assert.ok(false, 'promise was rejected: ' + JSON.stringify(result)) + }) + }) + }) + + describe('when exception is thrown', function () { + let readFileSyncStub + + before(function () { + readFileSyncStub = sinon.stub(fakeFileSystem, 'readFileSync').throws('error reading file') + }) + + after(function () { + readFileSyncStub.restore() + }) + + it('does not crash when exception thrown during read', function () { + return sessionStore.loadAppState() + .then(function (result) { + assert.ok(result.firstRunTimestamp) + }, function (result) { + assert.ok(false, 'promise was rejected: ' + JSON.stringify(result)) + }) + }) + }) + }) + + describe('when calling JSON.parse', function () { + describe('exception is thrown', function () { + let readFileSyncStub + + before(function () { + readFileSyncStub = sinon.stub(fakeFileSystem, 'readFileSync').returns('this is not valid JSON') + }) + + after(function () { + readFileSyncStub.restore() + }) + + it('does not call runPreMigrations', function () { + runPreMigrationsSpy.reset() + return sessionStore.loadAppState() + .then(function (result) { + assert.equal(runPreMigrationsSpy.notCalled, true) + }, function (result) { + assert.ok(false, 'promise was rejected: ' + JSON.stringify(result)) + }) + }) + + it('does not call cleanAppData', function () { + cleanAppDataStub.reset() + return sessionStore.loadAppState() + .then(function (result) { + assert.equal(cleanAppDataStub.notCalled, true) + }, function (result) { + assert.ok(false, 'promise was rejected: ' + JSON.stringify(result)) + }) + }) + + it('calls defaultAppState to get a default app state', function () { + defaultAppStateSpy.reset() + return sessionStore.loadAppState() + .then(function (result) { + assert.equal(defaultAppStateSpy.calledOnce, true) + }, function (result) { + assert.ok(false, 'promise was rejected: ' + JSON.stringify(result)) + }) + }) + + it('does not call runPostMigrations', function () { + runPostMigrationsSpy.reset() + return sessionStore.loadAppState() + .then(function (result) { + assert.equal(runPostMigrationsSpy.notCalled, true) + }, function (result) { + assert.ok(false, 'promise was rejected: ' + JSON.stringify(result)) + }) + }) + + it('calls backupSessionStub', function () { + backupSessionStub.reset() + return sessionStore.loadAppState() + .then(function (result) { + assert.equal(backupSessionStub.calledOnce, true) + }, function (result) { + assert.ok(false, 'promise was rejected: ' + JSON.stringify(result)) + }) + }) + }) }) - it('calls cleanAppData if data.cleanedOnShutdown !== true', function () { + it('calls runPreMigrations', function () { + runPreMigrationsSpy.reset() return sessionStore.loadAppState() - .then(function (result) { - assert.equal(cleanAppDataStub.calledOnce, true) - }, function (result) { - console.log('failed: ', result) - assert.fail() + .then(function (result) { + assert.equal(runPreMigrationsSpy.calledOnce, true) + }, function (result) { + assert.ok(false, 'promise was rejected: ' + JSON.stringify(result)) + }) + }) + + describe('when checking data.cleanedOnShutdown', function () { + let readFileSyncStub + + describe('when true', function () { + before(function () { + readFileSyncStub = sinon.stub(fakeFileSystem, 'readFileSync').returns(JSON.stringify({ + cleanedOnShutdown: true, + lastAppVersion: fakeElectron.app.getVersion() + })) + }) + after(function () { + readFileSyncStub.restore() + }) + it('does not call cleanAppData', function () { + cleanAppDataStub.reset() + return sessionStore.loadAppState() + .then(function (result) { + assert.equal(cleanAppDataStub.notCalled, true) + }, function (result) { + assert.ok(false, 'promise was rejected: ' + JSON.stringify(result)) + }) + }) + }) + + describe('when NOT true', function () { + before(function () { + readFileSyncStub = sinon.stub(fakeFileSystem, 'readFileSync').returns(JSON.stringify({ + cleanedOnShutdown: false, + lastAppVersion: fakeElectron.app.getVersion() + })) + }) + after(function () { + readFileSyncStub.restore() + }) + it('calls cleanAppData', function () { + cleanAppDataStub.reset() + return sessionStore.loadAppState() + .then(function (result) { + assert.equal(cleanAppDataStub.calledOnce, true) + }, function (result) { + assert.ok(false, 'promise was rejected: ' + JSON.stringify(result)) + }) + }) + }) + }) + + describe('when checking data.lastAppVersion', function () { + let readFileSyncStub + + describe('when it matches app.getVersion', function () { + before(function () { + readFileSyncStub = sinon.stub(fakeFileSystem, 'readFileSync').returns(JSON.stringify({ + cleanedOnShutdown: true, + lastAppVersion: fakeElectron.app.getVersion() + })) + }) + after(function () { + readFileSyncStub.restore() + }) + it('does not call cleanAppData', function () { + cleanAppDataStub.reset() + return sessionStore.loadAppState() + .then(function (result) { + assert.equal(cleanAppDataStub.notCalled, true) + }, function (result) { + assert.ok(false, 'promise was rejected: ' + JSON.stringify(result)) + }) + }) + }) + + describe('when it does NOT match app.getVersion', function () { + before(function () { + readFileSyncStub = sinon.stub(fakeFileSystem, 'readFileSync').returns(JSON.stringify({ + cleanedOnShutdown: true, + lastAppVersion: 'NOT A REAL VERSION' + })) + }) + after(function () { + readFileSyncStub.restore() + }) + it('calls cleanAppData', function () { + cleanAppDataStub.reset() + return sessionStore.loadAppState() + .then(function (result) { + assert.equal(cleanAppDataStub.calledOnce, true) + }, function (result) { + assert.ok(false, 'promise was rejected: ' + JSON.stringify(result)) + }) + }) + }) + }) + + it('calls defaultAppState', function () { + defaultAppStateSpy.reset() + return sessionStore.loadAppState() + .then(function (result) { + assert.equal(defaultAppStateSpy.calledOnce, true) + }, function (result) { + assert.ok(false, 'promise was rejected: ' + JSON.stringify(result)) + }) + }) + + it('calls runPostMigrations', function () { + runPostMigrationsSpy.reset() + return sessionStore.loadAppState() + .then(function (result) { + assert.equal(runPostMigrationsSpy.calledOnce, true) + }, function (result) { + assert.ok(false, 'promise was rejected: ' + JSON.stringify(result)) + }) + }) + + it('calls locale.init', function () { + localeInitSpy.reset() + return sessionStore.loadAppState() + .then(function (result) { + assert.equal(localeInitSpy.calledOnce, true) + }, function (result) { + assert.ok(false, 'promise was rejected: ' + JSON.stringify(result)) + }) + }) + }) + + describe('backupSession', function () { + let copySyncSpy + let existsSyncStub + before(function () { + copySyncSpy = sinon.spy(fakeFileSystem, 'copySync') + }) + after(function () { + copySyncSpy.restore() + }) + + describe('when session exists', function () { + before(function () { + existsSyncStub = sinon.stub(fakeFileSystem, 'existsSync').returns(true) + copySyncSpy.reset() + sessionStore.backupSession() + }) + after(function () { + existsSyncStub.restore() + }) + it('calls fs.existsSync', function () { + assert.equal(existsSyncStub.calledOnce, true) + }) + it('calls fs.copySync', function () { + assert.equal(copySyncSpy.calledOnce, true) + }) + }) + + describe('when session does not exist', function () { + before(function () { + existsSyncStub = sinon.stub(fakeFileSystem, 'existsSync').returns(false) + copySyncSpy.reset() + sessionStore.backupSession() + }) + after(function () { + existsSyncStub.restore() + }) + it('calls fs.existsSync', function () { + assert.equal(existsSyncStub.calledOnce, true) + }) + it('does not call fs.copySync', function () { + assert.equal(copySyncSpy.notCalled, true) }) }) }) diff --git a/test/unit/lib/fakeElectron.js b/test/unit/lib/fakeElectron.js index 338cc608ade..a8822bbbe47 100644 --- a/test/unit/lib/fakeElectron.js +++ b/test/unit/lib/fakeElectron.js @@ -30,7 +30,8 @@ const fakeElectron = { }, getPath: (param) => `${process.cwd()}/${param}`, getVersion: () => '0.14.0', - setLocale: (locale) => {} + setLocale: (locale) => {}, + exit: () => {} }, clipboard: { writeText: function () {