From c924a8cf6d632b0b041db8e95f8c8b6bad113d52 Mon Sep 17 00:00:00 2001 From: Brian Posey <15091170+btposey@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:09:03 -0500 Subject: [PATCH 1/5] WIP - Add broadcast logout functionality Jira ticket: CAMS-476 Co-authored-by: James Brooks <12275865+jamesobrooks@users.noreply.github.com> Co-authored-by: Brian Posey <15091170+btposey@users.noreply.github.com>, --- user-interface/src/login/Login.tsx | 2 ++ user-interface/src/login/SessionEnd.tsx | 2 ++ .../src/login/broadcast-logout.test.ts | 4 ++++ user-interface/src/login/broadcast-logout.ts | 19 +++++++++++++++++++ 4 files changed, 27 insertions(+) create mode 100644 user-interface/src/login/broadcast-logout.test.ts create mode 100644 user-interface/src/login/broadcast-logout.ts diff --git a/user-interface/src/login/Login.tsx b/user-interface/src/login/Login.tsx index a7e9857a7..0b41b5262 100644 --- a/user-interface/src/login/Login.tsx +++ b/user-interface/src/login/Login.tsx @@ -21,6 +21,7 @@ import ApiConfiguration from '@/configuration/apiConfiguration'; import { CamsUser } from '@common/cams/users'; import { CamsSession } from '@common/cams/session'; import { SUPERUSER } from '@common/cams/test-utilities/mock-user'; +import { initializeBroadcastLogout } from '@/login/broadcast-logout'; export type LoginProps = PropsWithChildren & { provider?: LoginProvider; @@ -42,6 +43,7 @@ export function Login(props: LoginProps): React.ReactNode { addApiAfterHook(http401Hook); initializeInactiveLogout(); + initializeBroadcastLogout(); const session: CamsSession | null = LocalStorage.getSession(); if (session) { diff --git a/user-interface/src/login/SessionEnd.tsx b/user-interface/src/login/SessionEnd.tsx index 7a3bb376a..150f9756a 100644 --- a/user-interface/src/login/SessionEnd.tsx +++ b/user-interface/src/login/SessionEnd.tsx @@ -5,6 +5,7 @@ import Button from '@/lib/components/uswds/Button'; import { LocalStorage } from '@/lib/utils/local-storage'; import { LOGIN_PATH, LOGOUT_SESSION_END_PATH } from './login-library'; import { BlankPage } from './BlankPage'; +import { broadcastLogout } from '@/login/broadcast-logout'; export function SessionEnd() { const location = useLocation(); @@ -16,6 +17,7 @@ export function SessionEnd() { LocalStorage.removeSession(); LocalStorage.removeAck(); + broadcastLogout(); useEffect(() => { if (location.pathname !== LOGOUT_SESSION_END_PATH) { diff --git a/user-interface/src/login/broadcast-logout.test.ts b/user-interface/src/login/broadcast-logout.test.ts new file mode 100644 index 000000000..88aa9d047 --- /dev/null +++ b/user-interface/src/login/broadcast-logout.test.ts @@ -0,0 +1,4 @@ +// TODO: Need to write a humble object that proxies the BroadcastChanner API so we can test. +describe('Broadcast Logout', () => { + test('should call postMessage', () => {}); +}); diff --git a/user-interface/src/login/broadcast-logout.ts b/user-interface/src/login/broadcast-logout.ts new file mode 100644 index 000000000..8004c100b --- /dev/null +++ b/user-interface/src/login/broadcast-logout.ts @@ -0,0 +1,19 @@ +import { LOGOUT_PATH } from '@/login/login-library'; + +let channel: BroadcastChannel; + +function handleLogout() { + const { host, protocol } = window.location; + const logoutUri = protocol + '//' + host + LOGOUT_PATH; + window.location.assign(logoutUri); + channel?.close(); +} + +export function initializeBroadcastLogout() { + channel = new BroadcastChannel('CAMS_logout'); + channel.onmessage = handleLogout; +} + +export function broadcastLogout() { + channel?.postMessage(''); +} From 620d75398b4da2df6adecc7cdf1e3d36b4af7c51 Mon Sep 17 00:00:00 2001 From: Arthur Morrow Date: Mon, 11 Nov 2024 12:29:05 -0500 Subject: [PATCH 2/5] finished broadcast-logout tests Jira ticket: CAMS-442 Co-authored-by: Arthur Morrow <133667008+amorrow-flexion@users.noreply.github.com> Co-authored-by: James Brooks <12275865+jamesobrooks@users.noreply.github.com> --- .../lib/humble/broadcast-channel-humble.ts | 19 ++++++ .../src/login/broadcast-logout.test.ts | 63 ++++++++++++++++++- user-interface/src/login/broadcast-logout.ts | 11 ++-- user-interface/vitest.config.mts | 1 + 4 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 user-interface/src/lib/humble/broadcast-channel-humble.ts diff --git a/user-interface/src/lib/humble/broadcast-channel-humble.ts b/user-interface/src/lib/humble/broadcast-channel-humble.ts new file mode 100644 index 000000000..eb803acdd --- /dev/null +++ b/user-interface/src/lib/humble/broadcast-channel-humble.ts @@ -0,0 +1,19 @@ +export class BroadcastChannelHumble { + private channel: BroadcastChannel; + + constructor(channelName: string) { + this.channel = new BroadcastChannel(channelName); + } + + onMessage(handler: (event: MessageEvent) => void) { + this.channel.onmessage = handler; + } + + postMessage(message: unknown) { + this.channel.postMessage(message); + } + + close() { + this.channel.close(); + } +} diff --git a/user-interface/src/login/broadcast-logout.test.ts b/user-interface/src/login/broadcast-logout.test.ts index 88aa9d047..bc9c5137c 100644 --- a/user-interface/src/login/broadcast-logout.test.ts +++ b/user-interface/src/login/broadcast-logout.test.ts @@ -1,4 +1,63 @@ -// TODO: Need to write a humble object that proxies the BroadcastChanner API so we can test. +import { BroadcastChannelHumble } from '@/lib/humble/broadcast-channel-humble'; +import { broadcastLogout, initializeBroadcastLogout } from '@/login/broadcast-logout'; + describe('Broadcast Logout', () => { - test('should call postMessage', () => {}); + test('should handle broadcast-logout properly', () => { + const postMessageSpy = vi + .spyOn(BroadcastChannelHumble.prototype, 'postMessage') + .mockReturnValue(); + // eslint-disable-next-line @typescript-eslint/ban-types + let onMessageFn: Function = vi.fn(); + const onMessageSpy = vi + .spyOn(BroadcastChannelHumble.prototype, 'onMessage') + .mockImplementation((arg) => { + onMessageFn = arg; + }); + + const closeSpy = vi.spyOn(BroadcastChannelHumble.prototype, 'close').mockReturnValue(); + + global.window = Object.create(window); + global.window.location = { + ancestorOrigins: { + length: 1, + contains: vi.fn(), + item: vi.fn(() => ''), + [Symbol.iterator]: function (): ArrayIterator { + throw new Error('Function not implemented.'); + }, + }, + hash: '', + host: 'some-host', + hostname: '', + href: '', + origin: 'http://dummy.com', + pathname: '', + port: '', + protocol: 'http:', + search: '', + assign: function (url: string | URL): void { + console.log('URL::::::: ' + url); + return; + }, + reload: function (): void { + throw new Error('Function not implemented.'); + }, + replace: function (_url: string | URL): void { + throw new Error('Function not implemented.'); + }, + }; + vi.spyOn(global.window.location, 'assign'); + const expectedUrl = + global.window.location.protocol + '//' + global.window.location.host + '/logout'; + + initializeBroadcastLogout(); + broadcastLogout(); + expect(postMessageSpy).toHaveBeenCalledWith('I am testing this'); + expect(onMessageSpy).toHaveBeenCalledWith(expect.any(Function)); + expect(onMessageFn).toEqual(expect.any(Function)); + onMessageFn(); + expect(closeSpy).toHaveBeenCalled(); + expect(global.window.location.assign).toHaveBeenCalledWith(expectedUrl); + //TODO: find a way to actually have the postMessage event be handled + }); }); diff --git a/user-interface/src/login/broadcast-logout.ts b/user-interface/src/login/broadcast-logout.ts index 8004c100b..430df1d33 100644 --- a/user-interface/src/login/broadcast-logout.ts +++ b/user-interface/src/login/broadcast-logout.ts @@ -1,8 +1,9 @@ +import { BroadcastChannelHumble } from '@/lib/humble/broadcast-channel-humble'; import { LOGOUT_PATH } from '@/login/login-library'; -let channel: BroadcastChannel; +let channel: BroadcastChannelHumble; -function handleLogout() { +export function handleLogout() { const { host, protocol } = window.location; const logoutUri = protocol + '//' + host + LOGOUT_PATH; window.location.assign(logoutUri); @@ -10,10 +11,10 @@ function handleLogout() { } export function initializeBroadcastLogout() { - channel = new BroadcastChannel('CAMS_logout'); - channel.onmessage = handleLogout; + channel = new BroadcastChannelHumble('CAMS_logout'); + channel.onMessage(handleLogout); } export function broadcastLogout() { - channel?.postMessage(''); + channel?.postMessage('I am testing this'); } diff --git a/user-interface/vitest.config.mts b/user-interface/vitest.config.mts index d43a5f88b..e3200b264 100644 --- a/user-interface/vitest.config.mts +++ b/user-interface/vitest.config.mts @@ -27,6 +27,7 @@ export default defineConfig({ '**/data-verification/consolidation/ConsolidationOrderAccordionView.tsx', '**/data-verification/consolidation/*Mock.ts', '**/*.d.ts', + '**/**humble.ts', ...coverageConfigDefaults.exclude, ], thresholds: { From b9b8fc42c4da101fd65eff12ffe006f98e9434cf Mon Sep 17 00:00:00 2001 From: James Brooks <12275865+jamesobrooks@users.noreply.github.com> Date: Mon, 11 Nov 2024 12:38:29 -0600 Subject: [PATCH 3/5] Simplify test Jira ticket: CAMS-476 Co-authored-by: Arthur Morrow <133667008+amorrow-flexion@users.noreply.github.com> --- .../src/login/broadcast-logout.test.ts | 49 ++++++------------- user-interface/src/login/broadcast-logout.ts | 2 +- 2 files changed, 16 insertions(+), 35 deletions(-) diff --git a/user-interface/src/login/broadcast-logout.test.ts b/user-interface/src/login/broadcast-logout.test.ts index bc9c5137c..e592d6954 100644 --- a/user-interface/src/login/broadcast-logout.test.ts +++ b/user-interface/src/login/broadcast-logout.test.ts @@ -1,5 +1,5 @@ import { BroadcastChannelHumble } from '@/lib/humble/broadcast-channel-humble'; -import { broadcastLogout, initializeBroadcastLogout } from '@/login/broadcast-logout'; +import { broadcastLogout, handleLogout, initializeBroadcastLogout } from '@/login/broadcast-logout'; describe('Broadcast Logout', () => { test('should handle broadcast-logout properly', () => { @@ -16,48 +16,29 @@ describe('Broadcast Logout', () => { const closeSpy = vi.spyOn(BroadcastChannelHumble.prototype, 'close').mockReturnValue(); - global.window = Object.create(window); + Object.defineProperty(global, 'window', Object.create(window)); global.window.location = { - ancestorOrigins: { - length: 1, - contains: vi.fn(), - item: vi.fn(() => ''), - [Symbol.iterator]: function (): ArrayIterator { - throw new Error('Function not implemented.'); - }, - }, - hash: '', host: 'some-host', - hostname: '', - href: '', - origin: 'http://dummy.com', - pathname: '', - port: '', protocol: 'http:', - search: '', - assign: function (url: string | URL): void { - console.log('URL::::::: ' + url); - return; - }, - reload: function (): void { - throw new Error('Function not implemented.'); - }, - replace: function (_url: string | URL): void { - throw new Error('Function not implemented.'); - }, - }; - vi.spyOn(global.window.location, 'assign'); + assign: function (_url: string | URL): void {}, + } as unknown as Location; + const assignSpy = vi.spyOn(global.window.location, 'assign'); const expectedUrl = global.window.location.protocol + '//' + global.window.location.host + '/logout'; initializeBroadcastLogout(); broadcastLogout(); - expect(postMessageSpy).toHaveBeenCalledWith('I am testing this'); - expect(onMessageSpy).toHaveBeenCalledWith(expect.any(Function)); - expect(onMessageFn).toEqual(expect.any(Function)); + + // Validate broadcastLogout function + expect(postMessageSpy).toHaveBeenCalledWith('Logout all windows'); + + // Validate initializeBroadcastLogout function + expect(onMessageSpy).toHaveBeenCalledWith(handleLogout); + expect(onMessageFn).toEqual(handleLogout); + + // Validate handleLogout function onMessageFn(); expect(closeSpy).toHaveBeenCalled(); - expect(global.window.location.assign).toHaveBeenCalledWith(expectedUrl); - //TODO: find a way to actually have the postMessage event be handled + expect(assignSpy).toHaveBeenCalledWith(expectedUrl); }); }); diff --git a/user-interface/src/login/broadcast-logout.ts b/user-interface/src/login/broadcast-logout.ts index 430df1d33..2db5628ee 100644 --- a/user-interface/src/login/broadcast-logout.ts +++ b/user-interface/src/login/broadcast-logout.ts @@ -16,5 +16,5 @@ export function initializeBroadcastLogout() { } export function broadcastLogout() { - channel?.postMessage('I am testing this'); + channel?.postMessage('Logout all windows'); } From 9a9700a72bb5a2d8b7d924263b74519a1bef0da7 Mon Sep 17 00:00:00 2001 From: Brian Posey <15091170+btposey@users.noreply.github.com> Date: Mon, 11 Nov 2024 13:50:09 -0500 Subject: [PATCH 4/5] Rename handleLogout => handleLogoutBroadcast Jira ticket: CAMS-476 Co-authored-by: Arthur Morrow <133667008+amorrow-flexion@users.noreply.github.com> Co-authored-by: Brian Posey <15091170+btposey@users.noreply.github.com>, --- user-interface/src/login/broadcast-logout.test.ts | 10 +++++++--- user-interface/src/login/broadcast-logout.ts | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/user-interface/src/login/broadcast-logout.test.ts b/user-interface/src/login/broadcast-logout.test.ts index e592d6954..b2c6910f2 100644 --- a/user-interface/src/login/broadcast-logout.test.ts +++ b/user-interface/src/login/broadcast-logout.test.ts @@ -1,5 +1,9 @@ import { BroadcastChannelHumble } from '@/lib/humble/broadcast-channel-humble'; -import { broadcastLogout, handleLogout, initializeBroadcastLogout } from '@/login/broadcast-logout'; +import { + broadcastLogout, + handleLogoutBroadcast, + initializeBroadcastLogout, +} from '@/login/broadcast-logout'; describe('Broadcast Logout', () => { test('should handle broadcast-logout properly', () => { @@ -33,8 +37,8 @@ describe('Broadcast Logout', () => { expect(postMessageSpy).toHaveBeenCalledWith('Logout all windows'); // Validate initializeBroadcastLogout function - expect(onMessageSpy).toHaveBeenCalledWith(handleLogout); - expect(onMessageFn).toEqual(handleLogout); + expect(onMessageSpy).toHaveBeenCalledWith(handleLogoutBroadcast); + expect(onMessageFn).toEqual(handleLogoutBroadcast); // Validate handleLogout function onMessageFn(); diff --git a/user-interface/src/login/broadcast-logout.ts b/user-interface/src/login/broadcast-logout.ts index 2db5628ee..2c836acec 100644 --- a/user-interface/src/login/broadcast-logout.ts +++ b/user-interface/src/login/broadcast-logout.ts @@ -3,7 +3,7 @@ import { LOGOUT_PATH } from '@/login/login-library'; let channel: BroadcastChannelHumble; -export function handleLogout() { +export function handleLogoutBroadcast() { const { host, protocol } = window.location; const logoutUri = protocol + '//' + host + LOGOUT_PATH; window.location.assign(logoutUri); @@ -12,7 +12,7 @@ export function handleLogout() { export function initializeBroadcastLogout() { channel = new BroadcastChannelHumble('CAMS_logout'); - channel.onMessage(handleLogout); + channel.onMessage(handleLogoutBroadcast); } export function broadcastLogout() { From 8b07678ef24dc9cc6cba4bf8897135644df54c1a Mon Sep 17 00:00:00 2001 From: Arthur Morrow <133667008+amorrow-flexion@users.noreply.github.com> Date: Mon, 11 Nov 2024 14:15:01 -0500 Subject: [PATCH 5/5] updated eslint-dsable CAMS-476 --- user-interface/src/login/broadcast-logout.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user-interface/src/login/broadcast-logout.test.ts b/user-interface/src/login/broadcast-logout.test.ts index b2c6910f2..4282e50e8 100644 --- a/user-interface/src/login/broadcast-logout.test.ts +++ b/user-interface/src/login/broadcast-logout.test.ts @@ -10,7 +10,7 @@ describe('Broadcast Logout', () => { const postMessageSpy = vi .spyOn(BroadcastChannelHumble.prototype, 'postMessage') .mockReturnValue(); - // eslint-disable-next-line @typescript-eslint/ban-types + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type let onMessageFn: Function = vi.fn(); const onMessageSpy = vi .spyOn(BroadcastChannelHumble.prototype, 'onMessage')