Skip to content

Commit

Permalink
test: exported entities and spotify client
Browse files Browse the repository at this point in the history
  • Loading branch information
MellKam committed Feb 16, 2024
1 parent a5fbb79 commit 2a8fefc
Show file tree
Hide file tree
Showing 16 changed files with 331 additions and 85 deletions.
46 changes: 46 additions & 0 deletions client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { SpotifyClient, SpotifyError } from "./client.ts";
import { sandbox } from "mock_fetch";
import { assert, assertEquals, assertInstanceOf } from "std/assert/mod.ts";

Deno.test("SpotifyClient: basic", async () => {
const { mock, fetch } = sandbox();

mock("GET@/v1/me", (req) => {
assert(req.url === "https://api.spotify.com/v1/me");
assert(req.headers.get("Accept") === "application/json");
assert(req.headers.get("Authorization") === "Bearer TOKEN");
return Response.json({ id: "123" });
});

const client = new SpotifyClient("TOKEN", { fetch });
const res = await client.fetch("/v1/me");
assert(res.status === 200);
assertEquals(await res.json(), { id: "123" });
});

Deno.test("SpotifyClient: error handling", async () => {
const { mock, fetch } = sandbox();
const playlistId = crypto.randomUUID();

mock("PUT@/v1/playlists/:playlistId/followers", () => {
return Response.json(
{ error: { status: 400, message: "Invalid request" } },
{ status: 400, statusText: "Bad Request" }
);
});

const client = new SpotifyClient("TOKEN", { fetch });

try {
await client.fetch(`/v1/playlists/${playlistId}/followers`, {
method: "PUT",
});
assert(false, "should throw an error");
} catch (error) {
assertInstanceOf(error, SpotifyError);
assert(error.message === "400 Bad Request : Invalid request");
assertEquals(error.body, {
error: { status: 400, message: "Invalid request" },
});
}
});
50 changes: 30 additions & 20 deletions client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,13 @@ export type RegularErrorObject = {
export class SpotifyError extends Error {
name = "SpotifyError";

public readonly response: Response;
public readonly body: RegularErrorObject | string | null;

constructor(
message: string,
response: Response,
body: RegularErrorObject | string | null,
public readonly response: Response,
public readonly body: RegularErrorObject | string | null,
options?: ErrorOptions
) {
super(message, options);
this.response = response;
this.body = body;
}

get url() {
Expand All @@ -52,10 +47,15 @@ const createSpotifyError = async (
response: Response,
options?: ErrorOptions
) => {
const urlWithoutQuery = response.url.split("?")[0];
let message = response.statusText
? `${response.status} ${response.statusText} (${urlWithoutQuery})`
: `${response.status} (${urlWithoutQuery})`;
? `${response.status} ${response.statusText}`
: response.status.toString();

const urlWithoutQuery = response.url.split("?")[0];
if (urlWithoutQuery) {
message += ` (${urlWithoutQuery})`;
}

let body: RegularErrorObject | string | null = null;

if (response.body && response.type !== "opaque") {
Expand All @@ -72,11 +72,11 @@ const createSpotifyError = async (
} catch (_) {
/* Ignore errors */
}
}

const bodyMessage = getBodyMessage(body);
if (bodyMessage) {
message += " : " + bodyMessage;
const bodyMessage = getBodyMessage(body);
if (bodyMessage) {
message += " : " + bodyMessage;
}
}

return new SpotifyError(message, response, body, options);
Expand All @@ -94,10 +94,12 @@ type FetchLike = (
export type Middleware = (next: FetchLike) => FetchLike;

/**
* Interface that provides a fetch method to make HTTP requests to Spotify API.
* Interface for making HTTP requests to the Spotify API.
* All Soundify endpoint functions expect the client to implement this interface.
* You can create a custom client by implementing this interface.
*/
export interface HTTPClient {
fetch(path: string, options?: FetchLikeOptions): Promise<Response>;
fetch: (path: string, options?: FetchLikeOptions) => Promise<Response>;
}

const isPlainObject = (obj: unknown): obj is Record<PropertyKey, unknown> => {
Expand All @@ -109,7 +111,13 @@ const isPlainObject = (obj: unknown): obj is Record<PropertyKey, unknown> => {
};

export type SpotifyClinetOptions = {
/**
* Use this option to provide a custom fetch function.
*/
fetch?: (input: URL, init?: RequestInit) => Promise<Response>;
/**
* @default "https://api.spotify.com/"
*/
baseUrl?: string;
/**
* @returns new access token
Expand Down Expand Up @@ -159,13 +167,15 @@ export class SpotifyClient implements HTTPClient {

let isRefreshed = false;

const wrappedFetch = (this.options.middlewares || []).reduceRight(
(next, mw) => mw(next),
(this.options.fetch || globalThis.fetch) as FetchLike
);

const recursiveFetch = async (): Promise<Response> => {
headers.set("Authorization", "Bearer " + this.accessToken);

const res = await (this.options.middlewares || []).reduceRight(
(next, mw) => mw(next),
(this.options.fetch || globalThis.fetch) as FetchLike
)(url, { ...opts, body, headers });
const res = await wrappedFetch(url, { ...opts, body, headers });

if (res.ok) return res;

Expand Down
3 changes: 2 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"oauth4webapi": "https://deno.land/x/[email protected]/mod.ts",
"oak": "https://deno.land/x/[email protected]/mod.ts",
"std/": "https://deno.land/[email protected]/",
"@soundify/web-api": "./src/mod.ts"
"@soundify/web-api": "./mod.ts",
"mock_fetch": "https://deno.land/x/[email protected]/mod.ts"
},
"fmt": { "useTabs": true }
}
39 changes: 38 additions & 1 deletion deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion endpoints/album/album.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
getNewAlbumReleases,
getSavedAlbums,
} from "./album.endpoints.ts";
import {
import type {
Album,
AlbumGroup,
AlbumType,
Expand Down
14 changes: 0 additions & 14 deletions endpoints/general.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { Episode } from "./episode/episode.types.ts";
import { Track } from "./track/track.types.ts";

export type PagingObject<TItem> = {
/**
* A link to the Web API endpoint returning the full result of the request.
Expand Down Expand Up @@ -183,14 +180,3 @@ export type Copyright = {
*/
type: "C" | "P";
};

export type ItemType =
| "artist"
| "album"
| "playlist"
| "track"
| "show"
| "episode"
| "audiobook";

export type TrackItem = Track | Episode;
9 changes: 5 additions & 4 deletions endpoints/player/player.types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ExternalUrls, TrackItem } from "../general.types.ts";
import type { ExternalUrls } from "../general.types.ts";
import type { Track } from "../track/track.types.ts";
import type { Episode } from "../episode/episode.types.ts";

export type Device = {
/**
Expand Down Expand Up @@ -88,7 +89,7 @@ export type PlaybackState = {
progress_ms: number | null;
/** If something is currently playing, return true. */
is_playing: boolean;
item: TrackItem;
item: Track | Episode;
/** The object type of the currently playing item. */
currently_playing_type: "track" | "episode" | "ad" | "unknown";
/**
Expand All @@ -101,9 +102,9 @@ export type PlaybackState = {

export type Queue = {
/** The currently playing track or episode. */
currently_playing: TrackItem | null;
currently_playing: Track | Episode | null;
/** The tracks or episodes in the queue. Can be empty. */
queue: TrackItem[];
queue: (Track | Episode)[];
};

export type PlayHistoryObject = {
Expand Down
Loading

0 comments on commit 2a8fefc

Please sign in to comment.