Skip to content

Commit

Permalink
Merge pull request #18 from fostertheweb/feat/async-store
Browse files Browse the repository at this point in the history
Feature: Support async cache store
  • Loading branch information
fostertheweb authored Jul 10, 2024
2 parents f47b484 + e929ba3 commit b7bff8a
Show file tree
Hide file tree
Showing 11 changed files with 84 additions and 76 deletions.
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20
30 changes: 15 additions & 15 deletions src/SpotifyApi.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { beforeEach, describe, expect, it } from "vitest";
import { SpotifyApi } from "./SpotifyApi";
import { buildUnitTestSdkInstance } from "./test/SpotifyApiBuilder";
import { FakeAuthStrategy } from "./test/FakeAuthStrategy";
import { FetchApiMock } from "./test/FetchApiMock";
import { validAlbumResult } from "./test/data/validAlbumResult";
import AuthorizationCodeWithPKCEStrategy from "./auth/AuthorizationCodeWithPKCEStrategy";
import ClientCredentialsStrategy from "./auth/ClientCredentialsStrategy";
import ImplicitGrantStrategy from "./auth/ImplicitGrantStrategy";
import ProvidedAccessTokenStrategy from "./auth/ProvidedAccessTokenStrategy";
import { AccessToken, SdkOptions } from "./types";
import InMemoryCachingStrategy from "./caching/InMemoryCachingStrategy";
import { FakeAuthStrategy } from "./test/FakeAuthStrategy";
import { FetchApiMock } from "./test/FetchApiMock";
import { buildUnitTestSdkInstance } from "./test/SpotifyApiBuilder";
import { validAlbumResult } from "./test/data/validAlbumResult";
import { AccessToken, SdkOptions } from "./types";

describe("SpotifyAPI Instance", () => {
let sut: SpotifyApi;
Expand All @@ -26,7 +26,7 @@ describe("SpotifyAPI Instance", () => {

const [headers, bodyString] = fetchMock.issuedRequestHeadersAndBody(0);
expect((headers as any).Authorization).toBe(
`Bearer ${FakeAuthStrategy.FAKE_AUTH_TOKEN}`,
`Bearer ${FakeAuthStrategy.FAKE_AUTH_TOKEN}`
);
});
});
Expand Down Expand Up @@ -76,10 +76,10 @@ describe("SpotifyAPI Instance", () => {
const sut = SpotifyApi.withUserAuthorization(
"client-id",
"https://localhost:3000",
["scope1", "scope2"],
["scope1", "scope2"]
);
expect(sut["authenticationStrategy"].constructor.name).toBe(
AuthorizationCodeWithPKCEStrategy.name,
AuthorizationCodeWithPKCEStrategy.name
);
});

Expand All @@ -89,7 +89,7 @@ describe("SpotifyAPI Instance", () => {
"scope2",
]);
expect(sut["authenticationStrategy"].constructor.name).toBe(
ClientCredentialsStrategy.name,
ClientCredentialsStrategy.name
);
});

Expand All @@ -99,30 +99,30 @@ describe("SpotifyAPI Instance", () => {
"scope2",
]);
expect(sut["authenticationStrategy"].constructor.name).toBe(
ImplicitGrantStrategy.name,
ImplicitGrantStrategy.name
);
});

it("can create an instance with the provided access token strategy configured", async () => {
const sut = SpotifyApi.withAccessToken("client-id", {} as AccessToken);
expect(sut["authenticationStrategy"].constructor.name).toBe(
ProvidedAccessTokenStrategy.name,
ProvidedAccessTokenStrategy.name
);
});

