Fix & test GET /videos

This commit is contained in:
Zoe Roux 2025-05-03 14:37:05 +02:00
parent 45e769828b
commit e26bc931f5
No known key found for this signature in database
8 changed files with 232 additions and 30 deletions

View File

@ -14,6 +14,7 @@ import { KError } from "~/models/error";
import { bubbleVideo } from "~/models/examples"; import { bubbleVideo } from "~/models/examples";
import { import {
Page, Page,
type Resource,
Sort, Sort,
createPage, createPage,
isUuid, isUuid,
@ -54,8 +55,9 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
slug: shows.slug, slug: shows.slug,
}) })
.from(videos) .from(videos)
.crossJoin( .leftJoin(
sql`jsonb_array_elements_text(${videos.guess}->'year') as year`, sql`jsonb_array_elements_text(${videos.guess}->'year') as year`,
sql`true`,
) )
.innerJoin(entryVideoJoin, eq(entryVideoJoin.videoPk, videos.pk)) .innerJoin(entryVideoJoin, eq(entryVideoJoin.videoPk, videos.pk))
.innerJoin(entries, eq(entries.pk, entryVideoJoin.entryPk)) .innerJoin(entries, eq(entries.pk, entryVideoJoin.entryPk))
@ -78,7 +80,10 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
const [{ guesses }] = await db const [{ guesses }] = await db
.with(years, guess) .with(years, guess)
.select({ .select({
guesses: jsonbObjectAgg<Guesses["guesses"]>(guess.guess, guess.years), guesses: jsonbObjectAgg<Record<string, Resource>>(
guess.guess,
guess.years,
),
}) })
.from(guess); .from(guess);
@ -98,7 +103,7 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
return { return {
paths: paths.map((x) => x.path), paths: paths.map((x) => x.path),
guesses, guesses: guesses ?? {},
unmatched: unmatched.map((x) => x.path), unmatched: unmatched.map((x) => x.path),
}; };
}, },
@ -177,8 +182,7 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
path: videos.path, path: videos.path,
}); });
} catch (e) { } catch (e) {
if (!isUniqueConstraint(e)) if (!isUniqueConstraint(e)) throw e;
throw e;
return error(409, { return error(409, {
status: 409, status: 409,
message: comment` message: comment`

View File

@ -144,5 +144,7 @@ export const jsonbBuildObject = <T>(select: JsonFields) => {
}; };
export const isUniqueConstraint = (e: unknown): boolean => { export const isUniqueConstraint = (e: unknown): boolean => {
return typeof e === "object" && e != null && "code" in e && e.code === "23505"; return (
typeof e === "object" && e != null && "code" in e && e.code === "23505"
);
}; };

View File

@ -13,6 +13,7 @@ export const Resource = () =>
id: t.String({ format: "uuid" }), id: t.String({ format: "uuid" }),
slug: t.String({ format: "slug" }), slug: t.String({ format: "slug" }),
}); });
export type Resource = ReturnType<typeof Resource>["static"];
const checker = TypeCompiler.Compile(t.String({ format: "uuid" })); const checker = TypeCompiler.Compile(t.String({ format: "uuid" }));
export const isUuid = (id: string) => checker.Check(id); export const isUuid = (id: string) => checker.Check(id);

View File

@ -167,10 +167,7 @@ export const Guesses = t.Object({
paths: t.Array(t.String()), paths: t.Array(t.String()),
guesses: t.Record( guesses: t.Record(
t.String(), t.String(),
t.Record( t.Record(t.String({ pattern: "^([1-9][0-9]{3})|unknown$" }), Resource()),
t.Union([t.Literal("unknown"), t.String({ pattern: "[1-9][0-9]*" })]),
Resource(),
),
), ),
unmatched: t.Array(t.String()), unmatched: t.Array(t.String()),
}); });
@ -188,7 +185,7 @@ registerExamples(Guesses, {
id: "43b742f5-9ce6-467d-ad29-74460624020a", id: "43b742f5-9ce6-467d-ad29-74460624020a",
slug: "evangelion", slug: "evangelion",
}, },
1995: { "1995": {
id: "43b742f5-9ce6-467d-ad29-74460624020a", id: "43b742f5-9ce6-467d-ad29-74460624020a",
slug: "evangelion", slug: "evangelion",
}, },

