Merge pull request #54 from yusufipk/fix/bunny-upload-reservation

fix(uploads): count a Bunny upload from the moment it is admitted
This commit is contained in:
Yusuf İpek
2026-08-18 11:12:30 +03:00
committed by GitHub
32 changed files with 1377 additions and 142 deletions
+109
View File
@@ -0,0 +1,109 @@
// The grant a share-link visitor gets for a direct upload.
//
// A guest's upload is billed to the workspace owner, not to the guest, so this
// token is the only thing tying what they declared and what they hold to the
// upload it was issued for. Two claims matter here beyond the existing subject
// binding: the provider's own video id, which is what makes releasing the hold
// on the guest's say-so safe, and the declared size, which is what the asset is
// charged until Bunny reports a figure of its own.
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
createGuestUploadToken,
readGuestUploadGrant,
verifyGuestUploadToken,
} from '@/lib/guest-upload-token';
const SUBJECT = {
projectId: 'project-1',
videoId: 'video-1',
intent: 'bunny' as const,
context: '203.0.113.7:public',
};
const BUNNY_VIDEO_ID = 'bunnyvideo-1-abcdefgh';
beforeEach(() => {
vi.stubEnv('GUEST_UPLOAD_TOKEN_SECRET', 'test-guest-upload-token-secret');
});
describe('readGuestUploadGrant', () => {
it('carries back the reservation and the declared size it was signed with', () => {
const token = createGuestUploadToken({
...SUBJECT,
providerVideoId: BUNNY_VIDEO_ID,
reservationId: 'reservation-1',
declaredSizeBytes: BigInt(4096),
});
expect(readGuestUploadGrant(token, SUBJECT, BUNNY_VIDEO_ID)).toEqual({
reservationId: 'reservation-1',
declaredSizeBytes: BigInt(4096),
});
});
// The binding that makes a guest release safe: presenting this token to cancel
// deletes the upload it stands for, so it cannot be used to drop the hold of
// an upload that is still running.
it('refuses a grant presented against a different provider video', () => {
const token = createGuestUploadToken({
...SUBJECT,
providerVideoId: BUNNY_VIDEO_ID,
reservationId: 'reservation-1',
});
expect(readGuestUploadGrant(token, SUBJECT, 'bunnyvideo-2-abcdefgh')).toBeNull();
expect(readGuestUploadGrant(token, SUBJECT, null)).toBeNull();
expect(verifyGuestUploadToken(token, SUBJECT, 'bunnyvideo-2-abcdefgh')).toBe(false);
});
it('still refuses a grant for another subject, bound video or not', () => {
const token = createGuestUploadToken({
...SUBJECT,
providerVideoId: BUNNY_VIDEO_ID,
reservationId: 'reservation-1',
});
expect(
readGuestUploadGrant(token, { ...SUBJECT, videoId: 'video-2' }, BUNNY_VIDEO_ID)
).toBeNull();
expect(readGuestUploadGrant(token, { ...SUBJECT, intent: 'image' }, BUNNY_VIDEO_ID)).toBeNull();
expect(
readGuestUploadGrant(token, { ...SUBJECT, context: '198.51.100.9:public' }, BUNNY_VIDEO_ID)
).toBeNull();
});
// The image and audio grants carry none of this, and a grant issued before the
// claims existed keeps working rather than failing an upload in flight.
it('reads a grant with no claims as holding nothing', () => {
const token = createGuestUploadToken({ ...SUBJECT, intent: 'image' });
const subject = { ...SUBJECT, intent: 'image' as const };
expect(readGuestUploadGrant(token, subject)).toEqual({
reservationId: null,
declaredSizeBytes: null,
});
expect(verifyGuestUploadToken(token, subject, BUNNY_VIDEO_ID)).toBe(true);
});
it('refuses a forged signature', () => {
const token = createGuestUploadToken({
...SUBJECT,
providerVideoId: BUNNY_VIDEO_ID,
reservationId: 'reservation-1',
});
const [payload] = token.split('.');
expect(readGuestUploadGrant(`${payload}.forged`, SUBJECT, BUNNY_VIDEO_ID)).toBeNull();
});
it('reads a non-positive declared size as nothing declared', () => {
const token = createGuestUploadToken({
...SUBJECT,
providerVideoId: BUNNY_VIDEO_ID,
declaredSizeBytes: BigInt(-1),
});
expect(readGuestUploadGrant(token, SUBJECT, BUNNY_VIDEO_ID)?.declaredSizeBytes).toBeNull();
});
});
+60
View File
@@ -0,0 +1,60 @@
import { describe, expect, it } from 'vitest';
import { parseDeclaredUploadSize } from '@/lib/upload-size';
const MAX = BigInt(5) * BigInt(1024) * BigInt(1024) * BigInt(1024);
function size(result: ReturnType<typeof parseDeclaredUploadSize>): bigint | null {
return 'sizeBytes' in result ? result.sizeBytes : null;
}
describe('parseDeclaredUploadSize', () => {
it('accepts a size sent as a string, which is how a client sends bytes it cannot hold in a number', () => {
expect(size(parseDeclaredUploadSize('4294967296', MAX))).toBe(BigInt(4294967296));
});
it('accepts a size sent as a number', () => {
expect(size(parseDeclaredUploadSize(1024, MAX))).toBe(BigInt(1024));
});
it('rejects a missing size', () => {
expect(parseDeclaredUploadSize(undefined, MAX)).toEqual({
error: 'sizeBytes must be a positive integer',
});
});
// Zero was the old behaviour of every Bunny init: it asked the quota whether
// it could store nothing, and the answer was always yes.
it('rejects zero', () => {
expect(parseDeclaredUploadSize(0, MAX)).toEqual({
error: 'sizeBytes must be a positive integer',
});
});
it('rejects a negative size', () => {
expect(parseDeclaredUploadSize(-1, MAX)).toEqual({
error: 'sizeBytes must be a positive integer',
});
});
it('rejects a fractional size rather than rounding it', () => {
expect(parseDeclaredUploadSize(1.5, MAX)).toEqual({
error: 'sizeBytes must be a positive integer',
});
});
it('rejects text that is not a number', () => {
expect(parseDeclaredUploadSize('a lot', MAX)).toEqual({
error: 'sizeBytes must be a positive integer',
});
});
it('rejects a size over the ceiling', () => {
expect(parseDeclaredUploadSize(MAX + BigInt(1), MAX)).toEqual({
error: 'File exceeds the maximum allowed upload size',
});
});
it('accepts a size exactly at the ceiling', () => {
expect(size(parseDeclaredUploadSize(MAX, MAX))).toBe(MAX);
});
});