it("when access token provided, it is accurately retrieved taking precedence over any existing cached token.", async () => {
const config: SdkOptions = {
cachingStrategy: new InMemoryCachingStrategy(),
};
config.cachingStrategy?.setCacheItem(
await config.cachingStrategy?.setCacheItem(
"spotify-sdk:ProvidedAccessTokenStrategy:token",
{ access_token: "some-old-token" },
{ access_token: "some-old-token" }
);

const sut = SpotifyApi.withAccessToken(
"client-id",
{ access_token: "some-new-token" } as AccessToken,
config,
config
);
const token = await sut.getAccessToken();

Expand All @@ -139,7 +139,7 @@ describe("SpotifyAPI Instance", () => {
it("authenticates successfully", async () => {
const response = await sut.authenticate();
expect(response.accessToken.access_token).toBe(
FakeAuthStrategy.FAKE_AUTH_TOKEN,
FakeAuthStrategy.FAKE_AUTH_TOKEN
);
expect(response.authenticated).toBe(true);

Expand Down
4 changes: 2 additions & 2 deletions src/SpotifyApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,8 @@ export class SpotifyApi {
/**
* Removes the access token if it exists.
*/
public logOut(): void {
this.authenticationStrategy.removeAccessToken();
public async logOut(): Promise<void> {
await this.authenticationStrategy.removeAccessToken();
}

public static withUserAuthorization(
Expand Down
30 changes: 15 additions & 15 deletions src/auth/AuthorizationCodeWithPKCEStrategy.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type {
ICachable,
SdkConfiguration,
AccessToken,
ICachable,
ICachingStrategy,
SdkConfiguration,
} from "../types.js";
import AccessTokenHelpers from "./AccessTokenHelpers.js";
import IAuthStrategy, { emptyAccessToken } from "./IAuthStrategy.js";
Expand All @@ -25,7 +25,7 @@ export default class AuthorizationCodeWithPKCEStrategy
constructor(
protected clientId: string,
protected redirectUri: string,
protected scopes: string[],
protected scopes: string[]
) {}

public setConfiguration(configuration: SdkConfiguration): void {
Expand All @@ -42,23 +42,23 @@ export default class AuthorizationCodeWithPKCEStrategy
async (expiring) => {
return AccessTokenHelpers.refreshCachedAccessToken(
this.clientId,
expiring,
expiring
);
},
}
);

return token;
}

public async getAccessToken(): Promise<AccessToken | null> {
const token = await this.cache.get<AccessToken>(
AuthorizationCodeWithPKCEStrategy.cacheKey,
AuthorizationCodeWithPKCEStrategy.cacheKey
);
return token;
}

public removeAccessToken(): void {
this.cache.remove(AuthorizationCodeWithPKCEStrategy.cacheKey);
public async removeAccessToken(): Promise<void> {
await this.cache.remove(AuthorizationCodeWithPKCEStrategy.cacheKey);
}

private async redirectOrVerifyToken(): Promise<AccessToken> {
Expand All @@ -83,24 +83,24 @@ export default class AuthorizationCodeWithPKCEStrategy
verifier,
expiresOnAccess: true,
};
this.cache.setCacheItem("spotify-sdk:verifier", singleUseVerifier);
await this.cache.setCacheItem("spotify-sdk:verifier", singleUseVerifier);

const redirectTarget = await this.generateRedirectUrlForUser(
this.scopes,
challenge,
challenge
);
await this.configuration!.redirectionStrategy.redirect(redirectTarget);
}

private async verifyAndExchangeCode(code: string) {
const cachedItem = await this.cache.get<CachedVerifier>(
"spotify-sdk:verifier",
"spotify-sdk:verifier"
);
const verifier = cachedItem?.verifier;

if (!verifier) {
throw new Error(
"No verifier found in cache - can't validate query string callback parameters.",
"No verifier found in cache - can't validate query string callback parameters."
);
}

Expand All @@ -118,7 +118,7 @@ export default class AuthorizationCodeWithPKCEStrategy

protected async generateRedirectUrlForUser(
scopes: string[],
challenge: string,
challenge: string
) {
const scope = scopes.join(" ");

Expand All @@ -135,7 +135,7 @@ export default class AuthorizationCodeWithPKCEStrategy

protected async exchangeCodeForToken(
code: string,
verifier: string,
verifier: string
): Promise<AccessToken> {
const params = new URLSearchParams();
params.append("client_id", this.clientId);
Expand All @@ -154,7 +154,7 @@ export default class AuthorizationCodeWithPKCEStrategy

if (!result.ok) {
throw new Error(
`Failed to exchange code for token: ${result.statusText}, ${text}`,
`Failed to exchange code for token: ${result.statusText}, ${text}`
);
}

Expand Down
4 changes: 2 additions & 2 deletions src/auth/ClientCredentialsStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ export default class ClientCredentialsStrategy implements IAuthStrategy {
return token;
}

public removeAccessToken(): void {
this.cache.remove(ClientCredentialsStrategy.cacheKey);
public async removeAccessToken(): Promise<void> {
await this.cache.remove(ClientCredentialsStrategy.cacheKey);
}

private async getTokenFromApi(): Promise<AccessToken> {
Expand Down
2 changes: 1 addition & 1 deletion src/auth/IAuthStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ export default interface IAuthStrategy {
setConfiguration(configuration: SdkConfiguration): void;
getOrCreateAccessToken(): Promise<AccessToken>;
getAccessToken(): Promise<AccessToken | null>;
removeAccessToken(): void;
removeAccessToken(): void | Promise<void>;
}
14 changes: 7 additions & 7 deletions src/auth/ImplicitGrantStrategy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type {
SdkConfiguration,
AccessToken,
ICachingStrategy,
SdkConfiguration,
} from "../types.js";
import AccessTokenHelpers from "./AccessTokenHelpers.js";
import IAuthStrategy, { emptyAccessToken } from "./IAuthStrategy.js";
Expand All @@ -16,7 +16,7 @@ export default class ImplicitGrantStrategy implements IAuthStrategy {
constructor(
private clientId: string,
private redirectUri: string,
private scopes: string[],
private scopes: string[]
) {}

public setConfiguration(configuration: SdkConfiguration): void {
Expand All @@ -33,23 +33,23 @@ export default class ImplicitGrantStrategy implements IAuthStrategy {
async (expiring) => {
return AccessTokenHelpers.refreshCachedAccessToken(
this.clientId,
expiring,
expiring
);
},
}
);

return token;
}

public async getAccessToken(): Promise<AccessToken | null> {
const token = await this.cache.get<AccessToken>(
ImplicitGrantStrategy.cacheKey,
ImplicitGrantStrategy.cacheKey
);
return token;
}

public removeAccessToken(): void {
this.cache.remove(ImplicitGrantStrategy.cacheKey);
public async removeAccessToken(): Promise<void> {
await this.cache.remove(ImplicitGrantStrategy.cacheKey);
}

private async redirectOrVerifyToken(): Promise<AccessToken> {
Expand Down
24 changes: 12 additions & 12 deletions src/caching/GenericCache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,27 @@ describe("GenericCache", () => {
});

it("should set and get a value", async () => {
sut.set("test", { test: "test" }, 1000);
await sut.set("test", { test: "test" }, 1000);
const result = await sut.get<{ test: string }>("test");
expect(result?.test).toBe("test");
});

it("should remove a value", async () => {
sut.set("test", { test: "test" }, 1000);
await sut.set("test", { test: "test" }, 1000);
sut.remove("test");
const result = await sut.get<{ test: string }>("test");
expect(result).toBeNull();
});

it("should return null for expired value", async () => {
sut.set("test", { test: "test" }, 0);
await sut.set("test", { test: "test" }, 0);
const result = await sut.get<{ test: string }>("test");
expect(result).toBeNull();
});

it("should return and remove value if expiresOnAccess is true", async () => {
const value = { test: "test", expiresOnAccess: true };
sut.setCacheItem("test", value);
await sut.setCacheItem("test", value);

const result = await sut.get<{ test: string }>("test");
expect(result).toEqual(value);
Expand All @@ -49,7 +49,7 @@ describe("GenericCache", () => {
await expect(
sut.getOrCreate("test", async () => {
return null as any;
}),
})
).rejects.toThrow("Could not create cache item");
});

Expand All @@ -61,7 +61,7 @@ describe("GenericCache", () => {
});

it("should return existing item if it exists in the cache when getOrCreate is called", async () => {
sut.set("test", { test: "test" }, 1000);
await sut.set("test", { test: "test" }, 1000);

const result = await sut.getOrCreate("test", async () => {
return { test: "test2" };
Expand All @@ -81,7 +81,7 @@ describe("GenericCache", () => {
},
async (item) => {
return { test: "test2", expires: Date.now() + 10 * (60 * 1000) };
},
}
);

await wait(500);
Expand All @@ -102,7 +102,7 @@ describe("GenericCache", () => {
},
async (item) => {
throw new Error("Should not be called");
},
}
);

await wait(500);
Expand All @@ -123,7 +123,7 @@ describe("GenericCache", () => {
},
async (item) => {
return { test: "test2", expires: Date.now() + 10 * (60 * 1000) };
},
}
);

let result = await sut.get<{ test: string }>("test");
Expand All @@ -142,7 +142,7 @@ describe("GenericCache", () => {
},
async (item) => {
throw new Error("Test error");
},
}
);

let result = await sut.get<{ test: string }>("test");
Expand All @@ -161,7 +161,7 @@ describe("GenericCache", () => {
},
async (item) => {
throw new Error("Test error");
},
}
);

await wait(500);
Expand All @@ -185,7 +185,7 @@ describe("GenericCache", () => {
async (item) => {
renewCalled = true;
throw new Error("Should not be called");
},
}
);

sut.remove("test");
Expand Down
Loading

0 comments on commit b7bff8a

Please sign in to comment.