View File

@ -17,3 +17,29 @@ export const createVideo = async (video: SeedVideo | SeedVideo[]) => {
const body = await resp.json(); const body = await resp.json();
return [resp, body] as const; return [resp, body] as const;
}; };
export const getVideos = async () => {
const resp = await app.handle(
new Request(buildUrl("videos"), {
method: "GET",
headers: await getJwtHeaders(),
}),
);
const body = await resp.json();
return [resp, body] as const;
};
export const deleteVideo = async (paths: string[]) => {
const resp = await app.handle(
new Request(buildUrl("videos"), {
method: "DELETE",
body: JSON.stringify(paths),
headers: {
"Content-Type": "application/json",
...(await getJwtHeaders()),
},
}),
);
const body = await resp.json();
return [resp, body] as const;
};

View File

@ -1,7 +1,7 @@
import { db, migrate } from "~/db"; import { db, migrate } from "~/db";
import { profiles, shows } from "~/db/schema"; import { profiles, shows } from "~/db/schema";
import { madeInAbyss } from "~/models/examples"; import { bubble, madeInAbyss } from "~/models/examples";
import { createSerie, createVideo } from "./helpers"; import { createMovie, createSerie, createVideo, getVideos } from "./helpers";
// test file used to run manually using `bun tests/manual.ts` // test file used to run manually using `bun tests/manual.ts`
// run those before running this script // run those before running this script
@ -12,22 +12,42 @@ await migrate();
await db.delete(shows); await db.delete(shows);
await db.delete(profiles); await db.delete(profiles);
const [__, ser] = await createSerie(madeInAbyss); const [_, ser] = await createSerie(madeInAbyss);
console.log(ser); const [__, mov] = await createMovie(bubble);
const [_, body] = await createVideo({ const [resp, body] = await createVideo([
guess: { title: "mia", season: [1], episode: [13], from: "test" }, {
part: null, guess: { title: "mia", season: [1], episode: [13], from: "test" },
path: "/video/mia s1e13.mkv", part: null,
rendering: "renderingsha", path: "/video/mia s1e13.mkv",
version: 1, rendering: "sha2",
for: [ version: 1,
{ for: [{ slug: `${madeInAbyss.slug}-s1e13` }],
serie: madeInAbyss.slug, },
season: madeInAbyss.entries[0].seasonNumber!, {
episode: madeInAbyss.entries[0].episodeNumber!, guess: {
title: "mia",
season: [2],
episode: [1],
year: [2017],
from: "test",
}, },
], part: null,
}); path: "/video/mia 2017 s2e1.mkv",
rendering: "sha8",
version: 1,
for: [{ slug: `${madeInAbyss.slug}-s2e1` }],
},
{
guess: { title: "bubble", from: "test" },
part: null,
path: "/video/bubble.mkv",
rendering: "sha5",
version: 1,
for: [{ movie: bubble.slug }],
},
]);
console.log(body); console.log(body);
const [___, ret] = await getVideos();
console.log(JSON.stringify(ret, undefined, 4));
process.exit(0); process.exit(0);

View File

