Skip to content

Mock Factories Reference

The $test/utils module provides comprehensive mock factories for all external services and Cloudflare bindings used in show.fm.

Supabase Client

createMockSupabaseClient(overrides?)

Creates a fully-functional mock Supabase client with chainable query methods.

typescript
import { createMockSupabaseClient, mockUser } from '$test';

const client = createMockSupabaseClient();

Default Mock User

typescript
// Default user returned by auth.getUser()
{
  id: 'user-123',
  email: '[email protected]',
  user_metadata: { full_name: 'Test User' }
}

Customizing Auth

typescript
const client = createMockSupabaseClient({
	auth: {
		getUser: vi.fn().mockResolvedValue({
			data: { user: { id: 'custom-user', email: '[email protected]' } },
			error: null
		})
	}
});

Customizing Table Responses

typescript
const client = createMockSupabaseClient({
	from: {
		podcasts: {
			single: vi.fn().mockResolvedValue({
				data: { id: 'pod-123', name: 'My Podcast' },
				error: null
			})
		},
		episodes: {
			select: vi.fn().mockReturnThis(),
			eq: vi.fn().mockReturnThis(),
			single: vi.fn().mockResolvedValue({
				data: { id: 'ep-123', title: 'Episode 1' },
				error: null
			})
		}
	}
});

Supported Query Methods

All Supabase query chain methods are mocked:

CategoryMethods
CRUDselect, insert, update, delete, upsert
Filterseq, neq, gt, gte, lt, lte, like, ilike, is, in, contains, containedBy, range, textSearch, filter, not, or, match
Modifiersorder, limit, offset, single, maybeSingle, throwOnError

FTS Query Builder Mock

For testing PostgreSQL Full-Text Search queries, use a specialized chainable mock:

typescript
// Create chainable FTS mock with customizable response
function createMockQueryBuilder(resolvedData: unknown[] = [], count = 0): MockQueryBuilder {
	const builder = {
		select: vi.fn().mockReturnThis(),
		textSearch: vi.fn().mockReturnThis(),
		eq: vi.fn().mockReturnThis(),
		in: vi.fn().mockReturnThis(),
		gte: vi.fn().mockReturnThis(),
		lte: vi.fn().mockReturnThis(),
		order: vi.fn().mockReturnThis(),
		range: vi.fn().mockResolvedValue({
			data: resolvedData,
			count,
			error: null
		})
	};
	return builder;
}

// Create mock Supabase client with FTS support
function createFtsMockClient(builder: MockQueryBuilder) {
	return {
		from: vi.fn().mockReturnValue(builder)
	} as unknown as SupabaseClient<Database>;
}

Usage Example

typescript
import { ftsQuery } from '$lib/search/fts-search';

describe('FTS Queries', () => {
	it('should apply textSearch with websearch type', async () => {
		const mockData = [{ id: '1', title: 'Test Episode' }];
		const mockBuilder = createMockQueryBuilder(mockData, 1);
		const mockSupabase = createFtsMockClient(mockBuilder);

		await ftsQuery(mockSupabase, 'episodes', { query: 'test' });

		// Verify FTS was called correctly
		expect(mockBuilder.textSearch).toHaveBeenCalledWith('fts', 'test', {
			type: 'websearch',
			config: 'english'
		});
	});

	it('should apply date range filters', async () => {
		const mockBuilder = createMockQueryBuilder([], 0);
		const mockSupabase = createFtsMockClient(mockBuilder);
		const from = new Date('2024-01-01');
		const to = new Date('2024-12-31');

		await ftsQuery(mockSupabase, 'episodes', {
			dateRange: { published_at: { from, to } }
		});

		expect(mockBuilder.gte).toHaveBeenCalledWith('published_at', from.toISOString());
		expect(mockBuilder.lte).toHaveBeenCalledWith('published_at', to.toISOString());
	});

	it('should handle pagination correctly', async () => {
		const mockBuilder = createMockQueryBuilder([], 50);
		const mockSupabase = createFtsMockClient(mockBuilder);

		const result = await ftsQuery(mockSupabase, 'episodes', {
			pagination: { page: 3, perPage: 10 }
		});

		// Page 3, 10 per page → range(20, 29)
		expect(mockBuilder.range).toHaveBeenCalledWith(20, 29);
		expect(result.totalPages).toBe(5);
	});
});

