Advertisement

← vibesdk

worker/api/handlers/space-preview-ratelimit.test.ts

3.2 KB Download file
import { beforeEach, describe, expect, it, vi } from 'vitest';
// Import via the same specifier the handler uses so `instanceof` matches the
// class identity inside space-preview.ts (Vite resolves the `shared` alias).
import { RateLimitExceededError } from 'shared/types/errors';

// getPreviewVersion is always 0 here (no visibility revocation under test).
vi.mock('../../database/services/AppService', () => ({
	AppService: class {
		async getPreviewVersion() {
			return 0;
		}
	},
}));

// Simulate the per-token limiter: allow up to LIMIT calls per token, then throw.
const LIMIT = 3;
const callsByToken = new Map<string, number>();
vi.mock('../../services/rate-limit/rateLimits', () => ({
	RateLimitService: {
		enforceSpacePreviewRateLimit: async (
			_env: unknown,
			_config: unknown,
			tokenId: string,
		) => {
			const n = (callsByToken.get(tokenId) ?? 0) + 1;
			callsByToken.set(tokenId, n);
			if (n > LIMIT) {
				throw new RateLimitExceededError('Space preview rate limit exceeded', 'spacePreview');
			}
		},
	},
}));

const { handleSpacePreview } = await import('./space-preview');
const { signSpacePreviewToken } = await import('../../utils/spacePreviewToken');

const JWT_SECRET = 'test-secret-for-space-preview-token-validation-1234567890';

type HandlerEnv = Parameters<typeof handleSpacePreview>[1];

function makeEnv(): HandlerEnv {
	const stub = {
		fetch: async () => new Response('ok', { headers: { 'content-type': 'text/html' } }),
	};
	return {
		JWT_SECRET,
		CUSTOM_DOMAIN: 'example.com',
		CUSTOM_PREVIEW_DOMAIN: '',
		SPACE_DO: { idFromName: (n: string) => n, get: () => stub },
	} as unknown as HandlerEnv;
}

function previewRequest(token: string): Request {
	return new Request(
		`https://example.com/space/space-a/preview/main/?t=${encodeURIComponent(token)}`,
	);
}

beforeEach(() => {
	callsByToken.clear();
});

describe('handleSpacePreview rate limiting', () => {
	it('returns 200 up to the limit then 429 for sustained same-token traffic', async () => {
		const env = makeEnv();
		const token = await signSpacePreviewToken(env, {
			spaceName: 'space-a',
			branch: 'main',
			userId: 'user-a',
			previewVersion: 0,
		});
		const req = () => handleSpacePreview(previewRequest(token), env, 'space-a', 'main');

		for (let i = 0; i < LIMIT; i++) {
			expect((await req()).status).toBe(200);
		}
		// Over the limit.
		expect((await req()).status).toBe(429);
	});

	it('counts distinct tokens separately', async () => {
		const env = makeEnv();
		const tokenA = await signSpacePreviewToken(env, {
			spaceName: 'space-a',
			branch: 'main',
			userId: 'user-a',
			previewVersion: 0,
		});
		const tokenB = await signSpacePreviewToken(env, {
			spaceName: 'space-a',
			branch: 'main',
			userId: 'user-b',
			previewVersion: 0,
		});

		// Exhaust token A.
		for (let i = 0; i < LIMIT; i++) {
			await handleSpacePreview(previewRequest(tokenA), env, 'space-a', 'main');
		}
		expect((await handleSpacePreview(previewRequest(tokenA), env, 'space-a', 'main')).status).toBe(429);
		// Token B is unaffected.
		expect((await handleSpacePreview(previewRequest(tokenB), env, 'space-a', 'main')).status).toBe(200);
	});
});