Skip to content

Commit

Permalink
Merge pull request #30 from akvo/feature/27-add-user-page
Browse files Browse the repository at this point in the history
Feature/27 add user page
  • Loading branch information
wayangalihpratama committed Jun 30, 2023
2 parents 30106ed + e6b6b45 commit e8a687e
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 13 deletions.
14 changes: 14 additions & 0 deletions app/__mocks__/expo-crypto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const mockDigestStringAsync = jest.fn();

export const Crypto = {
CryptoDigestAlgorithm: {
SHA1: 'SHA1',
},
digestStringAsync: mockDigestStringAsync,
};

export const digestStringAsync = (algorithm, data) => {
return Promise.resolve(`Mocked${data}-${algorithm}`);
};

export const CryptoDigestAlgorithm = Crypto.CryptoDigestAlgorithm;
37 changes: 28 additions & 9 deletions app/src/pages/AddUser.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { View, ToastAndroid } from 'react-native';
import { View, ToastAndroid, Platform } from 'react-native';
import { ListItem, Button, Input } from '@rneui/themed';
import { Formik, ErrorMessage } from 'formik';
import * as Crypto from 'expo-crypto';
Expand Down Expand Up @@ -29,12 +29,15 @@ const AddUser = ({ navigation }) => {
UserState.update((s) => {
s.id = id;
});

ToastAndroid.show('Success!', ToastAndroid.SHORT);
if (Platform.OS === 'android') {
ToastAndroid.show('Success!', ToastAndroid.SHORT);
}
setLoading(false);
})
.catch(() => {
ToastAndroid.show('Unable to save the data to the database', ToastAndroid.LONG);
if (Platform.OS === 'android') {
ToastAndroid.show('Unable to save the data to the database', ToastAndroid.LONG);
}
setLoading(false);
});
};
Expand All @@ -49,12 +52,16 @@ const AddUser = ({ navigation }) => {
s.id = insertId;
});
}
ToastAndroid.show('Success!', ToastAndroid.SHORT);

if (Platform.OS === 'android') {
ToastAndroid.show('Success!', ToastAndroid.SHORT);
}
setLoading(false);
})
.catch(() => {
ToastAndroid.show('Unable to save the data to the database', ToastAndroid.LONG);
if (Platform.OS === 'android') {
ToastAndroid.show('Unable to save the data to the database', ToastAndroid.LONG);
}
setLoading(false);
});
};
Expand Down Expand Up @@ -131,7 +138,9 @@ const AddUser = ({ navigation }) => {
}
})
.catch(() => {
ToastAndroid.show('Unable to load profile', ToastAndroid.LONG);
if (Platform.OS === 'android') {
ToastAndroid.show('Unable to load profile', ToastAndroid.LONG);
}
});
}, []);