Storage Mock

typescript
const { storage } = createMockSupabaseClient();

// Available methods
await storage.from('bucket').upload('path', file);
await storage.from('bucket').download('path');
await storage.from('bucket').remove(['path']);
storage.from('bucket').getPublicUrl('path');

Cloudflare R2 Bucket

createMockR2Bucket(overrides?)

Creates an in-memory R2 bucket backed by a Map for realistic storage testing.

typescript
import { createMockR2Bucket } from '$test';

const bucket = createMockR2Bucket();

Basic Operations

typescript
// Upload
await bucket.put('podcasts/123/cover.jpg', imageBuffer, {
	httpMetadata: { contentType: 'image/jpeg' },
	customMetadata: { podcastId: '123' }
});

// Download
const object = await bucket.get('podcasts/123/cover.jpg');
const data = await object?.arrayBuffer();

// Check existence
const head = await bucket.head('podcasts/123/cover.jpg');
if (head) {
	console.log('Size:', head.size);
}

// Delete
await bucket.delete('podcasts/123/cover.jpg');
// Or batch delete
await bucket.delete(['file1.jpg', 'file2.jpg']);

// List with prefix
const { objects } = await bucket.list({ prefix: 'podcasts/123/' });

Available Methods

MethodDescription
put(key, value, options)Store data with optional metadata
get(key)Retrieve object (returns null if not found)
head(key)Get metadata without body
delete(key | keys[])Remove one or more objects
list(options)List objects with prefix/limit/cursor
createMultipartUpload()Start multipart upload
resumeMultipartUpload()Resume multipart upload

Cloudflare Queues

Producer: createMockQueue<T>()

Creates a mock queue for testing message producers in the main app.

typescript
import { createMockQueue } from '$test';

interface AutomationMessage {
	type: 'execute_rule';
	rule_id: string;
	execution_id: string;
}

const queue = createMockQueue<AutomationMessage>();

// Send message
await queue.send({
	type: 'execute_rule',
	rule_id: 'rule-123',
	execution_id: 'exec-456'
});

// Assertions
expect(queue.send).toHaveBeenCalledWith(expect.objectContaining({ type: 'execute_rule' }));
expect(queue._messages).toHaveLength(1);

Batch Sending

typescript
await queue.sendBatch([
	{ body: { type: 'execute_rule', rule_id: 'rule-1' } },
	{ body: { type: 'execute_rule', rule_id: 'rule-2' } }
]);

expect(queue._messages).toHaveLength(2);

Consumer: createMockMessage<T>(body, attempts?)

Creates a mock message for testing queue consumer workers.

typescript
import { createMockMessage, createMockMessageBatch } from '$test';

const message = createMockMessage(
	{
		type: 'execute_rule',
		rule_id: 'rule-123'
	},
	1
); // attempts = 1

// Test your handler
await workerHandler.queue(createMockMessageBatch([message]), mockEnv);

// Verify acknowledgment
expect(message.ack).toHaveBeenCalled();
expect(message.retry).not.toHaveBeenCalled();

Message Properties

PropertyTypeDescription
idstringUnique message ID
bodyTMessage payload
timestampDateWhen message was created
attemptsnumberNumber of delivery attempts
ack()vi.fn()Spy for successful processing
retry()vi.fn()Spy for retry requests

Batch: createMockMessageBatch<T>(messages, queueName?)

Creates a message batch for testing queue consumer handlers.

typescript
const batch = createMockMessageBatch(
	[createMockMessage({ rule_id: 'rule-1' }), createMockMessage({ rule_id: 'rule-2' })],
	'automation-executions'
);

