This example shows the exact changes needed to fix the missing rpc() method issue.
/**
* Admin Service Unit Tests
*/
import { describe, test, expect, vi, beforeEach } from 'vitest';
import * as adminService from '@/lib/supabase/services/admin';
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '@/lib/supabase/types-compat';
describe('Admin Service', () => {
let mockSupabase: any;
beforeEach(() => {
mockSupabase = {
from: vi.fn().mockReturnThis(),
select: vi.fn().mockReturnThis(),
insert: vi.fn().mockReturnThis(),
update: vi.fn().mockReturnThis(),
eq: vi.fn().mockReturnThis(),
ilike: vi.fn().mockReturnThis(),
or: vi.fn().mockReturnThis(),
gte: vi.fn().mockReturnThis(),
order: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnThis(),
range: vi.fn().mockReturnThis(),
single: vi.fn().mockResolvedValue({ data: null, error: null }),
rpc: vi.fn().mockReturnThis(), // ❌ PROBLEM: No maybeSingle() support!
};
});
// ... tests follow
});The manual rpc: vi.fn().mockReturnThis() doesn't properly chain to .maybeSingle() or .single(), causing tests to fail when the service calls rpc('function_name').maybeSingle().
/**
* Admin Service Unit Tests
*/
import { describe, test, expect, vi, beforeEach } from 'vitest';
import * as adminService from '@/lib/supabase/services/admin';
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '@/lib/supabase/types-compat';
// ✅ ADD: Import mock factory
import { createSupabaseMock, createRpcMock } from '@/tests/helpers/supabase-mock';
describe('Admin Service', () => {
let mockSupabase: any;
beforeEach(() => {
// ✅ CHANGE: Use factory instead of manual mock
mockSupabase = createSupabaseMock();
});
// ... tests follow (mostly unchanged)
});- Import added (line 10): Import the mock factory helpers
- Mock creation simplified (line 17): Replace 30 lines of manual setup with 1 line
test('calculates platform statistics correctly', async () => {
let fromCallCount = 0;
mockSupabase.from.mockImplementation((table) => {
fromCallCount++;
if (fromCallCount === 1) {
// verifyAdminAccess
return {
select: vi.fn().mockReturnThis(),
eq: vi.fn().mockReturnThis(),
single: vi.fn().mockResolvedValue({
data: { user_type: 'admin' },
error: null,
}),
};
}
// Stats queries
if (table === 'users' && fromCallCount === 2) {
return {
select: vi.fn().mockResolvedValue({ count: 250, error: null }),
};
}
// ... more table mocks
});
const result = await adminService.getPlatformStats(
mockSupabase as SupabaseClient<Database>,
'admin-user-123'
);
expect(result).toEqual({
totalUsers: 250,
// ...
});
});test('calculates platform statistics correctly', async () => {
let fromCallCount = 0;
mockSupabase.from = vi.fn((table) => {
fromCallCount++;
if (fromCallCount === 1) {
// verifyAdminAccess
return {
select: vi.fn().mockReturnThis(),
eq: vi.fn().mockReturnThis(),
single: vi.fn().mockResolvedValue({
data: { user_type: 'admin' },
error: null,
}),
};
}
// Stats queries - same as before
if (table === 'users' && fromCallCount === 2) {
return {
select: vi.fn().mockResolvedValue({ count: 250, error: null }),
};
}
// ... more table mocks
});
// ✅ ADD: Mock RPC function for optimized stats
mockSupabase.rpc = createRpcMock('get_platform_stats_aggregated', {
data: {
total_users: 250,
total_students: 150,
total_preceptors: 80,
total_matches: 200,
active_matches: 50,
total_payments: 120,
recent_signups: 15,
pending_matches: 30,
},
error: null,
});
const result = await adminService.getPlatformStats(
mockSupabase as SupabaseClient<Database>,
'admin-user-123'
);
expect(result).toEqual({
totalUsers: 250,
totalStudents: 150,
totalPreceptors: 80,
totalMatches: 200,
activeMatches: 50,
totalPayments: 120,
recentSignups: 15,
pendingMatches: 30,
});
});- RPC mock added (line 32): Use
createRpcMock()to properly mock the RPC function - Supports maybeSingle(): Now works when service calls
rpc('get_platform_stats_aggregated').maybeSingle()
For even cleaner tests, you can use global setup:
import { describe, test, expect, beforeEach } from 'vitest';
import * as adminService from '@/lib/supabase/services/admin';
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '@/lib/supabase/types-compat';
import {
setupSupabaseMock,
getSupabaseMock,
configureRpcMock,
MockPresets
} from '@/tests/helpers/vi-setup';
describe('Admin Service', () => {
setupSupabaseMock(); // Auto-resets before each test
test('calculates platform statistics correctly', async () => {
const mockSupabase = getSupabaseMock();
// Configure admin user lookup
let fromCallCount = 0;
mockSupabase.from = vi.fn((table) => {
fromCallCount++;
if (fromCallCount === 1 && table === 'users') {
return createSupabaseMock(MockPresets.userLookup('admin-123', 'admin'));
}
// ... other tables
return createSupabaseMock();
});
// Configure RPC
configureRpcMock('get_platform_stats_aggregated', {
data: {
total_users: 250,
total_students: 150,
total_preceptors: 80,
},
error: null,
});
const result = await adminService.getPlatformStats(
mockSupabase as SupabaseClient<Database>,
'admin-user-123'
);
expect(result.totalUsers).toBe(250);
});
});/tests/unit/admin.test.ts
- Line 10: Add import statement
- Line 17: Replace manual mock with
createSupabaseMock() - Lines 32-42 (in each RPC test): Add
createRpcMock()calls
All tests in admin.test.ts that use getPlatformStats which calls RPC functions.
Run the test to verify it works:
npx vitest run tests/unit/admin.test.tsExpected output:
✓ tests/unit/admin.test.ts (38 tests) 25ms
✓ Admin Service (38)
✓ getAuditLogsForEntity (6)
✓ getRecentPaymentEvents (5)
✓ listUsers (6)
✓ searchUsers (5)
✓ updateUserType (5)
✓ getPlatformStats (6) ← NOW WORKS!
✓ getRecentAuditLogs (6)
✓ Edge Cases (4)
This same pattern applies to all other test files:
- Import mock factory helpers
- Replace manual mock setup with
createSupabaseMock() - Add
createRpcMock()for any RPC function calls - Optionally use
MockPresetsfor common scenarios - Optionally use global setup for cleaner code
That's it! 🎉