diff --git a/src/auth-store.ts b/src/auth-store.ts index 449cdd2..6201c24 100644 --- a/src/auth-store.ts +++ b/src/auth-store.ts @@ -2,15 +2,15 @@ import { AppUserRead, JwtBundle } from "@/model"; import { only } from "@/util"; export interface AuthStore { - getData(storageKey: string): { jwtBundle: JwtBundle, user: AppUserRead } | void; - setData(storageKey: string, {}: { jwtBundle: JwtBundle; user: AppUserRead }): void; - removeData(storageKey: string): void; + getData(storageKey: string): Promise<{ jwtBundle: JwtBundle, user: AppUserRead } | void>; + setData(storageKey: string, {}: { jwtBundle: JwtBundle; user: AppUserRead }): Promise; + removeData(storageKey: string): Promise; } export class StorageAuthStore implements AuthStore { public constructor(protected storage: Storage) {} - public getData(storageKey: string) { + public async getData(storageKey: string) { const serialized = this.storage.getItem(storageKey); try { return only(JSON.parse(serialized!), "jwtBundle", "user") @@ -19,12 +19,12 @@ export class StorageAuthStore implements AuthStore { } } - public setData(storageKey: string, { jwtBundle, user }: { jwtBundle: JwtBundle; user: AppUserRead }) { + public async setData(storageKey: string, { jwtBundle, user }: { jwtBundle: JwtBundle; user: AppUserRead }) { const serialized = JSON.stringify({ jwtBundle, user }); this.storage.setItem(storageKey, serialized); } - removeData(storageKey: string): void { + public async removeData(storageKey: string) { this.storage.removeItem(storageKey); } } diff --git a/src/index.ts b/src/index.ts index b2e1f5a..2700f86 100644 --- a/src/index.ts +++ b/src/index.ts @@ -76,9 +76,9 @@ export class SkeletonKey } public async init() { - this.load(); this.bindMethods(); this.installListeners(); + await this.load(); if (this.isLoggedIn() && this.initialLoginCheck) await this.refreshInfo(); await this.installInterval(); this.emit("initialized", this); @@ -121,7 +121,7 @@ export class SkeletonKey this.jwtBundle = jwtTokenBundle as JwtBundle & TOKEN_DATA; this.user = user as AppUserRead & USER_DATA; this.emitSync("login", user); - this.persist(); + await this.persist(); return user; } @@ -129,7 +129,7 @@ export class SkeletonKey this.user = undefined; this.jwtBundle = undefined; this.emitSync("logout"); - this.persist(); + await this.persist(); return true; } @@ -165,7 +165,7 @@ export class SkeletonKey try { this.user = (await this.client.me(this.jwtBundle!.token)) as AppUserRead & USER_DATA; this.emitSync("refresh", "user", this.user); - this.persist(); + await this.persist(); } catch ({ response: { status } }) { this.handleStatus(status); } @@ -198,15 +198,15 @@ export class SkeletonKey return decode(this.jwtBundle!.refreshToken); } - public persist() { - if (this.isLoggedIn()) this.store.setData(this.storageKey, only(this, "jwtBundle", "user") as any); - else this.store.removeData(this.storageKey); + public async persist() { + if (this.isLoggedIn()) await this.store.setData(this.storageKey, only(this, "jwtBundle", "user") as any); + else await this.store.removeData(this.storageKey); } - public load() { + public async load() { if (!this.isLoggedIn()) { - const data = this.store.getData(this.storageKey); - if (!data) return this.persist(); + const data = await this.store.getData(this.storageKey); + if (!data) return await this.persist(); Object.assign(this, only(data, "jwtBundle", "user")); } } diff --git a/test/index.test.ts b/test/index.test.ts index 7d0f67b..7f5fc22 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -15,51 +15,52 @@ import { jest.useFakeTimers(); describe("index", () => { - beforeEach(() => mock.setup()); - afterEach(() => mock.teardown()); - afterEach(() => fetchReset()); + beforeEach(() => { + mock.setup(); + }); + afterEach(() => { + mock.teardown(); + interceptors.splice(0, interceptors.length); + fetchReset(); + }); describe("SkeletonKey", () => { const skey = SkeletonKeyDefaults.storageKey!; describe("new(), #installListeners()", () => { it("should register interceptors if enabled", () => { - interceptors.splice(0, interceptors.length); const key = new SkeletonKey(); expect(interceptors.length).toEqual(1); expect(interceptors[0]).toEqual(key); }); it("should not register interceptors if disabled", () => { - interceptors.splice(0, interceptors.length); new SkeletonKey({ intercept: false }); expect(interceptors.length).toEqual(0); }); }); describe("new(), #load()", () => { - it("should load data from localStorage if it exists", () => { + it("should load data from localStorage if it exists", async () => { localStorage.setItem(skey, STORAGE_VALID_TOKEN); - interceptors.splice(0, interceptors.length); const auth = new SkeletonKey({ intercept: false, initialLoginCheck: false }); + await auth.ensureInitialized(); expect(auth.jwtBundle!.token).toEqual(JWT_VALID_TOKEN); expect(auth.jwtBundle!.refreshToken).toEqual(JWT_VALID_REFRESH); expect(auth.userData).toEqual(JSON.parse(USER_DATA)); expect(auth.isLoggedIn()).toBeTruthy(); }); - it("should remove invalid data from localStorage", () => { + it("should remove invalid data from localStorage", async () => { localStorage.setItem(skey, STORAGE_VALID_TOKEN.substr(1)); - interceptors.splice(0, interceptors.length); const auth = new SkeletonKey({ intercept: false }); + await auth.ensureInitialized(); expect(auth.isLoggedIn()).toBeFalsy(); expect(localStorage.getItem(skey)).toBeFalsy(); }); it("should automatically delete token data if initialLoginCheck is true", async () => { localStorage.setItem(skey, STORAGE_VALID_TOKEN); - interceptors.splice(0, interceptors.length); - mock.get(urlAbsolute("/auth/me"), (req, res) => { res.status(401); res.header("Content-Type", "application/json"); @@ -80,7 +81,6 @@ describe("index", () => { describe("#installInterval()", () => { it("should be called on login and creation", async () => { localStorage.setItem(skey, STORAGE_VALID_TOKEN); - interceptors.splice(0, interceptors.length); const orig = SkeletonKey.prototype.installInterval; const spy = jest.fn(); @@ -94,7 +94,7 @@ describe("index", () => { SkeletonKey.prototype.installInterval = spy; const auth = new SkeletonKey({ intercept: false, renewType: "interval" }); - await auth.waitForEvent("initialized"); + await auth.ensureInitialized(); SkeletonKey.prototype.installInterval = orig; expect(spy).toHaveBeenCalledTimes(1); @@ -104,8 +104,6 @@ describe("index", () => { it("should not install a timer if the user isn't logged in", () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); - new SkeletonKey({ intercept: false, renewType: "interval" }); expect(setTimeout).not.toHaveBeenCalled(); @@ -113,8 +111,6 @@ describe("index", () => { it("should delete token data if the token is valid but incorrect", async () => { localStorage.setItem(skey, STORAGE_EXPIRED_TOKEN); - interceptors.splice(0, interceptors.length); - mock.get(urlAbsolute("/auth/refresh"), (req, res) => { res.status(401); res.header("Content-Type", "application/json"); @@ -123,6 +119,7 @@ describe("index", () => { }); const auth = new SkeletonKey({ intercept: false, renewType: "never", initialLoginCheck: false }); + await auth.ensureInitialized(); await auth.refreshToken(); @@ -134,8 +131,6 @@ describe("index", () => { it("should try to refresh the token if it needs to be refreshed", async () => { localStorage.setItem(skey, STORAGE_EXPIRED_TOKEN); - interceptors.splice(0, interceptors.length); - mock.get(urlAbsolute("/auth/refresh"), (req, res) => { expect(req.header("Authorization")).toEqual(`Bearer ${JWT_VALID_REFRESH}`); res.status(200); @@ -145,21 +140,20 @@ describe("index", () => { }); const auth = new SkeletonKey({ intercept: false, renewType: "interval", initialLoginCheck: false }); - await auth.waitForEvent("initialized"); + await auth.ensureInitialized(); expect(auth.jwtBundle!.token).toEqual(JWT_VALID_TOKEN); }); }); describe("#persist()", () => { - it("should write data to localStorage, if logged in", () => { + it("should write data to localStorage, if logged in", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); const auth = new SkeletonKey({ intercept: false }); auth.user = JSON.parse(USER_DATA); auth.jwtBundle = JSON.parse(STORAGE_VALID_TOKEN).jwtBundle; expect(auth.isLoggedIn()).toBeTruthy(); - auth.persist(); + await auth.persist(); expect(JSON.parse(localStorage.getItem(skey)!)).toEqual(JSON.parse(STORAGE_VALID_TOKEN)); }); }); @@ -167,14 +161,12 @@ describe("index", () => { describe("#isLoggedIn()", () => { it("should be false if no user or token are stored", () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); const auth = new SkeletonKey({ intercept: false }); expect(auth.isLoggedIn()).toBeFalsy(); }); it("should be true for valid user and tokens", () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); const auth = new SkeletonKey({ intercept: false }); auth.user = JSON.parse(USER_DATA); auth.jwtBundle = JSON.parse(STORAGE_VALID_TOKEN).jwtBundle; @@ -183,7 +175,6 @@ describe("index", () => { it("should be true for expired token if refresh token isn't expired", () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); const auth = new SkeletonKey({ intercept: false }); auth.user = JSON.parse(USER_DATA); auth.jwtBundle = JSON.parse(STORAGE_EXPIRED_TOKEN).jwtBundle; @@ -192,7 +183,6 @@ describe("index", () => { it("should be false for expired token and refresh token", () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); const auth = new SkeletonKey({ intercept: false }); auth.user = JSON.parse(USER_DATA); auth.jwtBundle = JSON.parse(STORAGE_EXPIRED_REFRESH).jwtBundle; @@ -203,7 +193,6 @@ describe("index", () => { describe("#onAction()", () => { it("should not do anything if no url is passed", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); const auth = new SkeletonKey({ intercept: false }); auth.user = JSON.parse(USER_DATA); auth.jwtBundle = JSON.parse(STORAGE_EXPIRED_TOKEN).jwtBundle; @@ -214,7 +203,6 @@ describe("index", () => { it("should not do anything if the url matches the refresh url", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); const auth = new SkeletonKey({ intercept: false }); auth.user = JSON.parse(USER_DATA); auth.jwtBundle = JSON.parse(STORAGE_EXPIRED_TOKEN).jwtBundle; @@ -225,7 +213,6 @@ describe("index", () => { it("should try to refresh the token if the token is expired and the refreshToken is valid", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); const auth = new SkeletonKey({ intercept: false }); auth.user = JSON.parse(USER_DATA); auth.jwtBundle = JSON.parse(STORAGE_EXPIRED_TOKEN).jwtBundle; @@ -236,7 +223,6 @@ describe("index", () => { it("should not try to refresh the token if the token is valid", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); const auth = new SkeletonKey({ intercept: false }); auth.user = JSON.parse(USER_DATA); auth.jwtBundle = JSON.parse(STORAGE_VALID_TOKEN).jwtBundle; @@ -247,7 +233,6 @@ describe("index", () => { it("should not try to refresh the token if the refresh token is expired", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); const auth = new SkeletonKey({ intercept: false }); auth.user = JSON.parse(USER_DATA); auth.jwtBundle = JSON.parse(STORAGE_EXPIRED_REFRESH).jwtBundle; @@ -264,8 +249,6 @@ describe("index", () => { it("should send a login request to the auth service", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); - const auth = new SkeletonKey({ intercept: false }); expect(auth.isLoggedIn()).toBeFalsy(); @@ -287,8 +270,6 @@ describe("index", () => { it("should logout the user before trying to log in again", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); - const auth = new SkeletonKey({ intercept: false }); auth.user = JSON.parse(USER_DATA); @@ -311,8 +292,6 @@ describe("index", () => { it("should emit the login event after successful login", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); - const auth = new SkeletonKey({ intercept: false }); mock.post(urlAbsolute("/auth/login"), (req, res) => { @@ -335,8 +314,6 @@ describe("index", () => { describe("#logout()", () => { it("should log out the current user", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); - const auth = new SkeletonKey({ intercept: false }); auth.user = JSON.parse(USER_DATA); auth.jwtBundle = JSON.parse(STORAGE_VALID_TOKEN).jwtBundle; @@ -347,8 +324,6 @@ describe("index", () => { it("trigger the 'logout' event", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); - const auth = new SkeletonKey({ intercept: false }); auth.user = JSON.parse(USER_DATA); auth.jwtBundle = JSON.parse(STORAGE_VALID_TOKEN).jwtBundle; @@ -370,8 +345,6 @@ describe("index", () => { it("should return a promise that resolves upon successful login", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); - const auth = new SkeletonKey({ intercept: false }); const spy = jest.fn(); auth.waitForLogin().then(spy); @@ -393,8 +366,6 @@ describe("index", () => { describe("#refreshToken()", () => { it("should refresh a jwt using the refreshToken", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); - const auth = new SkeletonKey({ intercept: false }); auth.user = JSON.parse(USER_DATA); auth.jwtBundle = JSON.parse(STORAGE_EXPIRED_TOKEN).jwtBundle; @@ -414,8 +385,6 @@ describe("index", () => { it("should fire a 'refresh' event", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); - const auth = new SkeletonKey({ intercept: false }); auth.user = JSON.parse(USER_DATA); auth.jwtBundle = JSON.parse(STORAGE_EXPIRED_TOKEN).jwtBundle; @@ -439,8 +408,6 @@ describe("index", () => { it("should return false if no tokens are defined", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); - const auth = new SkeletonKey({ intercept: false }); auth.user = JSON.parse(USER_DATA); @@ -451,8 +418,6 @@ describe("index", () => { describe("#refreshInfo()", () => { it("should refresh the user information", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); - const auth = new SkeletonKey({ intercept: false }); auth.user = JSON.parse(USER_DATA); auth.jwtBundle = JSON.parse(STORAGE_VALID_TOKEN).jwtBundle; @@ -474,8 +439,6 @@ describe("index", () => { it("should emit a 'refresh' event", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); - const auth = new SkeletonKey({ intercept: false }); auth.user = JSON.parse(USER_DATA); auth.jwtBundle = JSON.parse(STORAGE_VALID_TOKEN).jwtBundle; @@ -501,8 +464,6 @@ describe("index", () => { it("should return false if the user isn't logged in", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); - const auth = new SkeletonKey({ intercept: false }); expect(await auth.refreshInfo()).toBeFalsy(); }); @@ -511,8 +472,6 @@ describe("index", () => { describe("#onXhrSend()", () => { it("should set an auth header on an xhr object", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); - const auth = new SkeletonKey(); auth.user = JSON.parse(USER_DATA); auth.jwtBundle = JSON.parse(STORAGE_VALID_TOKEN).jwtBundle; @@ -527,8 +486,6 @@ describe("index", () => { it("should fire an 'action' event on send", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); - const auth = new SkeletonKey(); auth.user = JSON.parse(USER_DATA); auth.jwtBundle = JSON.parse(STORAGE_VALID_TOKEN).jwtBundle; @@ -548,8 +505,6 @@ describe("index", () => { describe("#onFetch()", () => { it("should add auth headers to request info", async () => { localStorage.removeItem(skey); - interceptors.splice(0, interceptors.length); - fetchMock(urlAbsolute("/some/thing"), (url, opts) => { expect((opts!.headers! as any)["Authorization"]).toEqual(`Bearer ${JWT_VALID_TOKEN}`); return 200;