Expand All @@ -154,6 +163,7 @@ const AddUser = ({ navigation }) => {
onChangeText={(value) => handleNameChange(formik.handleChange, value)}
value={name}
errorMessage={<ErrorMessage name="name" />}
testID="input-name"
/>
</ListItem.Content>
</ListItem>
Expand All @@ -166,6 +176,7 @@ const AddUser = ({ navigation }) => {
onChangeText={(value) => handlePasswordChange(formik.handleChange, value)}
value={password}
errorMessage={<ErrorMessage name="password" />}
testID="input-password"
/>
</ListItem.Content>
</ListItem>
Expand All @@ -178,17 +189,25 @@ const AddUser = ({ navigation }) => {
onChangeText={formik.handleChange('confirmPassword')}
value={formik.confirmPassword}
errorMessage={<ErrorMessage name="confirmPassword" />}
testID="input-confirm-password"
/>
</ListItem.Content>
</ListItem>

<View
style={{ display: 'flex', flexDirection: 'column', gap: 8, paddingHorizontal: 16 }}
>
<Button onPress={formik.handleSubmit} loading={loading}>
<Button onPress={formik.handleSubmit} loading={loading} testID="button-save">
{loading ? 'Saving...' : 'Save'}
</Button>
{userID && <Button title="Go to Dashboard" type="outline" onPress={goToHome} />}
{userID && (
<Button
title="Go to Dashboard"
type="outline"
onPress={goToHome}
testID="button-dashboard"
/>
)}
</View>
</BaseLayout.Content>
)}
Expand Down
174 changes: 174 additions & 0 deletions app/src/pages/__tests__/AddUser.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,184 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { render, renderHook, fireEvent, act, waitFor } from '@testing-library/react-native';
import { useNavigation } from '@react-navigation/native';
import * as Crypto from 'expo-crypto';

import AddUser from '../AddUser';
import { UserState } from '../../store';
import { conn, query } from '../../database';

jest.mock('@react-navigation/native', () => {
return {
useNavigation: () => ({
navigate: jest.fn(),
}),
};
});

jest.mock('expo-sqlite');

jest.mock('expo-crypto');

db = conn.init;

describe('AddUserPage', () => {
test('renders correctly', () => {
const tree = renderer.create(<AddUser />).toJSON();
expect(tree).toMatchSnapshot();
});
test('only username is required', async () => {
const { result: navigationRef } = renderHook(() => useNavigation());
const navigation = navigationRef.current;
const { getByText, getByTestId } = render(<AddUser navigation={navigation} />);
const saveButton = getByTestId('button-save');
expect(saveButton).toBeDefined();
fireEvent.press(saveButton);

await waitFor(() => {
const errorText = getByText('Username is required');
expect(errorText).toBeDefined();
});
});

test('confirm password not matched', async () => {
const { result: navigationRef } = renderHook(() => useNavigation());
const navigation = navigationRef.current;
const { getByText, getByTestId } = render(<AddUser navigation={navigation} />);

const passwordEl = getByTestId('input-password');
expect(passwordEl).toBeDefined();
const confirmPassEl = getByTestId('input-confirm-password');
expect(confirmPassEl).toBeDefined();

const pass1 = 'secret';
fireEvent.changeText(passwordEl, pass1);
const pass2 = 'Hello';
fireEvent.changeText(confirmPassEl, pass2);

const saveButton = getByTestId('button-save');
expect(saveButton).toBeDefined();
fireEvent.press(saveButton);

await waitFor(() => {
const errorText = getByText('Passwords must match');
expect(errorText).toBeDefined();
});
});

test('create username correctly', async () => {
const { result: navigationRef } = renderHook(() => useNavigation());
const navigation = navigationRef.current;
const { getByTestId } = render(<AddUser navigation={navigation} />);
const { result: userStateRef } = renderHook(() => UserState.useState((s) => s));

const usernameEl = getByTestId('input-name');
expect(usernameEl).toBeDefined();

const usernameVal = 'Jhon';
fireEvent.changeText(usernameEl, usernameVal);

const saveButton = getByTestId('button-save');
expect(saveButton).toBeDefined();
fireEvent.press(saveButton);

act(() => {
const insertQuery = query.insert('users', { id: 1, name: usernameVal });
conn.tx(db, insertQuery);

UserState.update((s) => {
s.id = 1;
s.name = usernameVal;
});
});

await waitFor(() => {
const { name: usernameState } = userStateRef.current;
expect(usernameEl.props.value).toBe(usernameVal);
expect(usernameState).toEqual(usernameVal);

const dashboardButton = getByTestId('button-dashboard');
expect(dashboardButton).toBeDefined();
});

const userData = [
{
id: 1,
name: usernameVal,
},
];
const mockSelectSql = jest.fn((query, params, successCallback) => {
successCallback(null, { rows: { length: userData.length, _array: userData } });
});
db.transaction.mockImplementation((transactionFunction) => {
transactionFunction({
executeSql: mockSelectSql,
});
});

const selectQuery = query.read('users', { id: 1 });
const resultSet = await conn.tx(db, selectQuery, [1]);
expect(resultSet.rows).toHaveLength(userData.length);
expect(resultSet.rows._array).toEqual(userData);
});

test('update password correctly', async () => {
const { result: navigationRef } = renderHook(() => useNavigation());
const navigation = navigationRef.current;
const { getByTestId } = render(<AddUser navigation={navigation} />);
const { result: userStateRef } = renderHook(() => UserState.useState((s) => s));

const passwordEl = getByTestId('input-password');
expect(passwordEl).toBeDefined();

const passwordVal = 'secret';
fireEvent.changeText(passwordEl, passwordVal);

const saveButton = getByTestId('button-save');
expect(saveButton).toBeDefined();
fireEvent.press(saveButton);

const passEncrypted = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA1,
passwordVal,
);

act(() => {
UserState.update((s) => {
s.password = passwordVal;
});
const updateQuery = query.update('users', { id: 1 }, { password: passEncrypted });
conn.tx(db, updateQuery);
});

await waitFor(() => {
const { password: passwordState } = userStateRef.current;
expect(passwordEl.props.value).toBe(passwordVal);
expect(passwordState).toEqual(passwordVal);
});

const userData = [
{
id: 1,
name: 'Jhon',
password: passEncrypted,
},
];
const mockSelectSql = jest.fn((query, params, successCallback) => {
successCallback(null, { rows: { length: userData.length, _array: userData } });
});
db.transaction.mockImplementation((transactionFunction) => {
transactionFunction({
executeSql: mockSelectSql,
});
});

const selectQuery = query.read('users', { id: 1 });
const resultSet = await conn.tx(db, selectQuery, [1]);

expect(resultSet.rows).toHaveLength(userData.length);

const userDB = resultSet.rows._array[0];
expect(userDB?.password).toEqual(passEncrypted);
});
});
7 changes: 4 additions & 3 deletions app/src/pages/__tests__/__snapshots__/AddUser.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ exports[`AddUserPage renders correctly 1`] = `
"minHeight": 40,
}
}
testID="RNE__Input__text-input"
testID="input-name"
underlineColorAndroid="transparent"
value=""
/>
Expand Down Expand Up @@ -602,7 +602,7 @@ exports[`AddUserPage renders correctly 1`] = `
"minHeight": 40,
}
}
testID="RNE__Input__text-input"
testID="input-password"
underlineColorAndroid="transparent"
value=""
/>
Expand Down Expand Up @@ -772,7 +772,7 @@ exports[`AddUserPage renders correctly 1`] = `
"minHeight": 40,
}
}
testID="RNE__Input__text-input"
testID="input-confirm-password"
underlineColorAndroid="transparent"
/>
</View>
Expand Down Expand Up @@ -849,6 +849,7 @@ exports[`AddUserPage renders correctly 1`] = `
"opacity": 1,
}
}
testID="button-save"
>
<View
style={
Expand Down
1 change: 0 additions & 1 deletion app/src/store/__tests__/buildParams.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ describe('BuildParamsState', () => {
appVersion,
} = result.current;
expect(authenticationType).toEqual(['code_assignment', 'username', 'password']);
expect(serverURL).toBe('https://api.example.com/nmis');
expect(debugMode).toBe(false);
expect(dataSyncInterval).toBe(300);
expect(errorHandling).toBe(true);
Expand Down

0 comments on commit e8a687e

Please sign in to comment.