@ -0,0 +1,154 @@
import { beforeAll, describe, expect, it } from "bun:test";
import {
createMovie,
createSerie,
createVideo,
getVideos,
} from "tests/helpers";
import { expectStatus } from "tests/utils";
import { db } from "~/db";
import { shows, videos } from "~/db/schema";
import { bubble, madeInAbyss } from "~/models/examples";
beforeAll(async () => {
await db.delete(shows);
await db.delete(videos);
let [ret, body] = await createSerie(madeInAbyss);
expectStatus(ret, body).toBe(201);
[ret, body] = await createMovie(bubble);
expectStatus(ret, body).toBe(201);
[ret, body] = await createVideo([
{
guess: { title: "mia", season: [1], episode: [13], from: "test" },
part: null,
path: "/video/mia s1e13.mkv",
rendering: "sha2",
version: 1,
for: [{ slug: `${madeInAbyss.slug}-s1e13` }],
},
{
guess: {
title: "mia",
season: [2],
episode: [1],
year: [2017],
from: "test",
},
part: null,
path: "/video/mia 2017 s2e1.mkv",
rendering: "sha8",
version: 1,
for: [{ slug: `${madeInAbyss.slug}-s2e1` }],
},
{
guess: { title: "bubble", from: "test" },
part: null,
path: "/video/bubble.mkv",
rendering: "sha5",
version: 1,
for: [{ movie: bubble.slug }],
},
]);
expectStatus(ret, body).toBe(201);
expect(body).toBeArrayOfSize(3);
expect(body[0].entries).toBeArrayOfSize(1);
expect(body[1].entries).toBeArrayOfSize(1);
expect(body[2].entries).toBeArrayOfSize(1);
});
describe("Video get/deletion", () => {
it("Get current state", async () => {
const [resp, body] = await getVideos();
expectStatus(resp, body).toBe(200);
expect(body.guesses).toMatchObject({
mia: {
unknown: {
id: expect.any(String),
slug: "made-in-abyss",
},
"2017": {
id: expect.any(String),
slug: "made-in-abyss",
},
},
bubble: {
unknown: {
id: expect.any(String),
slug: "bubble",
},
},
});
});
it("With unknown", async () => {
let [resp, body] = await createVideo({
guess: { title: "mia", season: [1], episode: [13], from: "test" },
part: null,
path: "/video/mia s1e13 unknown test.mkv",
rendering: "shanthnth",
version: 1,
});
expectStatus(resp, body).toBe(201);
[resp, body] = await getVideos();
expectStatus(resp, body).toBe(200);
expect(body.guesses).toMatchObject({
mia: {
unknown: {
id: expect.any(String),
slug: "made-in-abyss",
},
"2017": {
id: expect.any(String),
slug: "made-in-abyss",
},
},
bubble: {
unknown: {
id: expect.any(String),
slug: "bubble",
},
},
});
expect(body.unmatched).toBeArrayOfSize(1);
expect(body.unmatched[0]).toBe("/video/mia s1e13 unknown test.mkv");
});
it("Mismatch title guess", async () => {
let [resp, body] = await createVideo({
guess: { title: "mia", season: [1], episode: [13], from: "test" },
part: null,
path: "/video/mia s1e13 mismatch.mkv",
rendering: "mismatch",
version: 1,
for: [{ movie: "bubble" }],
});
expectStatus(resp, body).toBe(201);
[resp, body] = await getVideos();
expectStatus(resp, body).toBe(200);
expect(body.guesses).toMatchObject({
mia: {
unknown: {
id: expect.any(String),
// take the latest slug
slug: "bubble",
},
"2017": {
id: expect.any(String),
slug: "made-in-abyss",
},
},
bubble: {
unknown: {
id: expect.any(String),
slug: "bubble",
},
},
});
});
it.todo("Delete video", async () => {});
});

View File

@ -8,7 +8,6 @@ import { bubble, madeInAbyss } from "~/models/examples";
beforeAll(async () => { beforeAll(async () => {
await db.delete(shows); await db.delete(shows);
await db.delete(entries);
await db.delete(videos); await db.delete(videos);
let [ret, body] = await createSerie(madeInAbyss); let [ret, body] = await createSerie(madeInAbyss);
expectStatus(ret, body).toBe(201); expectStatus(ret, body).toBe(201);
@ -358,7 +357,6 @@ describe("Video seeding", () => {
expect(body.message).toBeString(); expect(body.message).toBeString();
}); });
it("Two for the same entry", async () => { it("Two for the same entry", async () => {
const [resp, body] = await createVideo({ const [resp, body] = await createVideo({
guess: { guess: {