ソースを参照

feat: wrote unit tests (#14)

Sv443 4 ヶ月 前
コミット
15d3cffce4
7 ファイル変更205 行追加53 行削除
  1. 33 0
      test/album.spec.ts
  2. 12 0
      test/constants.ts
  3. 46 0
      test/hooks.ts
  4. 0 53
      test/latency-test.ts
  5. 15 0
      test/misc.spec.ts
  6. 53 0
      test/search.spec.ts
  7. 46 0
      test/translations.spec.ts

+ 33 - 0
test/album.spec.ts

@@ -0,0 +1,33 @@
+import { baseUrl, defaultFetchOpts } from "./constants";
+import { checkAlbumProps, checkArtistProps } from "./hooks";
+
+describe(`GET /album/:id`, () => {
+  //#region /album/:id
+
+  it("Album details yields correct props", async () => {
+    const res = await fetch(`${baseUrl}/album/7105950`, defaultFetchOpts);
+    const body = await res.json();
+
+    expect(res.status).toBe(200);
+
+    expect(body?.error).toEqual(false);
+    expect(body?.matches).toEqual(1);
+
+    checkAlbumProps(body?.album);
+
+    checkArtistProps(body?.album?.artist);
+  });
+
+  //#region inv /album/:id
+
+  it("Invalid album yields error", async () => {
+    const res = await fetch(`${baseUrl}/album/0`, defaultFetchOpts);
+    const body = await res.json();
+
+    expect(res.status).toBe(400);
+
+    expect(body?.error).toEqual(true);
+    expect(body?.matches).toEqual(0);
+    expect(body?.message).toBeDefined();
+  });
+});

+ 12 - 0
test/constants.ts

@@ -0,0 +1,12 @@
+import "dotenv/config";
+
+export const baseUrl = `http://127.0.0.1:${process.env.HTTP_PORT}`;
+
+const authToken = process.env.AUTH_TOKENS?.split(",")[0];
+
+export const defaultFetchOpts: Partial<RequestInit> = {
+  method: "GET",
+  headers: {
+    ...(authToken ? { "Authentication": `Bearer ${authToken}` } : {}),
+  },
+};

+ 46 - 0
test/hooks.ts

@@ -0,0 +1,46 @@
+export function checkValProps(val: unknown, props: string[]) {
+  for(const prop of props)
+    expect(val).toHaveProperty(prop);
+}
+
+export function checkSongProps(songObj: unknown) {
+  return checkValProps(songObj, [
+    "url", 
+    "path", 
+    "lyricsState", 
+    "id", 
+    "meta.title", 
+    "meta.fullTitle", 
+    "meta.artists",
+  ]);
+}
+
+export function checkAlbumProps(albumObj: unknown) {
+  return checkValProps(albumObj, [
+    "name",
+    "fullTitle",
+    "url",
+    "coverArt",
+    "id",
+    "artist",
+  ]);
+}
+
+export function checkArtistProps(artistObj: unknown) {
+  return checkValProps(artistObj, [
+    "name",
+    "url",
+    "image",
+    "headerImage",
+  ]);
+}
+
+export function checkTranslationProps(translationObj: unknown) {
+  return checkValProps(translationObj, [
+    "language",
+    "id",
+    "path",
+    "title",
+    "url",
+  ]);
+}

+ 0 - 53
test/latency-test.ts

@@ -1,53 +0,0 @@
-// NOTE:
-// requires the env vars HTTP_PORT and AUTH_TOKENS to be set
-
-import "dotenv/config";
-import _axios from "axios";
-import percentile from "percentile";
-
-const settings = {
-  amount: 100,
-  url: `http://127.0.0.1:${process.env.HTTP_PORT}/search/top?q=pink guy - dog festival directions`,
-};
-
-
-const axios = _axios.create({ timeout: 20_000 });
-
-async function run() {
-  console.log(`\n\n>>> Running latency test with ${settings.amount} requests...\n`);
-  const startTs = Date.now();
-
-  const times = [];
-  for(let i = 0; i < settings.amount; i++) {
-    const start = Date.now();
-    await axios.get(settings.url, {
-      headers: {
-        "Cache-Control": "no-cache",
-        Authorization: `Bearer ${process.env.AUTH_TOKENS!.split(",")[0]}`,
-      },
-    });
-    times.push(Date.now() - start);
-
-    i % 10 === 0 && i !== 0 && console.log(`Sent ${i} of ${settings.amount} requests`);
-  }
-
-  const avg = (times.reduce((a, c) => a + c, 0) / times.length).toFixed(0);
-  const max = times.reduce((a, c) => Math.max(a, c), 0).toFixed(0);
-  const perc80 = percentile(80, times);
-  const perc90 = percentile(90, times);
-  const perc95 = percentile(95, times);
-  const perc99 = percentile(99, times);
-
-  console.log(`\n>>> Latency test finished after ${((Date.now() - startTs) / 1000).toFixed(2)}s`);
-  console.log();
-  console.log(`avg:\t${avg}\tms`);
-  console.log(`max:\t${max}\tms`);
-  console.log();
-  console.log(`80th%:\t${perc80}\tms`);
-  console.log(`90th%:\t${perc90}\tms`);
-  console.log(`95th%:\t${perc95}\tms`);
-  console.log(`99th%:\t${perc99}\tms`);
-  console.log();
-}
-
-run();

+ 15 - 0
test/misc.spec.ts

@@ -0,0 +1,15 @@
+import { baseUrl, defaultFetchOpts } from "./constants";
+
+describe("Misc", () => {
+  //#region health check
+
+  it("Health check", async () => {
+    const res = await fetch(baseUrl, {
+      ...defaultFetchOpts,
+      method: "HEAD",
+    });
+
+    expect(res.status).toBe(200);
+    expect(res.headers.get("api-info")).toBeDefined();
+  });
+});

+ 53 - 0
test/search.spec.ts

@@ -0,0 +1,53 @@
+import { randomBytes } from "crypto";
+import { baseUrl, defaultFetchOpts } from "./constants";
+import { checkSongProps } from "./hooks";
+
+describe(`GET /search/top`, () => {
+  //#region /search/top
+
+  it("Top search yields expected props", async () => {
+    const res = await fetch(`${baseUrl}/search/top?q=Lil Nas X - LIGHT AGAIN!`, defaultFetchOpts);
+    const body = await res.json();
+
+    expect(res.status).toBe(200);
+
+    expect(body?.error).toEqual(false);
+    expect(body?.matches).toEqual(1);
+
+    checkSongProps(body);
+  });
+
+  //#region /search
+
+  it("Regular search yields <=10 results", async () => {
+    const res = await fetch(`${baseUrl}/search?q=Lil Nas X`, defaultFetchOpts);
+    const body = await res.json();
+
+    expect(res.status).toBe(200);
+
+    expect(body?.error).toEqual(false);
+    expect(body?.matches).toBeLessThanOrEqual(10);
+
+    checkSongProps(body?.top);
+
+    expect(Array.isArray(body?.all)).toBe(true);
+
+    body?.all?.forEach((hit: any) => checkSongProps(hit));
+
+    expect(body?.all?.length).toEqual(body?.matches ?? -1);
+  });
+
+  //#region inv /search
+
+  it("Invalid search yields error", async () => {
+    const randText = randomBytes(32).toString("hex");
+    const res = await fetch(`${baseUrl}/search?q=${randText}`, defaultFetchOpts);
+    const body = await res.json();
+
+    expect(res.status).toBe(400);
+
+    expect(body?.error).toEqual(true);
+    expect(body?.matches).toEqual(0);
+    expect(body?.message).toBeDefined();
+  });
+});

+ 46 - 0
test/translations.spec.ts

@@ -0,0 +1,46 @@
+import { baseUrl, defaultFetchOpts } from "./constants";
+import { checkTranslationProps } from "./hooks";
+
+describe(`GET /translations/:id`, () => {
+  //#region /translations/:id
+
+  it("Translation yields correct props", async () => {
+    const res = await fetch(`${baseUrl}/translations/7105950`, defaultFetchOpts);
+    const body = await res.json();
+
+    expect(res.status).toBe(200);
+
+    expect(body?.error).toEqual(false);
+    expect(body?.matches).toBeGreaterThan(0);
+
+    expect(Array.isArray(body?.translations)).toBe(true);
+
+    body?.translations?.forEach((tr: unknown) => checkTranslationProps(tr));
+  });
+
+  //#region inv /translations/:id
+
+  it("Invalid album yields error", async () => {
+    const res = await fetch(`${baseUrl}/translations/0`, defaultFetchOpts);
+    const body = await res.json();
+
+    expect(res.status).toBe(400);
+
+    expect(body?.error).toEqual(true);
+    expect(body?.matches).toEqual(null);
+    expect(body?.message).toBeDefined();
+  });
+
+  //#region inv /translations
+
+  it("Translations path without ID yields error", async () => {
+    const res = await fetch(`${baseUrl}/translations`, defaultFetchOpts);
+    const body = await res.json();
+
+    expect(res.status).toBe(400);
+
+    expect(body?.error).toEqual(true);
+    expect(body?.matches).toEqual(null);
+    expect(body?.message).toBeDefined();
+  });
+});