diff --git a/app/mobile/src/api/fetchUtil.js b/app/mobile/src/api/fetchUtil.js
index e4eb445d..407e98ca 100644
--- a/app/mobile/src/api/fetchUtil.js
+++ b/app/mobile/src/api/fetchUtil.js
@@ -2,6 +2,11 @@ const TIMEOUT = 15000;
//await new Promise(r => setTimeout(r, 2000));
+export function createWebsocket(url) {
+console.log("REAL WEBSOCKET!");
+ return new WebSocket(url);
+}
+
export function checkResponse(response) {
if(response.status >= 400 && response.status < 600) {
throw new Error(response.url + " failed");
diff --git a/app/mobile/src/context/useAppContext.hook.js b/app/mobile/src/context/useAppContext.hook.js
index eb443cbc..ea6c096c 100644
--- a/app/mobile/src/context/useAppContext.hook.js
+++ b/app/mobile/src/context/useAppContext.hook.js
@@ -1,26 +1,25 @@
import { useEffect, useState, useRef, useContext } from 'react';
-import { getAvailable } from 'api/getAvailable';
import { setLogin } from 'api/setLogin';
import { clearLogin } from 'api/clearLogin';
import { removeProfile } from 'api/removeProfile';
import { setAccountAccess } from 'api/setAccountAccess';
import { addAccount } from 'api/addAccount';
-import { getUsername } from 'api/getUsername';
+import { createWebsocket } from 'api/fetchUtil';
import { StoreContext } from 'context/StoreContext';
import { AccountContext } from 'context/AccountContext';
import { ProfileContext } from 'context/ProfileContext';
import { CardContext } from 'context/CardContext';
import { ChannelContext } from 'context/ChannelContext';
-import { getVersion, getApplicationName, getDeviceId } from 'react-native-device-info';
+import { getVersion, getApplicationName, getDeviceId } from 'react-native-device-info'
import messaging from '@react-native-firebase/messaging';
export function useAppContext() {
const [state, setState] = useState({
session: null,
- loginTimestamp: null,
- disconnected: null,
- deviceToken: null,
+ status: 'disconnected',
+ first: true,
loggingOut: false,
+ adminToken: null,
version: getVersion(),
});
const store = useContext(StoreContext);
@@ -28,41 +27,38 @@ export function useAppContext() {
const profile = useContext(ProfileContext);
const card = useContext(CardContext);
const channel = useContext(ChannelContext);
- const count = useRef(0);
const delay = useRef(0);
const ws = useRef(null);
+ const deviceToken = useRef(null);
+ const access = useRef(null);
+ const init = useRef(false);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }))
}
useEffect(() => {
- messaging().getToken().then(token => {
- updateState({ deviceToken: token });
- })
-
- init();
+ (async () => {
+ deviceToken.current = await messaging().getToken();
+ access.current = await store.actions.init();
+ if (access.current) {
+ await setSession(access.current);
+ }
+ else {
+ updateState({ session: false });
+ }
+ init.current = true;
+ })();
}, []);
- const init = async () => {
- const access = await store.actions.init();
- if (access) {
- await setSession(access);
- }
- else {
- updateState({ session: false });
- }
- }
-
- const setSession = async (access) => {
- await account.actions.setSession(access);
- await profile.actions.setSession(access);
- await card.actions.setSession(access);
- await channel.actions.setSession(access);
- updateState({ session: true, server: access.server, token: access.appToken,
- loginTimestamp: access.created });
- setWebsocket(access.server, access.appToken);
+ const setSession = async () => {
+ updateState({ session: true });
+ await account.actions.setSession(access.current);
+ await profile.actions.setSession(access.current);
+ await card.actions.setSession(access.current);
+ await channel.actions.setSession(access.current);
+ setWebsocket(access.current);
}
const clearSession = async () => {
@@ -84,41 +80,54 @@ export function useAppContext() {
];
const actions = {
- available: getAvailable,
- username: getUsername,
create: async (server, username, password, token) => {
+ if (!init.current || access.current) {
+ throw new Error('invalid session state');
+ }
await addAccount(server, username, password, token);
- const access = await setLogin(username, server, password, getApplicationName(), getVersion(), getDeviceId(), state.deviceToken, notifications)
- await store.actions.setSession({ ...access, server});
- await setSession({ ...access, server });
- if (access.pushSupported) {
+ const session = await setLogin(username, server, password, getApplicationName(), getVersion(), getDeviceId(), deviceToken.current, notifications)
+ access.current = { server, token: session.appToken, guid: session.guid };
+ await store.actions.setSession(access.current);
+ await setSession();
+ if (session.pushSupported) {
messaging().requestPermission().then(status => {})
}
},
access: async (server, token) => {
- const access = await setAccountAccess(server, token, getApplicationName(), getVersion(), getDeviceId(), state.deviceToken, notifications);
- await store.actions.setSession({ ...access, server});
- await setSession({ ...access, server });
- if (access.pushSupported) {
+ if (!init.current || access.current) {
+ throw new Error('invalid session state');
+ }
+ const session = await setAccountAccess(server, token, getApplicationName(), getVersion(), getDeviceId(), deviceToken.current, notifications);
+ access.current = { server, token: session.appToken, guid: session.guid };
+ await store.actions.setSession(access.current);
+ await setSession();
+ if (session.pushSupported) {
messaging().requestPermission().then(status => {})
}
},
login: async (username, password) => {
+ if (!init.current || access.current) {
+ throw new Error('invalid session state');
+ }
const acc = username.split('@');
- const access = await setLogin(acc[0], acc[1], password, getApplicationName(), getVersion(), getDeviceId(), state.deviceToken, notifications)
- await store.actions.setSession({ ...access, server: acc[1]});
- await setSession({ ...access, server: acc[1] });
- if (access.pushSupported) {
+ const session = await setLogin(acc[0], acc[1], password, getApplicationName(), getVersion(), getDeviceId(), deviceToken.current, notifications)
+ access.current = { server: acc[1], token: session.appToken, guid: session.guid };
+ await store.actions.setSession(access.current);
+ await setSession();
+ if (session.pushSupported) {
messaging().requestPermission().then(status => {})
}
},
logout: async () => {
+ if (!access.current) {
+ throw new Error('invalid session state');
+ }
updateState({ loggingOut: true });
try {
await messaging().deleteToken();
const token = await messaging().getToken();
updateState({ deviceToken: token });
- await clearLogin(state.server, state.appToken);
+ await clearLogin(state.server, state.token);
}
catch (err) {
console.log(err);
@@ -128,39 +137,39 @@ export function useAppContext() {
updateState({ loggingOut: false });
},
remove: async () => {
- await removeProfile(state.server, state.appToken);
+ if (!access.current) {
+ throw new Error('invalid session state');
+ }
+ const { server, token } = access.current;
+ await removeProfile(server, token);
await clearSession();
await store.actions.clearSession();
},
}
- const setWebsocket = (server, token) => {
- clearWebsocket();
- ws.current = new WebSocket(`wss://${server}/status`);
+ const setWebsocket = (session) => {
+ ws.current = createWebsocket(`wss://${session.server}/status`);
ws.current.onmessage = (ev) => {
- delay.current = 0;
try {
+ delay.current = 0;
const rev = JSON.parse(ev.data);
- try {
- profile.actions.setRevision(rev.profile);
- account.actions.setRevision(rev.account);
- channel.actions.setRevision(rev.channel);
- card.actions.setRevision(rev.card);
- }
- catch(err) {
- console.log(err);
- }
- count.current = 0;
- updateState({ disconnected: count.current });
+ updateState({ first: false, status: 'connected' });
+ profile.actions.setRevision(rev.profile);
+ account.actions.setRevision(rev.account);
+ channel.actions.setRevision(rev.channel);
+ card.actions.setRevision(rev.card);
}
catch (err) {
console.log(err);
}
}
+ ws.current.onopen = () => {
+ ws.current.send(JSON.stringify({ AppToken: session.token }))
+ }
ws.current.onclose = (e) => {
- count.current += 1;
- updateState({ disconnected: count.current });
console.log(e)
+ count.current += 1;
+ updateState({ status: 'disconnected' });
setTimeout(() => {
if (ws.current != null) {
ws.current.onmessage = () => {}
@@ -168,25 +177,23 @@ export function useAppContext() {
ws.current.onopen = () => {}
ws.current.onerror = () => {}
delay.current = 1;
- setWebsocket(server, token);
+ setWebsocket(session);
}
}, 1000 * delay.current)
}
- ws.current.onopen = () => {
- ws.current.send(JSON.stringify({ AppToken: token }))
- }
ws.current.error = (e) => {
- count.current += 1;
- updateState({ disconnected: count.current });
- console.log(e)
+ console.log(e);
+ ws.current.close();
}
}
const clearWebsocket = () => {
if (ws.current) {
- ws.current.onclose = () => {}
- ws.current.close()
- ws.current = null
+ ws.current.onmessage = () => {};
+ ws.current.onclose = () => {};
+ ws.current.onerror = () => {};
+ ws.current.close();
+ ws.current = null;
}
}
diff --git a/app/mobile/test/App.test.js b/app/mobile/test/App.test.js
new file mode 100644
index 00000000..93bcb399
--- /dev/null
+++ b/app/mobile/test/App.test.js
@@ -0,0 +1,133 @@
+import React, { useState, useEffect, useContext } from 'react';
+import { View, Text } from 'react-native';
+import { useTestStoreContext } from './useTestStoreContext.hook';
+import {render, act, screen, waitFor, fireEvent} from '@testing-library/react-native'
+import * as fetchUtil from 'api/fetchUtil';
+import { UploadContextProvider } from 'context/UploadContext';
+import { AppContext, AppContextProvider } from 'context/AppContext';
+import { AccountContextProvider } from 'context/AccountContext';
+import { ProfileContextProvider } from 'context/ProfileContext';
+import { CardContextProvider } from 'context/CardContext';
+import { ChannelContextProvider } from 'context/ChannelContext';
+import { StoreContext } from 'context/StoreContext';
+
+function AppView() {
+ const [renderCount, setRenderCount] = useState(0);
+ const app = useContext(AppContext);
+
+ useEffect(() => {
+ setRenderCount(renderCount + 1);
+ }, [app.state]);
+
+ return (
+
+ { app.state.session }
+ { app.state.status }
+
+ );
+}
+
+function AppTestApp() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+let mockWebsocket;
+function MockWebsocket(url) {
+ this.url = url;
+ this.sent = false;
+ this.send = (msg) => { this.sent = true };
+};
+jest.mock('@react-native-firebase/messaging', () => () => ({ getToken: async () => '##' }));
+
+jest.mock('react-native-device-info', () => ({
+ getVersion: () => '##',
+ getApplicationName: () => '##',
+ getDeviceId: () => '##',
+}));
+
+const realUseContext = React.useContext;
+const realCreateWebsocket = fetchUtil.createWebsocket;
+const realFetchWithTimeout = fetchUtil.fetchWithTimeout;
+const realFetchWithCustomTimeout = fetchUtil.fetchWithCustomTimeout;
+beforeEach(() => {
+ const mockCreateWebsocket = jest.fn().mockImplementation((url) => {
+ mockWebsocket = new MockWebsocket(url);
+ return mockWebsocket;
+ });
+
+ const mockUseContext = jest.fn().mockImplementation((ctx) => {
+ if (ctx === StoreContext) {
+ return useTestStoreContext();
+ }
+ return realUseContext(ctx);
+ });
+ React.useContext = mockUseContext;
+
+ const mockFetch = jest.fn().mockImplementation((url, options) => {
+ return Promise.resolve({
+ json: () => Promise.resolve([])
+ });
+ });
+ fetchUtil.fetchWithTimeout = mockFetch;
+ fetchUtil.fetchWithCustomTimeout = mockFetch;
+ fetchUtil.createWebsocket = mockCreateWebsocket;
+});
+
+afterEach(() => {
+ React.useContext = realUseContext;
+ fetchUtil.fetchWithTimeout = realFetchWithTimeout;
+ fetchUtil.fetchWithCustomTimeout = realFetchWithCustomTimeout;
+ fetchUtil.createWebsocket = realCreateWebsocket;
+});
+
+test('testing', async () => {
+ render()
+
+ await waitFor(async () => {
+ expect(screen.getByTestId('session').props.children).toBe(false);
+ });
+
+ await act(async () => {
+ const app = screen.getByTestId('app').props.app;
+ app.actions.login('testlogin', 'testpassword');
+ });
+
+ await waitFor(async () => {
+ expect(mockWebsocket).not.toBe(undefined);
+ expect(mockWebsocket?.onopen).not.toBe(null);
+ expect(mockWebsocket?.onmessage).not.toBe(null);
+ expect(mockWebsocket?.onclose).not.toBe(null);
+ });
+
+ await act(async () => {
+ mockWebsocket.onopen();
+ });
+
+ await waitFor(async () => {
+ expect(mockWebsocket.sent).toBe(true);
+ });
+
+ await act(async () => {
+ mockWebsocket.onmessage({ data: JSON.stringify({ account: 1, profile: 1, card: 1, channel: 1 }) });
+ });
+
+ await waitFor(async () => {
+ expect(screen.getByTestId('status').props.children).toBe('connected');
+ });
+
+});
+
diff --git a/app/mobile/test/useTestStoreContext.hook.js b/app/mobile/test/useTestStoreContext.hook.js
index 6f8f4fb1..477df2ff 100644
--- a/app/mobile/test/useTestStoreContext.hook.js
+++ b/app/mobile/test/useTestStoreContext.hook.js
@@ -20,8 +20,7 @@ export function useTestStoreContext() {
const actions = {
init: async () => {
- console.log("TEST STORE INIT");
- return {};
+ return null;
},
setSession: async (access) => {
},
diff --git a/net/web/src/context/useAppContext.hook.js b/net/web/src/context/useAppContext.hook.js
index 17991f25..7420b7dc 100644
--- a/net/web/src/context/useAppContext.hook.js
+++ b/net/web/src/context/useAppContext.hook.js
@@ -198,7 +198,6 @@ export function useAppContext(websocket) {
ws.current.error = (e) => {
console.log(e)
ws.current.close();
- updateState({ status: 'disconnected' });
}
}