mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
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:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user