From a0923a375370a48d81d7cde58d4e00073812e57a Mon Sep 17 00:00:00 2001 From: obr42 <107439850+obr42@users.noreply.github.com> Date: Fri, 30 Sep 2022 16:43:25 +0000 Subject: [PATCH] #153 Add WebSocket Container (#154) --- package-lock.json | 339 ++++++++++++++++++++++++ package.json | 2 + src/containers/AuthContainer.tsx | 5 +- src/containers/DebugContainer.tsx | 18 +- src/containers/SocketContainer.test.tsx | 102 +++++++ src/containers/SocketContainer.tsx | 64 +++++ src/index.tsx | 9 +- src/test/testMocks.tsx | 37 ++- src/types/config-types.ts | 7 + 9 files changed, 567 insertions(+), 16 deletions(-) create mode 100644 src/containers/SocketContainer.test.tsx create mode 100644 src/containers/SocketContainer.tsx diff --git a/package-lock.json b/package-lock.json index 8fbef55f..03fa596b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "@babel/eslint-parser": "^7.18.2", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^12.1.5", + "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", "@types/lodash": "^4.14.180", @@ -72,6 +73,7 @@ "eslint-plugin-simple-import-sort": "^7.0.0", "husky": "^7.0.4", "jest": "^27.5.1", + "jest-websocket-mock": "^2.4.0", "lint-prepush": "^2.2.1", "prettier": "^2.7.1", "react-app-rewire-webpack-bundle-analyzer": "^1.1.0", @@ -4737,6 +4739,36 @@ "react-dom": "<18.0.0" } }, + "node_modules/@testing-library/react-hooks": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz", + "integrity": "sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "react-error-boundary": "^3.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0", + "react": "^16.9.0 || ^17.0.0", + "react-dom": "^16.9.0 || ^17.0.0", + "react-test-renderer": "^16.9.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-test-renderer": { + "optional": true + } + } + }, "node_modules/@testing-library/user-event": { "version": "13.5.0", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", @@ -13433,6 +13465,152 @@ "node": ">=8" } }, + "node_modules/jest-websocket-mock": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jest-websocket-mock/-/jest-websocket-mock-2.4.0.tgz", + "integrity": "sha512-AOwyuRw6fgROXHxMOiTDl1/T4dh3fV4jDquha5N0csS/PNp742HeTZWPAuKppVRSQ8s3fUGgJHoyZT9JDO0hMA==", + "dev": true, + "dependencies": { + "jest-diff": "^28.0.2", + "mock-socket": "^9.1.0" + } + }, + "node_modules/jest-websocket-mock/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-websocket-mock/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-websocket-mock/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-websocket-mock/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-websocket-mock/node_modules/diff-sequences": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", + "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-websocket-mock/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-websocket-mock/node_modules/jest-diff": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", + "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^28.1.1", + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-websocket-mock/node_modules/jest-get-type": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", + "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-websocket-mock/node_modules/pretty-format": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", + "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", + "dev": true, + "dependencies": { + "@jest/schemas": "^28.1.3", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-websocket-mock/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-websocket-mock/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/jest-websocket-mock/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -14635,6 +14813,15 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mock-socket": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.1.5.tgz", + "integrity": "sha512-3DeNIcsQixWHHKk6NdoBhWI4t1VMj5/HzfnI1rE/pLl5qKx7+gd4DNA07ehTaZ6MoUU053si6Hd+YtiM/tQZfg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -17255,6 +17442,22 @@ "react": "17.0.2" } }, + "node_modules/react-error-boundary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-error-overlay": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", @@ -23719,6 +23922,16 @@ "@types/react-dom": "<18.0.0" } }, + "@testing-library/react-hooks": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz", + "integrity": "sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "react-error-boundary": "^3.1.0" + } + }, "@testing-library/user-event": { "version": "13.5.0", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", @@ -30163,6 +30376,117 @@ } } }, + "jest-websocket-mock": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jest-websocket-mock/-/jest-websocket-mock-2.4.0.tgz", + "integrity": "sha512-AOwyuRw6fgROXHxMOiTDl1/T4dh3fV4jDquha5N0csS/PNp742HeTZWPAuKppVRSQ8s3fUGgJHoyZT9JDO0hMA==", + "dev": true, + "requires": { + "jest-diff": "^28.0.2", + "mock-socket": "^9.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "diff-sequences": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", + "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "jest-diff": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", + "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^28.1.1", + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.3" + } + }, + "jest-get-type": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", + "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", + "dev": true + }, + "pretty-format": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", + "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", + "dev": true, + "requires": { + "@jest/schemas": "^28.1.3", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -31106,6 +31430,12 @@ "minimist": "^1.2.6" } }, + "mock-socket": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.1.5.tgz", + "integrity": "sha512-3DeNIcsQixWHHKk6NdoBhWI4t1VMj5/HzfnI1rE/pLl5qKx7+gd4DNA07ehTaZ6MoUU053si6Hd+YtiM/tQZfg==", + "dev": true + }, "moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -32855,6 +33185,15 @@ "scheduler": "^0.20.2" } }, + "react-error-boundary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5" + } + }, "react-error-overlay": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", diff --git a/package.json b/package.json index ea262f87..d01c5c39 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@babel/eslint-parser": "^7.18.2", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^12.1.5", + "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", "@types/lodash": "^4.14.180", @@ -79,6 +80,7 @@ "eslint-plugin-simple-import-sort": "^7.0.0", "husky": "^7.0.4", "jest": "^27.5.1", + "jest-websocket-mock": "^2.4.0", "lint-prepush": "^2.2.1", "prettier": "^2.7.1", "react-app-rewire-webpack-bundle-analyzer": "^1.1.0", diff --git a/src/containers/AuthContainer.tsx b/src/containers/AuthContainer.tsx index f439fb61..10f5ffa8 100644 --- a/src/containers/AuthContainer.tsx +++ b/src/containers/AuthContainer.tsx @@ -1,4 +1,5 @@ import { DebugContainer } from 'containers/DebugContainer' +import { SocketContainer } from 'containers/SocketContainer' import { useMyAxios } from 'hooks/useMyAxios' import { TokenResponse, useToken } from 'hooks/useToken' import { useCallback, useEffect, useState } from 'react' @@ -22,6 +23,7 @@ export interface User extends UserBase { const useAuth = () => { const { DEBUG_LOGIN } = DebugContainer.useContainer() + const { updateSocketToken } = SocketContainer.useContainer() const { axiosInstance } = useMyAxios() const navigate = useNavigate() const [user, _setUser] = useState(null) @@ -79,10 +81,11 @@ const useAuth = () => { setUser(username) setToken({ access, refresh }) + updateSocketToken(access) window.localStorage.setItem(AuthEvents.LOGIN, new Date().toISOString()) }, - [axiosInstance, DEBUG_LOGIN, setUser, setToken], + [axiosInstance, DEBUG_LOGIN, setUser, setToken, updateSocketToken], ) return { diff --git a/src/containers/DebugContainer.tsx b/src/containers/DebugContainer.tsx index 7b3878b7..344eed5b 100644 --- a/src/containers/DebugContainer.tsx +++ b/src/containers/DebugContainer.tsx @@ -1,14 +1,24 @@ +import { DebugSettings } from 'types/config-types' import { createContainer } from 'unstated-next' -const useDebugContainer = () => { - const DEBUG_LOGIN = false - const DEBUG_AUTH = false - const DEBUG_LOCAL_STORAGE = false +const useDebugContainer = ( + initialState: DebugSettings = { + LOGIN: false, + AUTH: false, + LOCAL_STORAGE: false, + SOCKET: false, + }, +) => { + const DEBUG_LOGIN = initialState.LOGIN + const DEBUG_AUTH = initialState.AUTH + const DEBUG_LOCAL_STORAGE = initialState.LOCAL_STORAGE + const DEBUG_SOCKET = initialState.SOCKET return { DEBUG_LOGIN, DEBUG_AUTH, DEBUG_LOCAL_STORAGE, + DEBUG_SOCKET, } } diff --git a/src/containers/SocketContainer.test.tsx b/src/containers/SocketContainer.test.tsx new file mode 100644 index 00000000..f9aacc72 --- /dev/null +++ b/src/containers/SocketContainer.test.tsx @@ -0,0 +1,102 @@ +import { renderHook } from '@testing-library/react-hooks' +import WS from 'jest-websocket-mock' +import { SocketProvider } from 'test/testMocks' + +import { SocketContainer } from './SocketContainer' + +let consoleSpy: jest.SpyInstance + +describe('Socket Container', () => { + beforeEach(() => { + consoleSpy = jest.spyOn(console, 'log') + }) + + afterEach(() => { + consoleSpy.mockClear() + }) + + afterAll(() => { + consoleSpy.mockRestore() + }) + + test('adds function to listener list', () => { + const mockFn = jest.fn() + const { result } = renderHook(() => SocketContainer.useContainer(), { + wrapper: SocketProvider, + }) + result.current.addCallback('testCB', mockFn) + expect(consoleSpy).toHaveBeenCalledWith('Adding testCB socket listener') + }) + + test('does not error adding existing function to listener list', () => { + const mockFn = jest.fn() + const { result } = renderHook(() => SocketContainer.useContainer(), { + wrapper: SocketProvider, + }) + result.current.addCallback('testCB', mockFn) + result.current.addCallback('testCB', mockFn) + expect(consoleSpy).toHaveBeenCalledWith('Adding testCB socket listener') + expect(consoleSpy).toHaveBeenCalledTimes(2) + }) + + test('does not remove non-existent function from listener list if', () => { + const { result } = renderHook(() => SocketContainer.useContainer(), { + wrapper: SocketProvider, + }) + result.current.removeCallback('testCB') + expect(consoleSpy).not.toHaveBeenCalledWith( + 'Removing testCB socket listener', + ) + }) + + test('removes function from listener list', () => { + const mockFn = jest.fn() + const { result } = renderHook(() => SocketContainer.useContainer(), { + wrapper: SocketProvider, + }) + result.current.addCallback('testCB', mockFn) + consoleSpy.mockClear() + result.current.removeCallback('testCB') + expect(consoleSpy).toHaveBeenCalledWith('Removing testCB socket listener') + }) + + describe('Websocket Tests', () => { + let server: WS + + beforeEach(() => { + server = new WS('ws://localhost:2337/api/v1/socket/events') + }) + + afterEach(() => { + WS.clean() + }) + + test('calls listeners on message', async () => { + const testMsg = JSON.stringify({ test: 'This is a test' }) + const mockFn = jest.fn() + const { result } = renderHook(() => SocketContainer.useContainer(), { + wrapper: SocketProvider, + }) + result.current.addCallback('testAdd', mockFn) + await server.connected + server.send(testMsg) + expect(consoleSpy).toHaveBeenCalledWith( + 'Socket message', + JSON.parse(testMsg), + ) + expect(mockFn).toHaveBeenCalledWith(JSON.parse(testMsg)) + }, 8000) + + test('sends token to authenticate', async () => { + const msg = { name: 'UPDATE_TOKEN', payload: 'valid_token' } + const msgStr = JSON.stringify(msg) + const { result } = renderHook(() => SocketContainer.useContainer(), { + wrapper: SocketProvider, + }) + await server.connected + result.current.updateSocketToken(msg.payload) + await expect(server).toReceiveMessage(msgStr) + expect(server).toHaveReceivedMessages([msgStr]) + }) + }) +}) diff --git a/src/containers/SocketContainer.tsx b/src/containers/SocketContainer.tsx new file mode 100644 index 00000000..77d4fc51 --- /dev/null +++ b/src/containers/SocketContainer.tsx @@ -0,0 +1,64 @@ +import { DebugContainer } from 'containers/DebugContainer' +import { useEffect, useMemo, useRef } from 'react' +import { createContainer } from 'unstated-next' + +type WsCallback = (arg0: MessageEvent['data']) => void +interface CallbackList { + [key: string]: WsCallback +} + +const useSocket = () => { + const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://' + const eventUrl = + protocol + window.location.hostname + ':2337/api/v1/socket/events' + const ws = useMemo(() => new WebSocket(eventUrl), [eventUrl]) + + const cbList = useRef({}) + const { DEBUG_SOCKET } = DebugContainer.useContainer() + + useEffect(() => { + /** + * Emit event to each callback in list upon getting WS message + * @param message WS event + */ + ws.onmessage = (message) => { + const event = JSON.parse(message.data) + if (DEBUG_SOCKET) console.log('Socket message', event) + Object.values(cbList.current).forEach((callback: WsCallback) => { + callback(event) + }) + } + }, [DEBUG_SOCKET, ws]) + + /** + * Adds function to list of callbacks, WILL OVERWRITE EXISTING FUNCTION + * if key already exists in list + * @param key string Name of cb + * @param cb function Callback function + */ + const addCallback = (key: string, cb: WsCallback) => { + if (DEBUG_SOCKET) console.log(`Adding ${key} socket listener`) + cbList.current[key] = cb + } + + const removeCallback = (key: string) => { + if (cbList.current[key]) { + if (DEBUG_SOCKET) console.log(`Removing ${key} socket listener`) + delete cbList.current[key] + } + } + + const updateSocketToken = (token: string) => { + if (token && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ name: 'UPDATE_TOKEN', payload: token })) + } + } + + return { + addCallback, + removeCallback, + updateSocketToken, + } +} + +export const SocketContainer = createContainer(useSocket) diff --git a/src/index.tsx b/src/index.tsx index 59f90ce7..c06fe72f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,6 +5,7 @@ import { ThemeProvider } from 'components/UI/Theme/ThemeProvider' import { AuthContainer } from 'containers/AuthContainer' import { ServerConfigContainer } from 'containers/ConfigContainer' import { DebugContainer } from 'containers/DebugContainer' +import { SocketContainer } from 'containers/SocketContainer' import ReactDOM from 'react-dom' import { HashRouter } from 'react-router-dom' @@ -17,9 +18,11 @@ ReactDOM.render( - - - + + + + + diff --git a/src/test/testMocks.tsx b/src/test/testMocks.tsx index 4dcc50b7..8268b272 100644 --- a/src/test/testMocks.tsx +++ b/src/test/testMocks.tsx @@ -2,6 +2,7 @@ import ErrorBoundary from 'components/ErrorBoundary' import { AuthContainer } from 'containers/AuthContainer' import { ServerConfigContainer } from 'containers/ConfigContainer' import { DebugContainer } from 'containers/DebugContainer' +import { SocketContainer } from 'containers/SocketContainer' import { Suspense } from 'react' import { HashRouter } from 'react-router-dom' @@ -25,7 +26,9 @@ export const AllProviders = ({ children }: ProviderMocks) => { - {children} + + {children} + @@ -43,9 +46,11 @@ export const SuspendedProviders = ({ children }: ProviderMocks) => { - - LOADING...}>{children} - + + + LOADING...}>{children} + + @@ -64,9 +69,11 @@ export const LoggedInProviders = ({ children }: ProviderMocks) => { - - {children} - + + + {children} + + @@ -74,7 +81,7 @@ export const LoggedInProviders = ({ children }: ProviderMocks) => { ) } -const SubProvider = ({ children }: ProviderMocks) => { +const LoginProvider = ({ children }: ProviderMocks) => { const { login } = AuthContainer.useContainer() login('admin', 'password') .then(() => { @@ -86,3 +93,17 @@ const SubProvider = ({ children }: ProviderMocks) => { }) return <>{children} } + +/** + * Wrapper that only has socket provider and what it needs to run + * not authenticated, with logs on + * @param param0 + * @returns + */ +export const SocketProvider = ({ children }: ProviderMocks) => { + return ( + + {children} + + ) +} diff --git a/src/types/config-types.ts b/src/types/config-types.ts index e5f57662..0667dac1 100644 --- a/src/types/config-types.ts +++ b/src/types/config-types.ts @@ -15,3 +15,10 @@ export interface VersionConfig { current_api_version: string supported_api_versions: [string] } + +export interface DebugSettings { + LOGIN?: boolean + AUTH?: boolean + LOCAL_STORAGE?: boolean + SOCKET?: boolean +}