Skip to content

Commit

Permalink
Local session will now download the threat model file when saved.
Browse files Browse the repository at this point in the history
  • Loading branch information
lreading committed Feb 13, 2022
1 parent b481067 commit e3bff53
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 30 deletions.
48 changes: 48 additions & 0 deletions td.vue/src/service/save.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import env from './env.js';

/**
* Saves the threat model using electron specific APIs
* @param {Object} data
* @param {string} fileName
*/
const saveElectron = (data, fileName) => {
console.log(data);
console.log(fileName);
console.warn('Not implemented - TODO');
};

/**
* Saves a local copy from the browser.
* This appears to the user as a download
* @param {Object} data
* @param {string} fileName
*/
const saveLocalBrowser = (data, fileName) => {
const contentType = 'application/json';
const jsonData = JSON.stringify(data, null, 2);
const blob = new Blob([jsonData], { type: contentType });
var a = document.createElement('a');
a.href = window.URL.createObjectURL(blob);
a.download = fileName;
a.click();
a.remove();
};

/**
* Helper that figures out how to save the model.
* There are different flows for electron, vs browser local vs github
* @param {Vuex.Store} store
* @param {Object} data
* @param {string} fileName
*/
const local = (data, fileName) => {
if (env.isElectron()) {
saveElectron(data, fileName);
} else {
saveLocalBrowser(data, fileName);
}
};

export default {
local
};
18 changes: 12 additions & 6 deletions td.vue/src/store/modules/threatmodel.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
THREATMODEL_SELECTED,
THREATMODEL_SET_IMMUTABLE_COPY
} from '../actions/threatmodel.js';
import save from '../../service/save.js';
import threatmodelApi from '../../service/api/threatmodelApi.js';

export const clearState = (state) => {
Expand Down Expand Up @@ -84,12 +85,17 @@ const actions = {
// TODO: This ONLY works if the backend provider is GitHub
// We need a separate code flow for localSession
// localSession needs to handle both a "download" type feature as well as saving to disk in electron
await threatmodelApi.updateAsync(
rootState.repo.selected,
rootState.branch.selected,
state.data.summary.title,
state.data
);

if (getProviderType(rootState.provider.selected) !== providerTypes.local) {
await threatmodelApi.updateAsync(
rootState.repo.selected,
rootState.branch.selected,
state.data.summary.title,
state.data
);
} else {
save.local(state.data, `${state.data.summary.title}.json`);
}
Vue.$toast.success(i18n.get().t('threatmodel.saved'));
dispatch(THREATMODEL_SET_IMMUTABLE_COPY);
} catch (ex) {
Expand Down
43 changes: 43 additions & 0 deletions td.vue/tests/unit/service/save.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import env from '@/service/env.js';
import save from '@/service/save.js';

describe('service/save.js', () => {
const data = { foo: 'bar' };
const name = 'test.json';

describe('browser', () => {
let mockAnchor;
beforeEach(() => {
mockAnchor = {
click: jest.fn(),
remove: jest.fn()
};
env.isElectron = jest.fn().mockReturnValue(false);
window.URL = {
createObjectURL: jest.fn()
};
document.createElement = jest.fn().mockReturnValue(mockAnchor);
save.local(data, name);
});

it('creates the object url', () => {
expect(window.URL.createObjectURL).toHaveBeenCalledTimes(1);
});

it('clicks the link', () => {
expect(mockAnchor.click).toHaveBeenCalledTimes(1);
});

it('removes the anchor', () => {
expect(mockAnchor.remove).toHaveBeenCalledTimes(1);
});
});

describe('electron', () => {
beforeEach(() => {
env.isElectron = jest.fn().mockReturnValue(true);
});

// TODO
});
});
63 changes: 39 additions & 24 deletions td.vue/tests/unit/store/modules/threatmodel.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Vue from 'vue';

import save from '@/service/save.js';
import {
THREATMODEL_CLEAR,
THREATMODEL_CREATE,
Expand Down Expand Up @@ -205,38 +206,52 @@ describe('store/modules/threatmodel.js', () => {
};
});

describe('without error', () => {
describe('local provider', () => {
beforeEach(async () => {
jest.spyOn(threatmodelApi, 'updateAsync').mockResolvedValue({ data });
save.local = jest.fn();
mocks.rootState.provider.selected = 'local';
await threatmodelModule.actions[THREATMODEL_SAVE](mocks, 'tm');
});

it('dispatches the set immutable copy event', () => {
expect(mocks.dispatch).toHaveBeenCalledWith(THREATMODEL_SET_IMMUTABLE_COPY);
});

it('calls the updateAsync api', () => {
expect(threatmodelApi.updateAsync).toHaveBeenCalledTimes(1);
});

it('shows a toast success message', () => {
expect(Vue.$toast.success).toHaveBeenCalledTimes(1);
it('saves the file locally', () => {
expect(save.local).toHaveBeenCalledTimes(1);
});
});

describe('with API error', () => {
beforeEach(async () => {
jest.spyOn(threatmodelApi, 'updateAsync').mockRejectedValue({ data });
console.error = jest.fn();
await threatmodelModule.actions[THREATMODEL_SAVE](mocks, 'tm');
});

it('logs the error', () => {
expect(console.error).toHaveBeenCalledTimes(2);
describe('git provider', () => {
describe('without error', () => {
beforeEach(async () => {
jest.spyOn(threatmodelApi, 'updateAsync').mockResolvedValue({ data });
await threatmodelModule.actions[THREATMODEL_SAVE](mocks, 'tm');
});

it('dispatches the set immutable copy event', () => {
expect(mocks.dispatch).toHaveBeenCalledWith(THREATMODEL_SET_IMMUTABLE_COPY);
});

it('calls the updateAsync api', () => {
expect(threatmodelApi.updateAsync).toHaveBeenCalledTimes(1);
});

it('shows a toast success message', () => {
expect(Vue.$toast.success).toHaveBeenCalledTimes(1);
});
});

it('shows a toast error message', () => {
expect(Vue.$toast.error).toHaveBeenCalledTimes(1);

describe('with API error', () => {
beforeEach(async () => {
jest.spyOn(threatmodelApi, 'updateAsync').mockRejectedValue({ data });
console.error = jest.fn();
await threatmodelModule.actions[THREATMODEL_SAVE](mocks, 'tm');
});

it('logs the error', () => {
expect(console.error).toHaveBeenCalledTimes(2);
});

it('shows a toast error message', () => {
expect(Vue.$toast.error).toHaveBeenCalledTimes(1);
});
});
});
});
Expand Down

0 comments on commit e3bff53

Please sign in to comment.