From 39dcfb441856655da2d57511b3022d26cc346df5 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 29 Apr 2025 00:49:33 +0200 Subject: [PATCH] Test `POST /videos` --- api/src/controllers/videos.ts | 14 +++---- api/src/db/utils.ts | 5 ++- api/src/models/video.ts | 63 +++++++++++++++++--------------- api/tests/videos/scanner.test.ts | 50 +++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 38 deletions(-) create mode 100644 api/tests/videos/scanner.test.ts diff --git a/api/src/controllers/videos.ts b/api/src/controllers/videos.ts index 4c331d0a..81390336 100644 --- a/api/src/controllers/videos.ts +++ b/api/src/controllers/videos.ts @@ -7,11 +7,10 @@ import { conflictUpdateAllExcept, jsonbBuildObject, jsonbObjectAgg, - sqlarr, values, } from "~/db/utils"; import { bubbleVideo } from "~/models/examples"; -import { Page, isUuid } from "~/models/utils"; +import { isUuid } from "~/models/utils"; import { Guesses, SeedVideo, Video } from "~/models/video"; import { comment } from "~/utils"; import { computeVideoSlug } from "./seed/insert/entries"; @@ -138,10 +137,11 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] }) }) .from( values( - body.flatMap((x) => - x.for.map((e) => ({ + body.flatMap((x) => { + if (!x.for) return []; + return x.for.map((e) => ({ path: x.path, - needRendering: x.for.length > 1, + needRendering: x.for!.length > 1, entry: { ...e, movie: @@ -157,8 +157,8 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] }) : { slug: e.serie } : undefined, }, - })), - ), + })); + }), ).as("j"), ) .innerJoin(vidsI, eq(vidsI.path, sql`j.path`)) diff --git a/api/src/db/utils.ts b/api/src/db/utils.ts index 6c2fcccc..5681eac9 100644 --- a/api/src/db/utils.ts +++ b/api/src/db/utils.ts @@ -103,7 +103,10 @@ export const nullif = (val: SQL | Column, eq: SQL) => { return sql`nullif(${val}, ${eq})`; }; -export const jsonbObjectAgg = (key: SQLWrapper, value: SQL | SQLWrapper) => { +export const jsonbObjectAgg = ( + key: SQLWrapper, + value: SQL | SQLWrapper, +) => { return sql< Record >`jsonb_object_agg(${sql.join([key, value], sql.raw(","))})`; diff --git a/api/src/models/video.ts b/api/src/models/video.ts index 01a02e30..226f064d 100644 --- a/api/src/models/video.ts +++ b/api/src/models/video.ts @@ -68,44 +68,47 @@ export const SeedVideo = t.Object({ guess: Guess, - for: t.Array( - t.Union([ - t.Object({ - slug: t.String({ - format: "slug", - examples: ["made-in-abyss-dawn-of-the-deep-soul"], - }), - }), - t.Object({ - externalId: t.Optional(t.Union([EpisodeId, ExternalId()])), - }), - t.Object({ - movie: t.Union([ - t.String({ format: "uuid" }), - t.String({ format: "slug", examples: ["bubble"] }), - ]), - }), - t.Intersect([ + for: t.Optional( + t.Array( + t.Union([ t.Object({ - serie: t.Union([ + slug: t.String({ + format: "slug", + examples: ["made-in-abyss-dawn-of-the-deep-soul"], + }), + }), + t.Object({ + externalId: t.Optional(t.Union([EpisodeId, ExternalId()])), + }), + t.Object({ + movie: t.Union([ t.String({ format: "uuid" }), - t.String({ format: "slug", examples: ["made-in-abyss"] }), + t.String({ format: "slug", examples: ["bubble"] }), ]), }), - t.Union([ + t.Intersect([ t.Object({ - season: t.Integer({ minimum: 1 }), - episode: t.Integer(), - }), - t.Object({ - order: t.Number(), - }), - t.Object({ - special: t.Integer(), + serie: t.Union([ + t.String({ format: "uuid" }), + t.String({ format: "slug", examples: ["made-in-abyss"] }), + ]), }), + t.Union([ + t.Object({ + season: t.Integer({ minimum: 1 }), + episode: t.Integer(), + }), + t.Object({ + order: t.Number(), + }), + t.Object({ + special: t.Integer(), + }), + ]), ]), ]), - ]), + { default: [] }, + ), ), }); export type SeedVideo = Prettify; diff --git a/api/tests/videos/scanner.test.ts b/api/tests/videos/scanner.test.ts new file mode 100644 index 00000000..92badcd0 --- /dev/null +++ b/api/tests/videos/scanner.test.ts @@ -0,0 +1,50 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { eq } from "drizzle-orm"; +import { createVideo } from "tests/helpers"; +import { expectStatus } from "tests/utils"; +import { db } from "~/db"; +import { entries, shows, videos } from "~/db/schema"; + +beforeAll(async () => { + await db.delete(shows); + await db.delete(entries); + await db.delete(videos); +}); + +describe("Video seeding", () => { + it("Can create a video without entry", async () => { + const [resp, body] = await createVideo({ + guess: { title: "mia", from: "test" }, + part: null, + path: "/video/mia s1e13.mkv", + rendering: "sha", + version: 1, + }); + + expectStatus(resp, body).toBe(201); + expect(body).toBeArrayOfSize(1); + expect(body[0].id).toBeString(); + + const vid = await db.query.videos.findFirst({ + where: eq(videos.id, body[0].id), + with: { + evj: { with: { entry: true } }, + }, + }); + + expect(vid).not.toBeNil(); + expect(vid!.path).toBe("/video/mia s1e13.mkv"); + expect(vid!.guess).toBe({ title: "mia", from: "test" }); + + expect(body[0].slug).toBe("mia"); + // videos created without entries should create an /unknown entry. + expect(vid!.evj).toBeArrayOfSize(1); + expect(vid!.evj[0].slug).toBe("mia"); + expect(vid!.evj[0].entry).toMatchObject({ + kind: "unknown", + name: "mia", + // should we store the video path in the unknown entry? + // in db it would be the `description` field + }); + }); +});