Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

General Storage + Login #18

Merged
merged 19 commits into from Oct 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions e2e/basic-electron.spec.ts
Expand Up @@ -24,8 +24,8 @@ describe('Basic E2E tests', () => {
});

it('renders the initial view', async () => {
const btn = await app.client.$('#accept_button');
const btn = await app.client.$('#create_button');
const text = await btn.getText();
expect(text).toBe('Accept');
expect(text).toBe('Create');
});
});
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -121,6 +121,7 @@
"ts-loader": "8.0.2",
"tsconfig-paths-webpack-plugin": "3.3.0",
"typescript": "4.0.2",
"utility-types": "3.10.0",
"webpack-merge": "5.1.4"
},
"dependencies": {
Expand All @@ -131,6 +132,7 @@
"@reduxjs/toolkit": "1.4.0",
"electron-log": "4.2.4",
"electron-squirrel-startup": "1.0.0",
"electron-store": "6.0.1",
"react": "16.13.0",
"react-dom": "16.13.0",
"react-hot-loader": "4.12.21",
Expand Down
68 changes: 68 additions & 0 deletions src/api/db.spec.ts
@@ -0,0 +1,68 @@
import Store from 'electron-store';

import { DBRequestType } from '@types';

import { handleRequest } from './db';

jest.mock('path');

jest.mock('electron', () => ({
app: {
getPath: jest.fn()
}
}));

jest.mock('fs', () => ({
promises: {
stat: jest.fn().mockImplementation(() => Promise.resolve(false))
}
}));

jest.mock('electron-store', () => {
return jest.fn().mockImplementation(() => ({
get: jest.fn().mockImplementation((key: string) => {
if (key === 'accounts') {
return [];
}
return {};
}),
set: jest.fn()
}));
});

describe('handleRequest', () => {
it('get login state returns logged out correctly', async () => {
const result = await handleRequest({ type: DBRequestType.IS_LOGGED_IN });
expect(result).toBe(false);
});

it('init succesfully initializes the electron-store', async () => {
const result = await handleRequest({ type: DBRequestType.INIT, password: 'password' });
expect(result).toBe(true);
expect(Store).toHaveBeenCalled();
});

it('login succesfully initializes the electron-store', async () => {
const result = await handleRequest({ type: DBRequestType.LOGIN, password: 'password' });
expect(result).toBe(true);
expect(Store).toHaveBeenCalled();
});

it('get new user state returns true default', async () => {
const result = await handleRequest({ type: DBRequestType.IS_NEW_USER });
expect(result).toBe(true);
});

it('get login state returns logged in correctly', async () => {
const initResult = await handleRequest({ type: DBRequestType.INIT, password: 'password' });
expect(initResult).toBe(true);
const result = await handleRequest({ type: DBRequestType.IS_LOGGED_IN });
expect(result).toBe(true);
});

it('get accounts', async () => {
const result = await handleRequest({ type: DBRequestType.GET_ACCOUNTS });
// @todo
expect(result).toStrictEqual([]);
});
});
64 changes: 64 additions & 0 deletions src/api/db.ts
@@ -0,0 +1,64 @@
import { app, ipcMain } from 'electron';
import Store from 'electron-store';
import fs from 'fs';
import path from 'path';

import { IPC_CHANNELS } from '@config';
import { DBRequest, DBRequestType, DBResponse, IAccount } from '@types';

let store: Store;
FrederikBolding marked this conversation as resolved.
Show resolved Hide resolved

const init = (password: string) => {
try {
store = new Store({ encryptionKey: password, clearInvalidConfig: true });
// Write something to the store to actually create the file
store.set('accounts', []);
} catch (err) {
console.error(err);
return false;
}
return true;
};

const login = (password: string) => {
try {
store = new Store({ encryptionKey: password, clearInvalidConfig: false });
} catch (err) {
console.error(err);
return false;
}
return true;
};

const storeExists = async () => {
const configPath = path.join(app.getPath('userData'), 'config.json');
// Is new user if config file doesn't exist
return !!(await fs.promises.stat(configPath).catch(() => false));
};

const isLoggedIn = () => store !== undefined;

const getAccounts = () => {
return store.get('accounts') as IAccount[];
};

export const handleRequest = async (request: DBRequest): Promise<DBResponse> => {
switch (request.type) {
case DBRequestType.INIT:
return Promise.resolve(init(request.password));
case DBRequestType.LOGIN:
return Promise.resolve(login(request.password));
case DBRequestType.IS_LOGGED_IN:
return Promise.resolve(isLoggedIn());
case DBRequestType.IS_NEW_USER:
return !(await storeExists());
case DBRequestType.GET_ACCOUNTS:
return Promise.resolve(getAccounts());
default:
throw new Error('Undefined request type');
}
};

export const runService = () => {
ipcMain.handle(IPC_CHANNELS.DATABASE, (_e, request: DBRequest) => handleRequest(request));
};
30 changes: 30 additions & 0 deletions src/app/App.tsx
@@ -0,0 +1,30 @@
import React, { useEffect } from 'react';

import { Home, Login, NewUser } from './screens';
import { isLoggedIn, isNewUser } from './services';
import { useDispatch, useSelector } from './store';
import { setLoggedIn, setNewUser } from './store/auth';

export const App = () => {
const { loggedIn, newUser } = useSelector((state) => state.auth);
const dispatch = useDispatch();

useEffect(() => {
isNewUser().then((state) => {
dispatch(setNewUser(state));
});
isLoggedIn().then((state) => {
dispatch(setLoggedIn(state));
});
}, []);

if (newUser) {
return <NewUser />;
}

if (loggedIn) {
return <Home />;
}

return <Login />;
};
10 changes: 5 additions & 5 deletions src/app/index.tsx
Expand Up @@ -32,17 +32,17 @@ import ReactDOM from 'react-dom';
import { hot } from 'react-hot-loader';
import { Provider } from 'react-redux';

import { Home } from '@screens';

import { App } from './App';
import { createStore } from './store';

const store = createStore();

const App = () => (
const Root = () => (
<Provider store={store}>
<Home />
<App />
</Provider>
);
const Hot = hot(module)(App);

const Hot = hot(module)(Root);

ReactDOM.render(<Hot />, document.getElementById('root'));
2 changes: 1 addition & 1 deletion src/app/screens/Home.test.tsx
Expand Up @@ -26,7 +26,7 @@ function getComponent() {
}

describe('Home', () => {
test('it renders', async () => {
it('renders', async () => {
const { getByText } = getComponent();
expect(getByText('Accept').textContent).toBeDefined();
});
Expand Down
16 changes: 7 additions & 9 deletions src/app/screens/Home.tsx
Expand Up @@ -28,6 +28,9 @@ export const Home = () => {
}
};

const changePrivateKey = (e: React.ChangeEvent<HTMLInputElement>) =>
setPrivKey(e.currentTarget.value);

return (
<div>
{txQueueLength > 1 && (
Expand All @@ -38,15 +41,10 @@ export const Home = () => {
)}
{currentTx ? <pre>{JSON.stringify(currentTx, null, 2)}</pre> : 'Nothing to sign'}
<br />
<label htmlFor="privkey">
Private Key
<input
id="privkey"
name="privkey"
type="text"
onChange={(e) => setPrivKey(e.currentTarget.value)}
/>
</label>
<label htmlFor="privkey">Private Key</label>
<br />
<input id="privkey" name="privkey" type="text" onChange={changePrivateKey} />

<br />
<button id="deny_button" type="button" disabled={!currentTx} onClick={handleDeny}>
Deny
Expand Down
54 changes: 54 additions & 0 deletions src/app/screens/Login.test.tsx
@@ -0,0 +1,54 @@
import React from 'react';

import { fireEvent, render, waitFor } from '@testing-library/react';
import { Provider } from 'react-redux';

import { login as loginFunc } from '@app/services/DatabaseService';
import { createStore } from '@app/store';

import { Login } from '.';

jest.mock('@app/services/DatabaseService', () => ({
login: jest.fn().mockImplementation((password: string) => password === 'password')
}));

function getComponent() {
return render(
<Provider store={createStore()}>
<Login />
</Provider>
);
}

describe('Login', () => {
it('renders', async () => {
const { getByText } = getComponent();
expect(getByText('Login').textContent).toBeDefined();
});

it('can login', async () => {
const { getByLabelText, getByText } = getComponent();
const passwordInput = getByLabelText('Master Password');
expect(passwordInput).toBeDefined();
fireEvent.change(passwordInput, { target: { value: 'password' } });

const loginButton = getByText('Login');
expect(loginButton).toBeDefined();
fireEvent.click(loginButton);
expect(loginFunc).toHaveBeenCalledWith('password');
});

it('can fail login with wrong password', async () => {
const { getByLabelText, getByText } = getComponent();
const passwordInput = getByLabelText('Master Password');
expect(passwordInput).toBeDefined();
fireEvent.change(passwordInput, { target: { value: 'password1' } });

const loginButton = getByText('Login');
expect(loginButton).toBeDefined();
fireEvent.click(loginButton);
expect(loginFunc).toHaveBeenCalledWith('password');

waitFor(() => expect(getByText('An error occurred')).toBeDefined());
});
});
41 changes: 41 additions & 0 deletions src/app/screens/Login.tsx
@@ -0,0 +1,41 @@
import React, { useState } from 'react';

import { login } from '@app/services/DatabaseService';
import { useDispatch } from '@app/store';
import { setLoggedIn } from '@app/store/auth';

export const Login = () => {
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const dispatch = useDispatch();

const handleLogin = async () => {
try {
const result = await login(password);
dispatch(setLoggedIn(result));
if (!result) {
setError('An error occurred');
}
} catch (err) {
setError(err.message);
}
};

const changePassword = (e: React.ChangeEvent<HTMLInputElement>) =>
setPassword(e.currentTarget.value);

return (
<div>
<label>
Master Password
<input id="password" name="password" type="password" onChange={changePassword} />
</label>
<br />
<button type="button" disabled={password.length === 0} onClick={handleLogin}>
Login
</button>
<br />
{error}
</div>
);
};
40 changes: 40 additions & 0 deletions src/app/screens/NewUser.test.tsx
@@ -0,0 +1,40 @@
import React from 'react';

import { fireEvent, render } from '@testing-library/react';
import { Provider } from 'react-redux';

import { init } from '@app/services/DatabaseService';
import { createStore } from '@app/store';

import { NewUser } from '.';

jest.mock('@app/services/DatabaseService', () => ({
init: jest.fn()
}));

function getComponent() {
return render(
<Provider store={createStore()}>
<NewUser />
</Provider>
);
}

describe('NewUser', () => {
it('renders', async () => {
const { getByText } = getComponent();
expect(getByText('Create').textContent).toBeDefined();
});

it('can login', async () => {
const { getByLabelText, getByText } = getComponent();
const passwordInput = getByLabelText('Enter a new master password');
expect(passwordInput).toBeDefined();
fireEvent.change(passwordInput, { target: { value: 'password' } });

const loginButton = getByText('Create');
expect(loginButton).toBeDefined();
fireEvent.click(loginButton);
expect(init).toHaveBeenCalledWith('password');
});
});