Skip to content

Commit

Permalink
feat(anime): add endpoints for anime information and summary (#122)
Browse files Browse the repository at this point in the history
Introduces two new endpoints to access anime information:
1. /api/v1/anime/:id
2. /api/v1/anime/:id/summary
  • Loading branch information
rocktimsaikia authored Aug 29, 2024
1 parent b0320ba commit 3da93ed
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 34 deletions.
84 changes: 84 additions & 0 deletions server/src/controllers/anime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { Request, Response } from "express";
import { prisma } from "~/libs/prisma";

export const getAnimeInformation = async (req: Request, res: Response) => {
const animeId = req.params?.id;

if (!animeId) {
return res.status(400).json({
status: "error",
error: {
code: 400,
message: "Not a valid 'animeId'",
},
});
}

const anime = await prisma.anime.findUnique({
where: { id: Number.parseInt(animeId) },
include: { animeCharacters: true },
});

if (!anime) {
return res.status(404).json({
status: "error",
error: {
code: 404,
message: "No matching anime found",
},
});
}

const characters = anime.animeCharacters.map((c) => ({
id: c.id,
name: c.name,
}));
return res.json({
status: "success",
data: {
id: anime.id,
name: anime.name,
summary: anime.synopsis,
episodesCount: anime.episodeCount,
characters: characters,
},
});
};

export const getAnimeSummary = async (req: Request, res: Response) => {
const animeId = req.params?.id;

if (!animeId) {
return res.status(400).json({
status: "error",
error: {
code: 400,
message: "Not a valid 'animeId'",
},
});
}

const anime = await prisma.anime.findUnique({
where: { id: Number.parseInt(animeId) },
include: { animeCharacters: true },
});

if (!anime) {
return res.status(404).json({
status: "error",
error: {
code: 404,
message: "No matching anime found",
},
});
}

return res.json({
status: "success",
data: {
id: anime.id,
name: anime.name,
summary: anime.synopsis,
},
});
};
11 changes: 9 additions & 2 deletions server/src/libs/auth.util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const PROTECTED_ENDPOINTS_URLS = [
const PROTECTED_ENDPOINTS_QUERY_URLS = [
{
pathname: "/api/v1/quotes",
params: ["anime", "character", "page"],
Expand All @@ -9,10 +9,17 @@ const PROTECTED_ENDPOINTS_URLS = [
},
];

const PROTECTED_BASIC_ENDPOINTS = ["/api/v1/anime"];

export const isProtectedEndpoint = (url: string) => {
const { pathname, searchParams } = new URL(url, "https://animechan.io");

for (const endpoint of PROTECTED_ENDPOINTS_URLS) {
const updatedPath = pathname.split("/").slice(0, 4).join("/");
if (PROTECTED_BASIC_ENDPOINTS.includes(updatedPath)) {
return true;
}

for (const endpoint of PROTECTED_ENDPOINTS_QUERY_URLS) {
if (pathname.includes(endpoint.pathname)) {
for (const param of endpoint.params) {
if (searchParams.has(param)) {
Expand Down
5 changes: 5 additions & 0 deletions server/src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import express from "express";
import { getOneRandomQuote, getQuotes } from "~/controllers/quotes";
import packageJson from "~/../package.json";
import { getAnimeInformation } from "~/controllers/anime";

const router = express.Router();

Expand All @@ -16,4 +17,8 @@ router.get("/health", (_req, res) => {
router.get("/quotes/random", getOneRandomQuote);
router.get("/quotes", getQuotes);

// Anime routes
router.get("/anime/:id", getAnimeInformation);
router.get("/anime/:id/summary", getAnimeInformation);

export default router;
74 changes: 42 additions & 32 deletions server/test/auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,51 @@
import { expect, test } from "vitest";
import { describe, expect, test } from "vitest";
import { isProtectedEndpoint } from "../src/libs/auth.util";

const BASE_PATH = "/api/v1/";

test("Should pass on protected API urls", () => {
expect(isProtectedEndpoint(`${BASE_PATH}quotes/random?anime=naruto`)).toBeTruthy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes/random?character=naruto`)).toBeTruthy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes/random/?anime=naruto`)).toBeTruthy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes/random/?character=naruto`)).toBeTruthy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes?anime=naruto`)).toBeTruthy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes?character=naruto`)).toBeTruthy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes/?anime=naruto`)).toBeTruthy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes/?character=naruto`)).toBeTruthy();
});
describe("Query param based endpoints", () => {
test("Should pass on protected API urls", () => {
expect(isProtectedEndpoint(`${BASE_PATH}quotes/random?anime=naruto`)).toBeTruthy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes/random?character=naruto`)).toBeTruthy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes/random/?anime=naruto`)).toBeTruthy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes/random/?character=naruto`)).toBeTruthy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes?anime=naruto`)).toBeTruthy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes?character=naruto`)).toBeTruthy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes/?anime=naruto`)).toBeTruthy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes/?character=naruto`)).toBeTruthy();
});

test("should fail on unprotected/free API urls", () => {
expect(isProtectedEndpoint(`${BASE_PATH}quotes`)).toBeFalsy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes/`)).toBeFalsy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes/random`)).toBeFalsy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes/random/`)).toBeFalsy();
});
test("should fail on unprotected based API urls", () => {
expect(isProtectedEndpoint(`${BASE_PATH}quotes`)).toBeFalsy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes/`)).toBeFalsy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes/random`)).toBeFalsy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes/random/`)).toBeFalsy();
});

test("Should pass on valid protected API urls with additional params", () => {
expect(
isProtectedEndpoint(`${BASE_PATH}quotes/random?anime=naruto&character=naruto`),
).toBeTruthy();
expect(
isProtectedEndpoint(`${BASE_PATH}quotes/random/?character=naruto&anime=naruto`),
).toBeTruthy();
expect(
isProtectedEndpoint(`${BASE_PATH}quotes/random/?character=naruto&anime=naruto&page=2`),
).toBeTruthy();
});

test("Should pass on valid protected API urls with additional params", () => {
expect(
isProtectedEndpoint(`${BASE_PATH}quotes/random?anime=naruto&character=naruto`),
).toBeTruthy();
expect(
isProtectedEndpoint(`${BASE_PATH}quotes/random/?character=naruto&anime=naruto`),
).toBeTruthy();
expect(
isProtectedEndpoint(`${BASE_PATH}quotes/random/?character=naruto&anime=naruto&page=2`),
).toBeTruthy();
test("Should pass on protected API urls with additional unknown params", () => {
// Accepts additional not pre-defined params like `foo` here but only in the presence
// of a premium query param which is `character` in this case below.
expect(isProtectedEndpoint(`${BASE_PATH}quotes/random/?character=naruto&foo=bar`)).toBeTruthy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes/random/?foo=bar`)).toBeFalsy();
});
});

test("Should pass on protected API urls with additional unknown params", () => {
// Accepts additional not pre-defined params like `foo` here but only in the presence
// of a premium query param which is `character` in this case below.
expect(isProtectedEndpoint(`${BASE_PATH}quotes/random/?character=naruto&foo=bar`)).toBeTruthy();
expect(isProtectedEndpoint(`${BASE_PATH}quotes/random/?foo=bar`)).toBeFalsy();
describe("Standard path based endpoints", () => {
test("Should pass on protected API urls", () => {
expect(isProtectedEndpoint(`${BASE_PATH}anime/1`));
expect(isProtectedEndpoint(`${BASE_PATH}anime/1/`));
expect(isProtectedEndpoint(`${BASE_PATH}anime/foo`));
});
});

0 comments on commit 3da93ed

Please sign in to comment.