// Test handler
await workerHandler.queue(batch, mockEnv);

// Batch-level operations
batch.ackAll();
batch.retryAll();

Cloudflare Hyperdrive

createMockHyperdrive()

Creates a mock Hyperdrive binding for database connections.

typescript
import { createMockHyperdrive } from '$test';

const mockEnv = {
	HYPERDRIVE: createMockHyperdrive(),
	AUTOMATION_QUEUE: createMockQueue(),
	MEDIA_BUCKET: createMockR2Bucket()
};

Returns:

typescript
{
	connectionString: process.env.TEST_DATABASE_URL ||
		'postgresql://test:test@localhost:5432/test_db';
}

Database Mocking

For unit tests, mock the postgres client directly instead of making real database calls:

typescript
vi.mock('postgres', () => {
	const mockSql = vi.fn((strings, ...values) => {
		if (strings.join('?').includes('automation_rules')) {
			return Promise.resolve([{ id: 'rule-123', is_enabled: true }]);
		}
		return Promise.resolve([]);
	});
	mockSql.end = vi.fn();
	return { default: vi.fn(() => mockSql) };
});

Stripe Objects

mockStripeObjects.checkoutSession(overrides?)

Creates a mock Stripe Checkout Session.

typescript
import { mockStripeObjects } from '$test';

const session = mockStripeObjects.checkoutSession({
  customer: 'cus_456',
  metadata: { tier: 'pro' }
});

// Default values
{
  id: 'cs_test_123',
  url: 'https://checkout.stripe.com/pay/cs_test_123',
  customer: 'cus_test_123',
  subscription: 'sub_test_123',
  metadata: { user_id: 'user-123', tier: 'starter' }
}

mockStripeObjects.subscription(overrides?)

Creates a mock Stripe Subscription.

typescript
const subscription = mockStripeObjects.subscription({
  status: 'past_due'
});

// Default values
{
  id: 'sub_test_123',
  status: 'active',
  items: { data: [{ price: { id: 'price_starter' } }] }
}

Webhook Test Mock Pattern

For webhook handler tests, use hoisted mocks with mockImplementation to capture arguments for behavior assertions:

typescript
// Hoisted mocks ensure availability when vi.mock() is processed
const { mockConstructEvent, mockSubscriptionsRetrieve, mockSupabaseUpdate, mockSupabaseFrom } =
	vi.hoisted(() => ({
		mockConstructEvent: vi.fn(),
		mockSubscriptionsRetrieve: vi.fn(),
		mockSupabaseUpdate: vi.fn(),
		mockSupabaseFrom: vi.fn()
	}));

// Mock Stripe SDK with class
vi.mock('stripe', () => ({
	default: class MockStripe {
		webhooks = { constructEvent: mockConstructEvent };
		subscriptions = { retrieve: mockSubscriptionsRetrieve };
	}
}));

// Mock Supabase admin client with argument capture
vi.mock('$lib/supabase/admin', () => ({
	createAdminClient: vi.fn(() => ({
		from: mockSupabaseFrom.mockImplementation((table: string) => ({
			update: mockSupabaseUpdate.mockImplementation((data: Record<string, unknown>) => ({
				eq: vi.fn().mockImplementation((column: string, value: string) => ({
					select: vi.fn().mockResolvedValue({
						data: [{ id: 'user-123', ...data }],
						error: null
					})
				}))
			})),
			select: vi.fn().mockReturnValue({
				eq: vi.fn().mockResolvedValue({ data: null, error: null })
			})
		}))
	})),
	getTierFromPriceId: vi.fn((priceId: string) => {
		if (priceId.includes('professional') || priceId.includes('pro')) return 'professional';
		if (priceId.includes('enterprise')) return 'enterprise';
		return 'starter';
	})
}));

Behavior Assertions

