From 563ce365fb288c56bc5468681f1e6158b4adfb79 Mon Sep 17 00:00:00 2001 From: Zach Date: Thu, 5 Oct 2023 02:23:48 -0500 Subject: [PATCH 01/10] added test function and updated Session event listener for login or logout funcs --- e2e/node/e2e-test.spec.ts | 32 ++++++++++++++++++++++++++++++++ packages/node/src/Session.ts | 1 + 2 files changed, 33 insertions(+) diff --git a/e2e/node/e2e-test.spec.ts b/e2e/node/e2e-test.spec.ts index 9bf648b796..aca4a65bce 100644 --- a/e2e/node/e2e-test.spec.ts +++ b/e2e/node/e2e-test.spec.ts @@ -257,6 +257,12 @@ describe("Session events", () => { expect(expiredFunc).toHaveBeenCalledTimes(0); }); + it("is currently a work in progress", async () => { + session.events.on(EVENTS.LOGIN_OR_LOGOUT, () => { + + }) + }) + it("sends an event on session expiration", async () => { if (typeof session.info.expirationDate !== "number") { throw new Error("Cannot determine session expiration date"); @@ -275,3 +281,29 @@ describe("Session events", () => { expect(expiredFunc).toHaveBeenCalledTimes(1); }); }); + +describe("New combined login and logout session event", () => { + jest.setTimeout(15 * 60 * 1000); + + let session: Session; + let loginAndLogoutFunc: () => void; + let expiredFunc: () => void; + + beforeEach(async () => { + session = new Session(); + loginAndLogoutFunc = jest.fn(); + expiredFunc = jest.fn(); + session.events.on(EVENTS.LOGIN_OR_LOGOUT, loginAndLogoutFunc); + session.events.on(EVENTS.SESSION_EXPIRED, expiredFunc); + + await session.login(getCredentials()); + }); + + it("tests to make sure the function is called during both login and logout", async () => { + expect(session.info.isLoggedIn).toBe(true); + await session.logout(); + + expect(loginAndLogoutFunc).toHaveBeenCalledTimes(2); + expect(expiredFunc).toHaveBeenCalledTimes(0); + }) +}) \ No newline at end of file diff --git a/packages/node/src/Session.ts b/packages/node/src/Session.ts index 878345f6ab..0ef7c798fe 100644 --- a/packages/node/src/Session.ts +++ b/packages/node/src/Session.ts @@ -190,6 +190,7 @@ export class Session extends EventEmitter implements IHasSessionEventListener { this.events.on(EVENTS.SESSION_EXPIRED, () => this.internalLogout(false)); } + /** * Triggers the login process. Note that this method will redirect the user away from your app. * From e7c9684a241c6ef478aeae96492a7e28990647cf Mon Sep 17 00:00:00 2001 From: Zach Date: Thu, 5 Oct 2023 02:42:53 -0500 Subject: [PATCH 02/10] i accidentally put all the changes in the node/dist directory so I had to undo that embarrassment --- packages/core/src/SessionEventListener.ts | 50 ++++++++++++++++++++++- packages/core/src/constant.ts | 1 + 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/core/src/SessionEventListener.ts b/packages/core/src/SessionEventListener.ts index 3f73715abd..e2c4b8b636 100644 --- a/packages/core/src/SessionEventListener.ts +++ b/packages/core/src/SessionEventListener.ts @@ -57,7 +57,10 @@ type FALLBACK_ARGS = { // Prevents from using a SessionEventEmitter as an aritrary EventEmitter. listener: never; }; - +type LOGIN_AND_LOGOUT_ARGS = { + eventName: typeof EVENTS.LOGIN | typeof EVENTS.LOGOUT; + listener: () => void; +} export interface ISessionEventListener extends EventEmitter { /** * Register a listener called on successful login. @@ -77,6 +80,15 @@ export interface ISessionEventListener extends EventEmitter { eventName: LOGOUT_ARGS["eventName"], listener: LOGOUT_ARGS["listener"], ): this; + /** + * Register a listener called on a successful login or logout. + * @param eventName The login and logout event name. + * @param listener The callback called on a successful login and logout. + */ + on( + eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], + listener: LOGIN_AND_LOGOUT_ARGS["listener"] + ): this; /** * Register a listener called on session expiration. * @param eventName The session expiration event name. @@ -159,6 +171,15 @@ export interface ISessionEventListener extends EventEmitter { eventName: LOGOUT_ARGS["eventName"], listener: LOGOUT_ARGS["listener"], ): this; + /** + * Register a listener called on a successful login or logout. + * @param eventName The login and logout event name. + * @param listener The callback called on a successful login or logout. + */ + addListener( + eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], + listener: LOGIN_AND_LOGOUT_ARGS["listener"] + ): this; /** * Register a listener called on session expiration. * @param eventName The session expiration event name. @@ -241,6 +262,15 @@ export interface ISessionEventListener extends EventEmitter { eventName: LOGOUT_ARGS["eventName"], listener: LOGOUT_ARGS["listener"], ): this; + /** + * Register a listener called on the next successful login or logout. + * @param eventName The login and logout event name. + * @param listener The callback called on the next successful login or logout. + */ + once( + eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], + listener: LOGIN_AND_LOGOUT_ARGS["listener"] + ): this; /** * Register a listener called on the next session expiration. * @param eventName The session expiration event name. @@ -324,6 +354,15 @@ export interface ISessionEventListener extends EventEmitter { eventName: LOGOUT_ARGS["eventName"], listener: LOGOUT_ARGS["listener"], ): this; + /** + * Unregister a listener called on a successful login or logout. + * @param eventName The login and logout event name. + * @param listener The callback to unregister. + */ + off( + eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], + listener: LOGIN_AND_LOGOUT_ARGS["listener"] + ): this; /** * Unegister a listener called on session expiration. * @param eventName The session expiration event name. @@ -405,6 +444,15 @@ export interface ISessionEventListener extends EventEmitter { eventName: LOGOUT_ARGS["eventName"], listener: LOGOUT_ARGS["listener"], ): this; + /** + * Unregister a listener called on a successful login or logout. + * @param eventName The login and logout event name + * @param listener The callback to unregister + */ + removeListener( + eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], + listener: LOGIN_AND_LOGOUT_ARGS["listener"] + ): this; /** * Unegister a listener called on session expiration. * @param eventName The session expiration event name. diff --git a/packages/core/src/constant.ts b/packages/core/src/constant.ts index 98f34f3714..7582be6c44 100644 --- a/packages/core/src/constant.ts +++ b/packages/core/src/constant.ts @@ -35,6 +35,7 @@ export const EVENTS = { // Note that an `error` events MUST be listened to: https://nodejs.org/dist/latest-v16.x/docs/api/events.html#error-events. ERROR: "error", LOGIN: "login", + LOGIN_AND_LOGOUT: "login" || "logout", LOGOUT: "logout", NEW_REFRESH_TOKEN: "newRefreshToken", SESSION_EXPIRED: "sessionExpired", From 10b10aa9e5d7ffd90e655983932c68f72eee26da Mon Sep 17 00:00:00 2001 From: Zach Date: Thu, 5 Oct 2023 02:57:38 -0500 Subject: [PATCH 03/10] updated ugly event emitters, definitely not the best way to do this --- e2e/node/e2e-test.spec.ts | 8 +------- packages/core/src/SessionEventListener.ts | 2 +- packages/core/src/constant.ts | 2 +- packages/node/src/Session.ts | 3 +++ 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/e2e/node/e2e-test.spec.ts b/e2e/node/e2e-test.spec.ts index aca4a65bce..c7aa8d03c1 100644 --- a/e2e/node/e2e-test.spec.ts +++ b/e2e/node/e2e-test.spec.ts @@ -257,12 +257,6 @@ describe("Session events", () => { expect(expiredFunc).toHaveBeenCalledTimes(0); }); - it("is currently a work in progress", async () => { - session.events.on(EVENTS.LOGIN_OR_LOGOUT, () => { - - }) - }) - it("sends an event on session expiration", async () => { if (typeof session.info.expirationDate !== "number") { throw new Error("Cannot determine session expiration date"); @@ -293,7 +287,7 @@ describe("New combined login and logout session event", () => { session = new Session(); loginAndLogoutFunc = jest.fn(); expiredFunc = jest.fn(); - session.events.on(EVENTS.LOGIN_OR_LOGOUT, loginAndLogoutFunc); + session.events.on(EVENTS.LOGIN_AND_LOGOUT, loginAndLogoutFunc); session.events.on(EVENTS.SESSION_EXPIRED, expiredFunc); await session.login(getCredentials()); diff --git a/packages/core/src/SessionEventListener.ts b/packages/core/src/SessionEventListener.ts index e2c4b8b636..86c2c17dd0 100644 --- a/packages/core/src/SessionEventListener.ts +++ b/packages/core/src/SessionEventListener.ts @@ -58,7 +58,7 @@ type FALLBACK_ARGS = { listener: never; }; type LOGIN_AND_LOGOUT_ARGS = { - eventName: typeof EVENTS.LOGIN | typeof EVENTS.LOGOUT; + eventName: typeof EVENTS.LOGIN_AND_LOGOUT; listener: () => void; } export interface ISessionEventListener extends EventEmitter { diff --git a/packages/core/src/constant.ts b/packages/core/src/constant.ts index 7582be6c44..368a92ec52 100644 --- a/packages/core/src/constant.ts +++ b/packages/core/src/constant.ts @@ -35,7 +35,7 @@ export const EVENTS = { // Note that an `error` events MUST be listened to: https://nodejs.org/dist/latest-v16.x/docs/api/events.html#error-events. ERROR: "error", LOGIN: "login", - LOGIN_AND_LOGOUT: "login" || "logout", + LOGIN_AND_LOGOUT: "loginAndLogout", LOGOUT: "logout", NEW_REFRESH_TOKEN: "newRefreshToken", SESSION_EXPIRED: "sessionExpired", diff --git a/packages/node/src/Session.ts b/packages/node/src/Session.ts index 0ef7c798fe..f29b1c4ca8 100644 --- a/packages/node/src/Session.ts +++ b/packages/node/src/Session.ts @@ -216,6 +216,7 @@ export class Session extends EventEmitter implements IHasSessionEventListener { if (loginInfo?.isLoggedIn) { // Send a signal on successful client credentials login. (this.events as EventEmitter).emit(EVENTS.LOGIN); + (this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT); } }; @@ -285,6 +286,8 @@ export class Session extends EventEmitter implements IHasSessionEventListener { this.info.isLoggedIn = false; if (emitEvent) { (this.events as EventEmitter).emit(EVENTS.LOGOUT); + // I can't think of a better way to do this without reworking the events library + (this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT); } }; From e7024fe8f02f09d32ad04f558f5af50f24e6997f Mon Sep 17 00:00:00 2001 From: Zach Date: Thu, 5 Oct 2023 02:58:04 -0500 Subject: [PATCH 04/10] updated comment in internalLogout function of Session with explanation --- packages/node/src/Session.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/node/src/Session.ts b/packages/node/src/Session.ts index f29b1c4ca8..a7e159df1c 100644 --- a/packages/node/src/Session.ts +++ b/packages/node/src/Session.ts @@ -287,6 +287,9 @@ export class Session extends EventEmitter implements IHasSessionEventListener { if (emitEvent) { (this.events as EventEmitter).emit(EVENTS.LOGOUT); // I can't think of a better way to do this without reworking the events library + // I'm not sure how to emit partial events in Typescript the proper way + // eg -> login raises 0x10 and logout raises 0x01, LOGIN_AND_LOGOUT listens for both, while + // LOGIN listens for 0x10 and LOGOUT listens for 0x01 (this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT); } }; From a8c02a674ecae16a07510cc8c98905c4415f8a3e Mon Sep 17 00:00:00 2001 From: Zach Date: Wed, 21 Feb 2024 12:20:35 -0600 Subject: [PATCH 05/10] added initial synchonized calls and test --- packages/browser/src/Session.spec.ts | 29 +++++++++++++++++++++++ packages/core/src/SessionEventListener.ts | 14 +++++------ packages/core/src/constant.ts | 1 - packages/node/src/Session.spec.ts | 28 ++++++++++++++++++++++ 4 files changed, 64 insertions(+), 8 deletions(-) diff --git a/packages/browser/src/Session.spec.ts b/packages/browser/src/Session.spec.ts index af03cb3452..9602a246f1 100644 --- a/packages/browser/src/Session.spec.ts +++ b/packages/browser/src/Session.spec.ts @@ -982,6 +982,35 @@ describe("Session", () => { }); }); + describe("login and logout", () => { + it("calls the registered callback on login and logout", async () => { + const myCallback = jest.fn(); + const clientAuthentication = mockClientAuthentication(); + const mySession = new Session({ + clientAuthentication, + }); + + clientAuthentication.handleIncomingRedirect = jest + .fn() + .mockResolvedValue({ + isLoggedIn: true, + sessionId: "a session ID", + webId: "https://some.webid#them", + }); + mockLocalStorage({}); + + mySession.events.on(EVENTS.LOGIN || EVENTS.LOGOUT, myCallback); + await mySession.handleIncomingRedirect("https://some.url"); + expect(myCallback).toHaveBeenCalled(); + + mySession.events.on(EVENTS.LOGIN || EVENTS.LOGOUT, myCallback); + await mySession.logout(); + expect(myCallback).toHaveBeenCalled(); + + // expect(myCallback).toHaveBeenCalledTimes(2); + }) + }) + describe("sessionRestore", () => { it("calls the registered callback on session restore", async () => { // Set our window's location to our test value. diff --git a/packages/core/src/SessionEventListener.ts b/packages/core/src/SessionEventListener.ts index 86c2c17dd0..960c9d747f 100644 --- a/packages/core/src/SessionEventListener.ts +++ b/packages/core/src/SessionEventListener.ts @@ -58,7 +58,7 @@ type FALLBACK_ARGS = { listener: never; }; type LOGIN_AND_LOGOUT_ARGS = { - eventName: typeof EVENTS.LOGIN_AND_LOGOUT; + eventName: typeof EVENTS.LOGIN | typeof EVENTS.LOGOUT; listener: () => void; } export interface ISessionEventListener extends EventEmitter { @@ -86,8 +86,8 @@ export interface ISessionEventListener extends EventEmitter { * @param listener The callback called on a successful login and logout. */ on( - eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], - listener: LOGIN_AND_LOGOUT_ARGS["listener"] + eventName: LOGIN_ARGS["eventName"] | LOGOUT_ARGS["eventName"], + listener: LOGIN_ARGS["listener"] & LOGOUT_ARGS["listener"] ): this; /** * Register a listener called on session expiration. @@ -177,7 +177,7 @@ export interface ISessionEventListener extends EventEmitter { * @param listener The callback called on a successful login or logout. */ addListener( - eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], + eventName: LOGIN_ARGS["eventName"] | LOGOUT_ARGS["eventName"], listener: LOGIN_AND_LOGOUT_ARGS["listener"] ): this; /** @@ -268,7 +268,7 @@ export interface ISessionEventListener extends EventEmitter { * @param listener The callback called on the next successful login or logout. */ once( - eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], + eventName: LOGIN_ARGS["eventName"] | LOGOUT_ARGS["eventName"], listener: LOGIN_AND_LOGOUT_ARGS["listener"] ): this; /** @@ -360,7 +360,7 @@ export interface ISessionEventListener extends EventEmitter { * @param listener The callback to unregister. */ off( - eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], + eventName: LOGIN_ARGS["eventName"] | LOGOUT_ARGS["eventName"], listener: LOGIN_AND_LOGOUT_ARGS["listener"] ): this; /** @@ -450,7 +450,7 @@ export interface ISessionEventListener extends EventEmitter { * @param listener The callback to unregister */ removeListener( - eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], + eventName: LOGIN_ARGS["eventName"] | LOGOUT_ARGS["eventName"], listener: LOGIN_AND_LOGOUT_ARGS["listener"] ): this; /** diff --git a/packages/core/src/constant.ts b/packages/core/src/constant.ts index 368a92ec52..98f34f3714 100644 --- a/packages/core/src/constant.ts +++ b/packages/core/src/constant.ts @@ -35,7 +35,6 @@ export const EVENTS = { // Note that an `error` events MUST be listened to: https://nodejs.org/dist/latest-v16.x/docs/api/events.html#error-events. ERROR: "error", LOGIN: "login", - LOGIN_AND_LOGOUT: "loginAndLogout", LOGOUT: "logout", NEW_REFRESH_TOKEN: "newRefreshToken", SESSION_EXPIRED: "sessionExpired", diff --git a/packages/node/src/Session.spec.ts b/packages/node/src/Session.spec.ts index 03f618066c..7adc4f4eba 100644 --- a/packages/node/src/Session.spec.ts +++ b/packages/node/src/Session.spec.ts @@ -540,6 +540,34 @@ describe("Session", () => { }); }); + describe("loginAndLogout", () => { + it("calls the registered callback on login and logout", async () => { + const myCallback = jest.fn(); + const clientAuthentication = mockClientAuthentication(); + const mySession = new Session({ + clientAuthentication, + }); + + clientAuthentication.handleIncomingRedirect = jest + .fn() + .mockResolvedValue({ + isLoggedIn: true, + sessionId: "a session ID", + webId: "https://some.webid#them", + }); + + mySession.events.on(EVENTS.LOGIN || EVENTS.LOGOUT, myCallback) + await mySession.handleIncomingRedirect("https://some.url"); + expect(myCallback).toHaveBeenCalled(); + + mySession.events.on(EVENTS.LOGIN || EVENTS.LOGOUT, myCallback); + await mySession.logout(); + expect(myCallback).toHaveBeenCalled(); + + // expect(myCallback).toHaveBeenCalledTimes(2); + }) + }) + describe("sessionExpired", () => { it("calls the provided callback when receiving the appropriate event", async () => { const myCallback = jest.fn(); From c40679ef70703511e50d7b510fe90a7015666cb2 Mon Sep 17 00:00:00 2001 From: Zach Date: Wed, 21 Feb 2024 12:30:33 -0600 Subject: [PATCH 06/10] cleaned up typings --- packages/browser/src/Session.spec.ts | 16 +++++++++++++++- packages/node/src/Session.spec.ts | 19 ++++++++++++++++--- packages/node/src/Session.ts | 8 ++------ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/packages/browser/src/Session.spec.ts b/packages/browser/src/Session.spec.ts index 9602a246f1..6b01f2128e 100644 --- a/packages/browser/src/Session.spec.ts +++ b/packages/browser/src/Session.spec.ts @@ -1007,8 +1007,22 @@ describe("Session", () => { await mySession.logout(); expect(myCallback).toHaveBeenCalled(); - // expect(myCallback).toHaveBeenCalledTimes(2); }) + + it("does not call the registered callback if login isn't successful", async () => { + const myCallback = jest.fn(); + const clientAuthentication = mockClientAuthentication(); + clientAuthentication.handleIncomingRedirect = jest + .fn() + .mockResolvedValue({ + isLoggedIn: true, + sessionId: "a session ID", + webId: "https://some.webid#them", + }); + const mySession = new Session({ clientAuthentication }); + mySession.events.on(EVENTS.LOGIN || EVENTS.LOGOUT, myCallback); + expect(myCallback).not.toHaveBeenCalled(); + }); }) describe("sessionRestore", () => { diff --git a/packages/node/src/Session.spec.ts b/packages/node/src/Session.spec.ts index 7adc4f4eba..82a4f8e83b 100644 --- a/packages/node/src/Session.spec.ts +++ b/packages/node/src/Session.spec.ts @@ -540,7 +540,7 @@ describe("Session", () => { }); }); - describe("loginAndLogout", () => { + describe("login and logout", () => { it("calls the registered callback on login and logout", async () => { const myCallback = jest.fn(); const clientAuthentication = mockClientAuthentication(); @@ -563,9 +563,22 @@ describe("Session", () => { mySession.events.on(EVENTS.LOGIN || EVENTS.LOGOUT, myCallback); await mySession.logout(); expect(myCallback).toHaveBeenCalled(); - - // expect(myCallback).toHaveBeenCalledTimes(2); }) + + it("does not call the registered callback if login isn't successful", async () => { + const myCallback = jest.fn(); + const clientAuthentication = mockClientAuthentication(); + clientAuthentication.handleIncomingRedirect = jest + .fn() + .mockResolvedValue({ + isLoggedIn: true, + sessionId: "a session ID", + webId: "https://some.webid#them", + }); + const mySession = new Session({ clientAuthentication }); + mySession.events.on(EVENTS.LOGIN, myCallback); + expect(myCallback).not.toHaveBeenCalled(); + }); }) describe("sessionExpired", () => { diff --git a/packages/node/src/Session.ts b/packages/node/src/Session.ts index a7e159df1c..ac6a8e4951 100644 --- a/packages/node/src/Session.ts +++ b/packages/node/src/Session.ts @@ -216,7 +216,7 @@ export class Session extends EventEmitter implements IHasSessionEventListener { if (loginInfo?.isLoggedIn) { // Send a signal on successful client credentials login. (this.events as EventEmitter).emit(EVENTS.LOGIN); - (this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT); + // (this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT); } }; @@ -286,11 +286,7 @@ export class Session extends EventEmitter implements IHasSessionEventListener { this.info.isLoggedIn = false; if (emitEvent) { (this.events as EventEmitter).emit(EVENTS.LOGOUT); - // I can't think of a better way to do this without reworking the events library - // I'm not sure how to emit partial events in Typescript the proper way - // eg -> login raises 0x10 and logout raises 0x01, LOGIN_AND_LOGOUT listens for both, while - // LOGIN listens for 0x10 and LOGOUT listens for 0x01 - (this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT); + // (this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT); } }; From a035f48ff7ff8ca047160d5aab5bd34d9601b322 Mon Sep 17 00:00:00 2001 From: Zach Date: Wed, 21 Feb 2024 12:32:29 -0600 Subject: [PATCH 07/10] more tests for LOGIN||LOGOUT --- packages/node/src/Session.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node/src/Session.spec.ts b/packages/node/src/Session.spec.ts index 82a4f8e83b..52c0e2a9b1 100644 --- a/packages/node/src/Session.spec.ts +++ b/packages/node/src/Session.spec.ts @@ -576,7 +576,7 @@ describe("Session", () => { webId: "https://some.webid#them", }); const mySession = new Session({ clientAuthentication }); - mySession.events.on(EVENTS.LOGIN, myCallback); + mySession.events.on(EVENTS.LOGIN || EVENTS.LOGOUT, myCallback); expect(myCallback).not.toHaveBeenCalled(); }); }) From a3c78b4c2e000faff738a3db75bebf89ac3fb666 Mon Sep 17 00:00:00 2001 From: Zach Date: Wed, 21 Feb 2024 22:52:04 -0600 Subject: [PATCH 08/10] reset event handlers to default as or\and types aren't an option, added passing unit tests --- packages/browser/src/Session.spec.ts | 10 +++---- packages/browser/src/Session.ts | 2 ++ packages/core/src/SessionEventListener.ts | 14 ++++----- packages/core/src/constant.ts | 1 + packages/node/src/Session.spec.ts | 36 ++++++++++++++++++----- packages/node/src/Session.ts | 5 ++-- 6 files changed, 45 insertions(+), 23 deletions(-) diff --git a/packages/browser/src/Session.spec.ts b/packages/browser/src/Session.spec.ts index 6b01f2128e..dfc86c7057 100644 --- a/packages/browser/src/Session.spec.ts +++ b/packages/browser/src/Session.spec.ts @@ -999,13 +999,11 @@ describe("Session", () => { }); mockLocalStorage({}); - mySession.events.on(EVENTS.LOGIN || EVENTS.LOGOUT, myCallback); + mySession.events.on(EVENTS.LOGIN_AND_LOGOUT, myCallback); await mySession.handleIncomingRedirect("https://some.url"); - expect(myCallback).toHaveBeenCalled(); - - mySession.events.on(EVENTS.LOGIN || EVENTS.LOGOUT, myCallback); + expect(myCallback).toHaveBeenCalledTimes(1); await mySession.logout(); - expect(myCallback).toHaveBeenCalled(); + expect(myCallback).toHaveBeenCalledTimes(2); }) @@ -1020,7 +1018,7 @@ describe("Session", () => { webId: "https://some.webid#them", }); const mySession = new Session({ clientAuthentication }); - mySession.events.on(EVENTS.LOGIN || EVENTS.LOGOUT, myCallback); + mySession.events.on(EVENTS.LOGIN_AND_LOGOUT, myCallback); expect(myCallback).not.toHaveBeenCalled(); }); }) diff --git a/packages/browser/src/Session.ts b/packages/browser/src/Session.ts index f07fcc7283..4ccb8f23cf 100644 --- a/packages/browser/src/Session.ts +++ b/packages/browser/src/Session.ts @@ -274,6 +274,7 @@ export class Session extends EventEmitter implements IHasSessionEventListener { this.info.isLoggedIn = false; if (emitSignal) { (this.events as EventEmitter).emit(EVENTS.LOGOUT); + (this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT); } }; @@ -349,6 +350,7 @@ export class Session extends EventEmitter implements IHasSessionEventListener { // The login event can only be triggered **after** the user has been // redirected from the IdP with access and ID tokens. (this.events as EventEmitter).emit(EVENTS.LOGIN); + (this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT); } else { // If an URL is stored in local storage, we are being logged in after a // silent authentication, so remove our currently stored URL location diff --git a/packages/core/src/SessionEventListener.ts b/packages/core/src/SessionEventListener.ts index 960c9d747f..86c2c17dd0 100644 --- a/packages/core/src/SessionEventListener.ts +++ b/packages/core/src/SessionEventListener.ts @@ -58,7 +58,7 @@ type FALLBACK_ARGS = { listener: never; }; type LOGIN_AND_LOGOUT_ARGS = { - eventName: typeof EVENTS.LOGIN | typeof EVENTS.LOGOUT; + eventName: typeof EVENTS.LOGIN_AND_LOGOUT; listener: () => void; } export interface ISessionEventListener extends EventEmitter { @@ -86,8 +86,8 @@ export interface ISessionEventListener extends EventEmitter { * @param listener The callback called on a successful login and logout. */ on( - eventName: LOGIN_ARGS["eventName"] | LOGOUT_ARGS["eventName"], - listener: LOGIN_ARGS["listener"] & LOGOUT_ARGS["listener"] + eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], + listener: LOGIN_AND_LOGOUT_ARGS["listener"] ): this; /** * Register a listener called on session expiration. @@ -177,7 +177,7 @@ export interface ISessionEventListener extends EventEmitter { * @param listener The callback called on a successful login or logout. */ addListener( - eventName: LOGIN_ARGS["eventName"] | LOGOUT_ARGS["eventName"], + eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], listener: LOGIN_AND_LOGOUT_ARGS["listener"] ): this; /** @@ -268,7 +268,7 @@ export interface ISessionEventListener extends EventEmitter { * @param listener The callback called on the next successful login or logout. */ once( - eventName: LOGIN_ARGS["eventName"] | LOGOUT_ARGS["eventName"], + eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], listener: LOGIN_AND_LOGOUT_ARGS["listener"] ): this; /** @@ -360,7 +360,7 @@ export interface ISessionEventListener extends EventEmitter { * @param listener The callback to unregister. */ off( - eventName: LOGIN_ARGS["eventName"] | LOGOUT_ARGS["eventName"], + eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], listener: LOGIN_AND_LOGOUT_ARGS["listener"] ): this; /** @@ -450,7 +450,7 @@ export interface ISessionEventListener extends EventEmitter { * @param listener The callback to unregister */ removeListener( - eventName: LOGIN_ARGS["eventName"] | LOGOUT_ARGS["eventName"], + eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], listener: LOGIN_AND_LOGOUT_ARGS["listener"] ): this; /** diff --git a/packages/core/src/constant.ts b/packages/core/src/constant.ts index 98f34f3714..b24aee998d 100644 --- a/packages/core/src/constant.ts +++ b/packages/core/src/constant.ts @@ -36,6 +36,7 @@ export const EVENTS = { ERROR: "error", LOGIN: "login", LOGOUT: "logout", + LOGIN_AND_LOGOUT: "loginAndLogout", NEW_REFRESH_TOKEN: "newRefreshToken", SESSION_EXPIRED: "sessionExpired", SESSION_EXTENDED: "sessionExtended", diff --git a/packages/node/src/Session.spec.ts b/packages/node/src/Session.spec.ts index 52c0e2a9b1..9b123c554b 100644 --- a/packages/node/src/Session.spec.ts +++ b/packages/node/src/Session.spec.ts @@ -488,9 +488,28 @@ describe("Session", () => { const mySession = new Session({ clientAuthentication }); mySession.events.on(EVENTS.LOGIN, myCallback); await mySession.handleIncomingRedirect("https://some.url"); - expect(myCallback).toHaveBeenCalled(); + expect(myCallback).toHaveBeenCalledTimes(1); }); + it("does not call the registered cb on logout", async () => { + const myCallback = jest.fn(); + const clientAuthentication = mockClientAuthentication(); + clientAuthentication.handleIncomingRedirect = jest + .fn() + .mockResolvedValue({ + isLoggedIn: true, + sessionId: "a session ID", + webId: "https://some.webid#them", + }); + const mySession = new Session({ clientAuthentication }); + mySession.events.on(EVENTS.LOGIN, myCallback); + await mySession.handleIncomingRedirect("https://some.url"); + expect(myCallback).toHaveBeenCalledTimes(1); + + await mySession.logout(); + expect(myCallback).toHaveBeenCalledTimes(1); + }) + it("does not call the registered callback if login isn't successful", async () => { const myCallback = jest.fn(); const clientAuthentication = mockClientAuthentication(); @@ -536,8 +555,9 @@ describe("Session", () => { mySession.events.on(EVENTS.LOGOUT, myCallback); await mySession.logout(); - expect(myCallback).toHaveBeenCalled(); + expect(myCallback).toHaveBeenCalledTimes(1); }); + }); describe("login and logout", () => { @@ -556,13 +576,12 @@ describe("Session", () => { webId: "https://some.webid#them", }); - mySession.events.on(EVENTS.LOGIN || EVENTS.LOGOUT, myCallback) + mySession.events.on(EVENTS.LOGIN_AND_LOGOUT, myCallback) await mySession.handleIncomingRedirect("https://some.url"); - expect(myCallback).toHaveBeenCalled(); - - mySession.events.on(EVENTS.LOGIN || EVENTS.LOGOUT, myCallback); + expect(myCallback).toHaveBeenCalledTimes(1); + await mySession.logout(); - expect(myCallback).toHaveBeenCalled(); + expect(myCallback).toHaveBeenCalledTimes(2); }) it("does not call the registered callback if login isn't successful", async () => { @@ -576,7 +595,8 @@ describe("Session", () => { webId: "https://some.webid#them", }); const mySession = new Session({ clientAuthentication }); - mySession.events.on(EVENTS.LOGIN || EVENTS.LOGOUT, myCallback); + mySession.events.on(EVENTS.LOGIN_AND_LOGOUT, myCallback); + expect(myCallback).not.toHaveBeenCalled(); }); }) diff --git a/packages/node/src/Session.ts b/packages/node/src/Session.ts index ac6a8e4951..f4df762d49 100644 --- a/packages/node/src/Session.ts +++ b/packages/node/src/Session.ts @@ -216,7 +216,7 @@ export class Session extends EventEmitter implements IHasSessionEventListener { if (loginInfo?.isLoggedIn) { // Send a signal on successful client credentials login. (this.events as EventEmitter).emit(EVENTS.LOGIN); - // (this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT); + (this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT); } }; @@ -286,7 +286,7 @@ export class Session extends EventEmitter implements IHasSessionEventListener { this.info.isLoggedIn = false; if (emitEvent) { (this.events as EventEmitter).emit(EVENTS.LOGOUT); - // (this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT); + (this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT); } }; @@ -323,6 +323,7 @@ export class Session extends EventEmitter implements IHasSessionEventListener { // The login event can only be triggered **after** the user has been // redirected from the IdP with access and ID tokens. (this.events as EventEmitter).emit(EVENTS.LOGIN); + (this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT); } } } finally { From 10ce1e993ea9af78355eab73bd56d44579896d4c Mon Sep 17 00:00:00 2001 From: Zach Date: Wed, 28 Feb 2024 11:14:43 -0600 Subject: [PATCH 09/10] factored event listeners into Session constructor for node and browser packages, renamed LOGIN_AND_LOGOUT to SESSION_STATUS_CHANGE --- packages/browser/src/Session.spec.ts | 4 ++-- packages/browser/src/Session.ts | 27 +++++++++++++++++------ packages/core/src/SessionEventListener.ts | 24 ++++++++++---------- packages/core/src/constant.ts | 2 +- packages/node/src/Session.spec.ts | 4 ++-- packages/node/src/Session.ts | 10 +++++---- 6 files changed, 43 insertions(+), 28 deletions(-) diff --git a/packages/browser/src/Session.spec.ts b/packages/browser/src/Session.spec.ts index dfc86c7057..01bf168688 100644 --- a/packages/browser/src/Session.spec.ts +++ b/packages/browser/src/Session.spec.ts @@ -999,7 +999,7 @@ describe("Session", () => { }); mockLocalStorage({}); - mySession.events.on(EVENTS.LOGIN_AND_LOGOUT, myCallback); + mySession.events.on(EVENTS.SESSION_STATUS_CHANGE, myCallback); await mySession.handleIncomingRedirect("https://some.url"); expect(myCallback).toHaveBeenCalledTimes(1); await mySession.logout(); @@ -1018,7 +1018,7 @@ describe("Session", () => { webId: "https://some.webid#them", }); const mySession = new Session({ clientAuthentication }); - mySession.events.on(EVENTS.LOGIN_AND_LOGOUT, myCallback); + mySession.events.on(EVENTS.SESSION_STATUS_CHANGE, myCallback); expect(myCallback).not.toHaveBeenCalled(); }); }) diff --git a/packages/browser/src/Session.ts b/packages/browser/src/Session.ts index 4ccb8f23cf..4f4bc792e2 100644 --- a/packages/browser/src/Session.ts +++ b/packages/browser/src/Session.ts @@ -212,13 +212,28 @@ export class Session extends EventEmitter implements IHasSessionEventListener { // enable silent refresh. The current session ID specifically stored in 'localStorage' // (as opposed to using our storage abstraction layer) because it is only // used in a browser-specific mechanism. - this.events.on(EVENTS.LOGIN, () => - window.localStorage.setItem(KEY_CURRENT_SESSION, this.info.sessionId), - ); + this.events.on(EVENTS.LOGIN, () => { + // You have to use the semicolon on this next line + // Because the underlying JS interpreter cannot interpret whether + // the EventEmitter cast is an IIFE or different token. + window.localStorage.setItem(KEY_CURRENT_SESSION, this.info.sessionId); + (this.events as EventEmitter).emit(EVENTS.SESSION_STATUS_CHANGE) + }); + + this.events.on(EVENTS.LOGOUT, () => { + (this.events as EventEmitter).emit(EVENTS.SESSION_STATUS_CHANGE); + }) + + this.events.on(EVENTS.SESSION_EXPIRED, () => { + this.internalLogout(false); + (this.events as EventEmitter).emit(EVENTS.SESSION_STATUS_CHANGE); + }); + + this.events.on(EVENTS.ERROR, () => { + this.internalLogout(false); + }); - this.events.on(EVENTS.SESSION_EXPIRED, () => this.internalLogout(false)); - this.events.on(EVENTS.ERROR, () => this.internalLogout(false)); } /** @@ -274,7 +289,6 @@ export class Session extends EventEmitter implements IHasSessionEventListener { this.info.isLoggedIn = false; if (emitSignal) { (this.events as EventEmitter).emit(EVENTS.LOGOUT); - (this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT); } }; @@ -350,7 +364,6 @@ export class Session extends EventEmitter implements IHasSessionEventListener { // The login event can only be triggered **after** the user has been // redirected from the IdP with access and ID tokens. (this.events as EventEmitter).emit(EVENTS.LOGIN); - (this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT); } else { // If an URL is stored in local storage, we are being logged in after a // silent authentication, so remove our currently stored URL location diff --git a/packages/core/src/SessionEventListener.ts b/packages/core/src/SessionEventListener.ts index 86c2c17dd0..f8ad105ed0 100644 --- a/packages/core/src/SessionEventListener.ts +++ b/packages/core/src/SessionEventListener.ts @@ -57,8 +57,8 @@ type FALLBACK_ARGS = { // Prevents from using a SessionEventEmitter as an aritrary EventEmitter. listener: never; }; -type LOGIN_AND_LOGOUT_ARGS = { - eventName: typeof EVENTS.LOGIN_AND_LOGOUT; +type SESSION_STATUS_CHANGE_ARGS = { + eventName: typeof EVENTS.SESSION_STATUS_CHANGE; listener: () => void; } export interface ISessionEventListener extends EventEmitter { @@ -86,8 +86,8 @@ export interface ISessionEventListener extends EventEmitter { * @param listener The callback called on a successful login and logout. */ on( - eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], - listener: LOGIN_AND_LOGOUT_ARGS["listener"] + eventName: SESSION_STATUS_CHANGE_ARGS["eventName"], + listener: SESSION_STATUS_CHANGE_ARGS["listener"] ): this; /** * Register a listener called on session expiration. @@ -177,8 +177,8 @@ export interface ISessionEventListener extends EventEmitter { * @param listener The callback called on a successful login or logout. */ addListener( - eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], - listener: LOGIN_AND_LOGOUT_ARGS["listener"] + eventName: SESSION_STATUS_CHANGE_ARGS["eventName"], + listener: SESSION_STATUS_CHANGE_ARGS["listener"] ): this; /** * Register a listener called on session expiration. @@ -268,8 +268,8 @@ export interface ISessionEventListener extends EventEmitter { * @param listener The callback called on the next successful login or logout. */ once( - eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], - listener: LOGIN_AND_LOGOUT_ARGS["listener"] + eventName: SESSION_STATUS_CHANGE_ARGS["eventName"], + listener: SESSION_STATUS_CHANGE_ARGS["listener"] ): this; /** * Register a listener called on the next session expiration. @@ -360,8 +360,8 @@ export interface ISessionEventListener extends EventEmitter { * @param listener The callback to unregister. */ off( - eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], - listener: LOGIN_AND_LOGOUT_ARGS["listener"] + eventName: SESSION_STATUS_CHANGE_ARGS["eventName"], + listener: SESSION_STATUS_CHANGE_ARGS["listener"] ): this; /** * Unegister a listener called on session expiration. @@ -450,8 +450,8 @@ export interface ISessionEventListener extends EventEmitter { * @param listener The callback to unregister */ removeListener( - eventName: LOGIN_AND_LOGOUT_ARGS["eventName"], - listener: LOGIN_AND_LOGOUT_ARGS["listener"] + eventName: SESSION_STATUS_CHANGE_ARGS["eventName"], + listener: SESSION_STATUS_CHANGE_ARGS["listener"] ): this; /** * Unegister a listener called on session expiration. diff --git a/packages/core/src/constant.ts b/packages/core/src/constant.ts index b24aee998d..9584600f70 100644 --- a/packages/core/src/constant.ts +++ b/packages/core/src/constant.ts @@ -36,7 +36,7 @@ export const EVENTS = { ERROR: "error", LOGIN: "login", LOGOUT: "logout", - LOGIN_AND_LOGOUT: "loginAndLogout", + SESSION_STATUS_CHANGE: "sessionStatusChange", NEW_REFRESH_TOKEN: "newRefreshToken", SESSION_EXPIRED: "sessionExpired", SESSION_EXTENDED: "sessionExtended", diff --git a/packages/node/src/Session.spec.ts b/packages/node/src/Session.spec.ts index 9b123c554b..d51b873ddd 100644 --- a/packages/node/src/Session.spec.ts +++ b/packages/node/src/Session.spec.ts @@ -576,7 +576,7 @@ describe("Session", () => { webId: "https://some.webid#them", }); - mySession.events.on(EVENTS.LOGIN_AND_LOGOUT, myCallback) + mySession.events.on(EVENTS.SESSION_STATUS_CHANGE, myCallback) await mySession.handleIncomingRedirect("https://some.url"); expect(myCallback).toHaveBeenCalledTimes(1); @@ -595,7 +595,7 @@ describe("Session", () => { webId: "https://some.webid#them", }); const mySession = new Session({ clientAuthentication }); - mySession.events.on(EVENTS.LOGIN_AND_LOGOUT, myCallback); + mySession.events.on(EVENTS.SESSION_STATUS_CHANGE, myCallback); expect(myCallback).not.toHaveBeenCalled(); }); diff --git a/packages/node/src/Session.ts b/packages/node/src/Session.ts index f4df762d49..32417e12b6 100644 --- a/packages/node/src/Session.ts +++ b/packages/node/src/Session.ts @@ -186,8 +186,13 @@ export class Session extends EventEmitter implements IHasSessionEventListener { this.lastTimeoutHandle = timeoutHandle; }); + this.events.on(EVENTS.LOGIN, () => (this.events as EventEmitter).emit(EVENTS.SESSION_STATUS_CHANGE)); + this.events.on(EVENTS.LOGOUT, () => (this.events as EventEmitter).emit(EVENTS.SESSION_STATUS_CHANGE)); this.events.on(EVENTS.ERROR, () => this.internalLogout(false)); - this.events.on(EVENTS.SESSION_EXPIRED, () => this.internalLogout(false)); + this.events.on(EVENTS.SESSION_EXPIRED, () => { + this.internalLogout(false); + (this.events as EventEmitter).emit(EVENTS.SESSION_STATUS_CHANGE) + }); } @@ -216,7 +221,6 @@ export class Session extends EventEmitter implements IHasSessionEventListener { if (loginInfo?.isLoggedIn) { // Send a signal on successful client credentials login. (this.events as EventEmitter).emit(EVENTS.LOGIN); - (this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT); } }; @@ -286,7 +290,6 @@ export class Session extends EventEmitter implements IHasSessionEventListener { this.info.isLoggedIn = false; if (emitEvent) { (this.events as EventEmitter).emit(EVENTS.LOGOUT); - (this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT); } }; @@ -323,7 +326,6 @@ export class Session extends EventEmitter implements IHasSessionEventListener { // The login event can only be triggered **after** the user has been // redirected from the IdP with access and ID tokens. (this.events as EventEmitter).emit(EVENTS.LOGIN); - (this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT); } } } finally { From b73d27516f561132e5ba6ab1bbb75b1d4423f7d8 Mon Sep 17 00:00:00 2001 From: Zach Date: Wed, 28 Feb 2024 11:32:50 -0600 Subject: [PATCH 10/10] changed e2e spec test case and combined with larger block --- e2e/node/e2e-test.spec.ts | 44 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/e2e/node/e2e-test.spec.ts b/e2e/node/e2e-test.spec.ts index c7aa8d03c1..1221fe51bf 100644 --- a/e2e/node/e2e-test.spec.ts +++ b/e2e/node/e2e-test.spec.ts @@ -274,30 +274,30 @@ describe("Session events", () => { expect(logoutFunc).toHaveBeenCalledTimes(1); expect(expiredFunc).toHaveBeenCalledTimes(1); }); -}); - -describe("New combined login and logout session event", () => { - jest.setTimeout(15 * 60 * 1000); - let session: Session; - let loginAndLogoutFunc: () => void; - let expiredFunc: () => void; - - beforeEach(async () => { - session = new Session(); - loginAndLogoutFunc = jest.fn(); - expiredFunc = jest.fn(); - session.events.on(EVENTS.LOGIN_AND_LOGOUT, loginAndLogoutFunc); - session.events.on(EVENTS.SESSION_EXPIRED, expiredFunc); + it("sends a session status changed event on login, logout, and session expiration", async() => { + let sessionStatusChangeFunc: () => void; + sessionStatusChangeFunc = jest.fn(); + session.events.on(EVENTS.SESSION_STATUS_CHANGE, sessionStatusChangeFunc); + + expect(session.info.isLoggedIn).toBe(true); - await session.login(getCredentials()); - }); + if (typeof session.info.expirationDate !== "number") { + throw new Error("Cannot determine session expiration date"); + } + const expiresIn = session.info.expirationDate - Date.now(); + await new Promise((resolve) => { + setTimeout(resolve, expiresIn); + }); - it("tests to make sure the function is called during both login and logout", async () => { - expect(session.info.isLoggedIn).toBe(true); + expect(loginFunc).toHaveBeenCalledTimes(1); + expect(logoutFunc).toHaveBeenCalledTimes(0); + expect(expiredFunc).toHaveBeenCalledTimes(1); + expect(sessionStatusChangeFunc).toHaveBeenCalledTimes(2); await session.logout(); - - expect(loginAndLogoutFunc).toHaveBeenCalledTimes(2); - expect(expiredFunc).toHaveBeenCalledTimes(0); + expect(loginFunc).toHaveBeenCalledTimes(1); + expect(logoutFunc).toHaveBeenCalledTimes(1); + expect(expiredFunc).toHaveBeenCalledTimes(1); + expect(sessionStatusChangeFunc).toHaveBeenCalledTimes(3); }) -}) \ No newline at end of file +}); \ No newline at end of file