Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions apps/web/src/app/api/video/pack/__tests__/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ afterEach(() => {
extractVideoPackSpec.mockReset();
vi.resetModules();
vi.unstubAllGlobals();
vi.unstubAllEnvs();
});

function postRequest(body: unknown) {
Expand Down Expand Up @@ -119,6 +120,24 @@ describe('POST /api/video/pack', () => {
finish?.(specFor(CANON_B));
});

it('does not return 202 in production when Redis durability is unavailable', async () => {
vi.stubEnv('NODE_ENV', 'production');
vi.stubEnv('UPSTASH_REDIS_REST_URL', '');
vi.stubEnv('UPSTASH_REDIS_REST_TOKEN', '');
vi.stubEnv('KV_REST_API_URL', '');
vi.stubEnv('KV_REST_API_TOKEN', '');

const { POST, scheduled } = await loadPackRoute();
const res = await POST(postRequest({ url: 'https://www.youtube.com/watch?v=jNQXAC9IVRw' }));

expect(res.status).toBe(503);
const body = (await res.json()) as { status?: string; error?: string };
expect(body.status).toBe('error');
expect(body.error).toMatch(/durable video pack storage is not configured/i);
expect(scheduled).toHaveLength(0);
expect(extractVideoPackSpec).not.toHaveBeenCalled();
});

it('fails closed with a visible error when Gateway extract is unavailable', async () => {
extractVideoPackSpec.mockRejectedValue(
new VideoPackExtractError(
Expand Down
15 changes: 14 additions & 1 deletion apps/web/src/lib/__tests__/video-pack-store.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { execFileSync, spawn, type ChildProcess } from 'node:child_process';
import net from 'node:net';
import { createClient, type RedisClientType } from 'redis';
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
import { GOLDEN_IDENTITY_HASHES, applyExtractedSpec, buildIdentityPack } from '@/lib/video-pack';
import {
PROCESSING_STALE_MS,
Expand Down Expand Up @@ -117,9 +117,22 @@ function createRedis(initial: unknown = null) {

afterEach(() => {
resetVideoPackStoreForTests();
vi.unstubAllEnvs();
});

describe('video-pack store', () => {
it('fails closed in production when Redis durability is unavailable', async () => {
vi.stubEnv('NODE_ENV', 'production');
vi.stubEnv('UPSTASH_REDIS_REST_URL', '');
vi.stubEnv('UPSTASH_REDIS_REST_TOKEN', '');
vi.stubEnv('KV_REST_API_URL', '');
vi.stubEnv('KV_REST_API_TOKEN', '');

await expect(claimPackProcessing(IDENTITY)).rejects.toThrow(
/durable video pack storage is not configured/i,
);
});

it('returns null for an unknown source_hash', async () => {
expect(await getPackRecord(HASH)).toBeNull();
});
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/lib/video-pack-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ const memoryStore = new Map<string, VideoPackRecord>();

let redisPromise: Promise<VideoPackRedisClient | null> | null = null;

function assertDurableVideoPackStorageConfigured(): void {
if (process.env.NODE_ENV !== 'production') return;
if (resolveUpstashRedisCredentials()) return;
throw new Error(
'Durable video pack storage is not configured in production. Set UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN (or KV_REST_API_URL + KV_REST_API_TOKEN).',
);
}

export function packStoreKey(sourceHash: string): string {
return `${VIDEO_PACK_STORE_PREFIX}${sourceHash}`;
}
Expand Down Expand Up @@ -209,6 +217,7 @@ export async function claimPackProcessing(
identity: PackProcessingIdentity,
now: Date = new Date(),
): Promise<'claimed' | VideoPackRecord> {
assertDurableVideoPackStorageConfigured();
const processing: Extract<VideoPackRecord, { state: 'processing' }> = {
state: 'processing',
video_id: identity.video_id,
Expand All @@ -220,6 +229,9 @@ export async function claimPackProcessing(

const key = packStoreKey(identity.source_hash);
const redis = await getRedis();
if (process.env.NODE_ENV === 'production' && !redis) {
throw new Error('Durable video pack storage is unavailable in production.');
}
if (redis) {
try {
const result = await redis.eval<unknown>(
Expand Down
18 changes: 12 additions & 6 deletions apps/web/src/lib/video-pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,12 +356,18 @@ export async function handleIdentityPackPost(request: Request): Promise<Response
// A new POST retries after a visible failure; GET keeps serving the error.
}

const claimed = await claimPackProcessing({
video_id: identity.video_id,
source_url: identity.source_url,
source_hash: sourceHash,
id: identity.id,
});
let claimed: Awaited<ReturnType<typeof claimPackProcessing>>;
try {
claimed = await claimPackProcessing({
video_id: identity.video_id,
source_url: identity.source_url,
source_hash: sourceHash,
id: identity.id,
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Video pack claim failed.';
return NextResponse.json({ status: 'error', error: message }, { status: 503 });
}
if (claimed !== 'claimed') {
return recordToResponse(claimed);
}
Expand Down
Loading