Files
SnapOtter/tests/integration/platform/media-engine-unavailable.test.ts
T

93 lines
3.8 KiB
TypeScript
Raw Normal View History

import { mkdtempSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { ffmpegAvailable } from "@snapotter/media-engine";
import { afterEach, describe, expect, it, vi } from "vitest";
import { fixtureRoot } from "../../fixtures/index.js";
/**
* A container missing ffmpeg is an operator problem, not a bad upload.
*
* media-input caught every probe failure and reported "may be corrupt or in an
* unsupported format", so an operator whose image lacks ffprobe was told their
* perfectly good MP4 was broken, with nothing pointing at the real cause. The
* CI integration shards ship without ffmpeg by design, which is where this
* surfaced: every media tool failed there for a reason no caller could see.
*
* These run without the ffmpegAvailable() gate the sibling media specs use,
* because the whole point is the behaviour when the binary is gone.
*/
const MP4 = readFileSync(join(fixtureRoot, "video", "formats", "tiny.mp4"));
function scratch(): { scratchDir: string } {
return { scratchDir: mkdtempSync(join(tmpdir(), "media-unavailable-")) };
}
const originalFfprobe = process.env.FFPROBE_PATH;
afterEach(() => {
if (originalFfprobe === undefined) delete process.env.FFPROBE_PATH;
else process.env.FFPROBE_PATH = originalFfprobe;
vi.resetModules();
});
/**
* media-engine resolves the binary once and caches it in a module variable, so
* changing FFPROBE_PATH after any earlier call has no effect. Re-import through
* a fresh module graph so each case actually gets the environment it sets.
*/
async function freshHandler(kind: "video") {
vi.resetModules();
const { MediaInputHandler } = await import("../../../apps/api/src/modality/media-input.js");
return new MediaInputHandler(kind);
}
describe("media validation when the engine is unavailable", () => {
it("reports the missing engine instead of blaming the upload", async () => {
process.env.FFPROBE_PATH = join(tmpdir(), "definitely-not-ffprobe");
const handler = await freshHandler("video");
const error = await handler.prepare(MP4, "clip.mp4", scratch()).then(
() => null,
(caught: unknown) => caught,
);
// Shape, not instanceof: vi.resetModules() gives the handler a different
// copy of the error class than a static import here would hold.
const failure = error as { name?: string; message: string; statusCode?: number };
expect(failure.name, "a missing engine must still reject the request").toBe(
"InputValidationError",
);
// The upload is fine. Saying otherwise sends the operator looking at their
// file instead of their container.
expect(failure.message).not.toMatch(/corrupt/i);
expect(failure.message).toMatch(/ffprobe|ffmpeg|unavailable|not installed/i);
// 400 tells the caller to fix their request, which cannot help here. This
// is the server missing a dependency, so it belongs in the 5xx range.
expect(failure.statusCode).toBeGreaterThanOrEqual(500);
});
// Needs a working ffprobe to reach the parse failure at all. CI shards ship
// without ffmpeg, where every media input is an engine gap instead.
it.skipIf(!ffmpegAvailable())(
"still rejects a genuinely corrupt upload as a client error",
async () => {
delete process.env.FFPROBE_PATH;
const handler = await freshHandler("video");
const notMedia = Buffer.from("this is not a video file, it is prose");
const error = await handler.prepare(notMedia, "clip.mp4", scratch()).then(
() => null,
(caught: unknown) => caught,
);
const failure = error as { name?: string; message: string; statusCode?: number };
expect(failure.name).toBe("InputValidationError");
expect(failure.statusCode).toBe(400);
expect(failure.message).toMatch(/corrupt|unsupported/i);
},
);
});