typescript
// Verify correct table was accessed
expect(mockSupabaseFrom).toHaveBeenCalledWith('user_profiles');

// Verify correct data was written (not just that update was called)
expect(mockSupabaseUpdate).toHaveBeenCalledWith(
	expect.objectContaining({
		subscription_tier: 'starter',
		subscription_status: 'active',
		stripe_customer_id: 'cus_test_123'
	})
);

Important

The Stripe webhook handler does NOT store stripe_subscription_id in the database. Test assertions should not expect this field.


Resend Email

mockResendSuccess(emailId?)

Mocks a successful Resend API response.

typescript
import { mockResendSuccess } from '$test';

beforeEach(() => {
	mockResendSuccess('email-abc');
});

it('should send email', async () => {
	await sendEmail({ to: '[email protected]', subject: 'Test' });

	expect(global.fetch).toHaveBeenCalledWith(
		'https://api.resend.com/emails',
		expect.objectContaining({ method: 'POST' })
	);
});

mockResendError(status, message)

Mocks a failed Resend API response.

typescript
import { mockResendError } from '$test';

it('should handle rate limiting', async () => {
	mockResendError(429, 'Rate limit exceeded');

	await expect(sendEmail({})).rejects.toThrow('Rate limit');
});

Test Data Factories

testData.podcast(overrides?)

typescript
import { testData } from '$test';

const podcast = testData.podcast({ name: 'Custom Name' });

// Default values
{
  id: 'podcast-123',
  name: 'Test Podcast',
  slug: 'test-podcast',
  description: 'A test podcast for unit tests',
  created_at: '2026-01-14T...'
}

testData.episode(overrides?)

typescript
const episode = testData.episode({ status: 'published' });

// Default values
{
  id: 'episode-123',
  title: 'Test Episode',
  podcast_id: 'podcast-123',
  description: 'A test episode',
  status: 'draft',
  created_at: '2026-01-14T...'
}

testData.booking(overrides?)

typescript
const booking = testData.booking({ guest_email: '[email protected]' });

// Default values
{
  id: 'booking-123',
  guest_email: '[email protected]',
  guest_name: 'Test Guest',
  scheduled_time: '2026-01-15T...',  // +1 day from now
  status: 'pending'
}

Component Test Helpers

waitForDomUpdate()

Waits for DOM updates after Svelte 5 state changes.

typescript
import { waitForDomUpdate } from '$test';

it('should update counter', async () => {
	render(Counter);
	fireEvent.click(screen.getByRole('button'));

	await waitForDomUpdate();

	expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

createScreenHelpers(screen)

Type-safe wrapper for common Testing Library queries.

typescript
import { createScreenHelpers } from '$test';
import { screen } from '@testing-library/svelte';

const helpers = createScreenHelpers(screen);

// Type-safe queries
const button = helpers.getButton('Submit');
const input = helpers.getInput('Email');
const link = helpers.getLink('Learn more');
const heading = helpers.getHeading('Welcome');

Complete Worker Environment Example

typescript
import {
	createMockR2Bucket,
	createMockQueue,
	createMockHyperdrive,
	createMockMessage,
	createMockMessageBatch
} from '$test';

describe('Automation Executor Worker', () => {
	const mockEnv = {
		MEDIA_BUCKET: createMockR2Bucket(),
		AUTOMATION_QUEUE: createMockQueue(),
		HYPERDRIVE: createMockHyperdrive(),
		PUBLIC_APP_URL: 'https://app.test.com',
		RESEND_API_KEY: 'test-key'
	};

	it('should process automation message', async () => {
		const message = createMockMessage({
			type: 'execute_rule',
			rule_id: 'rule-123',
			execution_id: 'exec-456',
			payload: { podcast_id: 'pod-789' }
		});

		const batch = createMockMessageBatch([message], 'automation-executions');

		await workerHandler.queue(batch, mockEnv);

		expect(message.ack).toHaveBeenCalled();
	});
});

Internal documentation - Not for public distribution