Skip to content
28 changes: 27 additions & 1 deletion e2e/node/e2e-test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,4 +274,30 @@ describe("Session events", () => {
expect(logoutFunc).toHaveBeenCalledTimes(1);
expect(expiredFunc).toHaveBeenCalledTimes(1);
});
});

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);

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);
});

expect(loginFunc).toHaveBeenCalledTimes(1);
expect(logoutFunc).toHaveBeenCalledTimes(0);
expect(expiredFunc).toHaveBeenCalledTimes(1);
expect(sessionStatusChangeFunc).toHaveBeenCalledTimes(2);
await session.logout();
expect(loginFunc).toHaveBeenCalledTimes(1);
expect(logoutFunc).toHaveBeenCalledTimes(1);
expect(expiredFunc).toHaveBeenCalledTimes(1);
expect(sessionStatusChangeFunc).toHaveBeenCalledTimes(3);
})
});
41 changes: 41 additions & 0 deletions packages/browser/src/Session.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,47 @@ 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<ClientAuthentication["handleIncomingRedirect"]>()
.mockResolvedValue({
isLoggedIn: true,
sessionId: "a session ID",
webId: "https://some.webid#them",
});
mockLocalStorage({});

mySession.events.on(EVENTS.SESSION_STATUS_CHANGE, myCallback);
await mySession.handleIncomingRedirect("https://some.url");
expect(myCallback).toHaveBeenCalledTimes(1);
await mySession.logout();
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<ClientAuthentication["handleIncomingRedirect"]>()
.mockResolvedValue({
isLoggedIn: true,
sessionId: "a session ID",
webId: "https://some.webid#them",
});
const mySession = new Session({ clientAuthentication });
mySession.events.on(EVENTS.SESSION_STATUS_CHANGE, myCallback);
expect(myCallback).not.toHaveBeenCalled();
});
})

describe("sessionRestore", () => {
it("calls the registered callback on session restore", async () => {
// Set our window's location to our test value.
Expand Down
25 changes: 20 additions & 5 deletions packages/browser/src/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

/**
Expand Down
50 changes: 49 additions & 1 deletion packages/core/src/SessionEventListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ type FALLBACK_ARGS = {
// Prevents from using a SessionEventEmitter as an aritrary EventEmitter.
listener: never;
};

type SESSION_STATUS_CHANGE_ARGS = {
eventName: typeof EVENTS.SESSION_STATUS_CHANGE;
listener: () => void;
}
export interface ISessionEventListener extends EventEmitter {
/**
* Register a listener called on successful login.
Expand All @@ -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: SESSION_STATUS_CHANGE_ARGS["eventName"],
listener: SESSION_STATUS_CHANGE_ARGS["listener"]
): this;
/**
* Register a listener called on session expiration.
* @param eventName The session expiration event name.
Expand Down Expand Up @@ -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: SESSION_STATUS_CHANGE_ARGS["eventName"],
listener: SESSION_STATUS_CHANGE_ARGS["listener"]
): this;
/**
* Register a listener called on session expiration.
* @param eventName The session expiration event name.
Expand Down Expand Up @@ -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: SESSION_STATUS_CHANGE_ARGS["eventName"],
listener: SESSION_STATUS_CHANGE_ARGS["listener"]
): this;
/**
* Register a listener called on the next session expiration.
* @param eventName The session expiration event name.
Expand Down Expand Up @@ -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: SESSION_STATUS_CHANGE_ARGS["eventName"],
listener: SESSION_STATUS_CHANGE_ARGS["listener"]
): this;
/**
* Unegister a listener called on session expiration.
* @param eventName The session expiration event name.
Expand Down Expand Up @@ -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: SESSION_STATUS_CHANGE_ARGS["eventName"],
listener: SESSION_STATUS_CHANGE_ARGS["listener"]
): this;
/**
* Unegister a listener called on session expiration.
* @param eventName The session expiration event name.
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const EVENTS = {
ERROR: "error",
LOGIN: "login",
LOGOUT: "logout",
SESSION_STATUS_CHANGE: "sessionStatusChange",
NEW_REFRESH_TOKEN: "newRefreshToken",
SESSION_EXPIRED: "sessionExpired",
SESSION_EXTENDED: "sessionExtended",
Expand Down
65 changes: 63 additions & 2 deletions packages/node/src/Session.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClientAuthentication["handleIncomingRedirect"]>()
.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();
Expand Down Expand Up @@ -536,10 +555,52 @@ describe("Session", () => {

mySession.events.on(EVENTS.LOGOUT, myCallback);
await mySession.logout();
expect(myCallback).toHaveBeenCalled();
expect(myCallback).toHaveBeenCalledTimes(1);
});

});

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<ClientAuthentication["handleIncomingRedirect"]>()
.mockResolvedValue({
isLoggedIn: true,
sessionId: "a session ID",
webId: "https://some.webid#them",
});

mySession.events.on(EVENTS.SESSION_STATUS_CHANGE, myCallback)
await mySession.handleIncomingRedirect("https://some.url");
expect(myCallback).toHaveBeenCalledTimes(1);

await mySession.logout();
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<ClientAuthentication["handleIncomingRedirect"]>()
.mockResolvedValue({
isLoggedIn: true,
sessionId: "a session ID",
webId: "https://some.webid#them",
});
const mySession = new Session({ clientAuthentication });
mySession.events.on(EVENTS.SESSION_STATUS_CHANGE, myCallback);

expect(myCallback).not.toHaveBeenCalled();
});
})

describe("sessionExpired", () => {
it("calls the provided callback when receiving the appropriate event", async () => {
const myCallback = jest.fn();
Expand Down
8 changes: 7 additions & 1 deletion packages/node/src/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,16 @@ 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)
});
}


/**
* Triggers the login process. Note that this method will redirect the user away from your app.
*
Expand Down