Comprehensive testing strategies for all authentication approaches, from unit tests to integration testing and production health checks.
πΊ
End-to-End
(User Flows)
πΊπΊπΊπΊπΊπΊπΊ
Integration Tests
(Service + Database)
πΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊ
Unit Tests (Business Logic)
Focus Areas:
- β Unit Tests: Core authentication logic, email validation, CUID generation
- β Integration Tests: Database operations, RLS policies, service interactions
- β End-to-End Tests: Complete user flows (signup β profile β access)
- β Performance Tests: Response times, concurrent users, database load
- β Security Tests: RLS enforcement, unauthorized access prevention
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([SupabaseClient])
void main() {
group('AuthCuidMappingService Unit Tests', () {
late AuthCuidMappingService service;
late MockSupabaseClient mockClient;
setUp(() {
mockClient = MockSupabaseClient();
service = AuthCuidMappingService();
// Inject mock client
});
group('Email Validation', () {
test('accepts valid email formats', () {
final validEmails = [
'user@example.com',
'test.email+tag@domain.co.uk',
'user123@sub.domain.org',
'first.last@company-name.com',
];
for (final email in validEmails) {
expect(service.isValidEmail(email), isTrue,
reason: 'Should accept valid email: $email');
}
});
test('rejects invalid email formats', () {
final invalidEmails = [
'',
'invalid-email',
'@domain.com',
'user@',
'user@.com',
'user name@domain.com', // space
'user@domain', // no TLD
];
for (final email in invalidEmails) {
expect(service.isValidEmail(email), isFalse,
reason: 'Should reject invalid email: $email');
}
});
});
group('User Creation Logic', () {
test('generates CUID for new users', () async {
// Mock successful database response
when(mockClient.from('users').insert(any).select().single())
.thenAnswer((_) async => {
'id': 'cuid_test_123',
'email': 'test@example.com',
'name': 'Test User',
});
final result = await service.createOrGetUserByEmail(
email: 'test@example.com',
name: 'Test User',
role: UserRole.consultee,
);
expect(result, equals('cuid_test_123'));
expect(result.startsWith('c'), isTrue, reason: 'Should be CUID format');
});
test('returns existing CUID for duplicate email', () async {
// Mock existing user found
when(mockClient.from('users').select().eq('email', any).maybeSingle())
.thenAnswer((_) async => {
'id': 'existing_cuid_456',
'email': 'existing@example.com',
});
final result = await service.createOrGetUserByEmail(
email: 'existing@example.com',
name: 'New Name',
role: UserRole.consultant,
);
expect(result, equals('existing_cuid_456'));
// Verify no insert was attempted
verifyNever(mockClient.from('users').insert(any));
});
});
group('Error Handling', () {
test('handles database connection errors gracefully', () async {
when(mockClient.from('users').select().eq('email', any).maybeSingle())
.thenThrow(Exception('Database connection failed'));
expect(
() => service.getUserByEmail('test@example.com'),
throwsA(isA<Exception>()),
);
});
test('handles malformed database responses', () async {
when(mockClient.from('users').select().eq('email', any).maybeSingle())
.thenAnswer((_) async => {'invalid': 'response'});
expect(
() => service.getUserByEmail('test@example.com'),
throwsA(isA<FormatException>()),
);
});
});
});
}import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsBinding.ensureInitialized();
group('Email-Based Authentication Integration', () {
late AuthCuidMappingService mappingService;
late AuthService authService;
setUpAll(() async {
// Initialize with test Supabase instance
await Supabase.initialize(
url: 'https://test-project.supabase.co',
anonKey: 'test-anon-key',
);
mappingService = AuthCuidMappingService();
authService = AuthService();
});
tearDownAll(() async {
// Cleanup test data
await cleanupTestData();
});
testWidgets('Complete signup flow creates user and profile', (tester) async {
// Arrange
const testEmail = 'integration-test@example.com';
const testPassword = 'testPassword123';
const testName = 'Integration Test User';
// Clean up any existing test data
await cleanupTestUser(testEmail);
// Act - Sign up user
final authResponse = await authService.signUp(
email: testEmail,
password: testPassword,
name: testName,
role: UserRole.consultee,
);
// Assert - Supabase user created
expect(authResponse.user, isNotNull);
expect(authResponse.user!.email, equals(testEmail));
// Assert - CUID user created
final cuidUser = await mappingService.getUserByEmail(testEmail);
expect(cuidUser, isNotNull);
expect(cuidUser!.email, equals(testEmail));
expect(cuidUser.name, equals(testName));
expect(cuidUser.role, equals(UserRole.consultee));
expect(cuidUser.id.startsWith('c'), isTrue);
// Assert - Profile created
final consulteeProfile = await getConsulteeProfileByUserId(cuidUser.id);
expect(consulteeProfile, isNotNull);
// Cleanup
await cleanupTestUser(testEmail);
});
testWidgets('Signin flow retrieves existing user', (tester) async {
// Arrange - Create test user first
const testEmail = 'signin-test@example.com';
const testPassword = 'testPassword123';
await authService.signUp(
email: testEmail,
password: testPassword,
name: 'Signin Test User',
role: UserRole.consultant,
);
// Sign out to test signin
await authService.signOut();
// Act - Sign in
final authResponse = await authService.signInWithPassword(
email: testEmail,
password: testPassword,
);
// Assert - Successful signin
expect(authResponse.user, isNotNull);
expect(authResponse.user!.email, equals(testEmail));
// Assert - Can access CUID profile
final currentProfile = await authService.getCurrentUserProfile();
expect(currentProfile, isNotNull);
expect(currentProfile!.email, equals(testEmail));
expect(currentProfile.role, equals(UserRole.consultant));
// Cleanup
await cleanupTestUser(testEmail);
});
testWidgets('RLS policies prevent unauthorized access', (tester) async {
// Arrange - Create two users
const user1Email = 'user1@example.com';
const user2Email = 'user2@example.com';
await createTestUser(user1Email, 'password1', UserRole.consultant);
await createTestUser(user2Email, 'password2', UserRole.consultee);
// Act - Sign in as user1
await authService.signInWithPassword(
email: user1Email,
password: 'password1',
);
final user1Profile = await authService.getCurrentUserProfile();
expect(user1Profile!.email, equals(user1Email));
// Try to access user2's data directly (should fail with RLS)
final user2CUID = await getCuidByEmail(user2Email);
expect(
() => getProfileDirectly(user2CUID),
throwsA(isA<PostgrestException>()), // RLS should block this
);
// Cleanup
await cleanupTestUser(user1Email);
await cleanupTestUser(user2Email);
});
testWidgets('Email change updates both auth and CUID user', (tester) async {
// This test would verify email change functionality
// Implementation depends on your email change workflow
});
});
}
// Helper functions for integration tests
Future<void> cleanupTestUser(String email) async {
// Remove from both auth.users and public.users
// Implementation depends on your cleanup strategy
}
Future<void> createTestUser(String email, String password, UserRole role) async {
final authService = AuthService();
await authService.signUp(
email: email,
password: password,
name: 'Test User',
role: role,
);
}import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:flutter/material.dart';
void main() {
IntegrationTestWidgetsBinding.ensureInitialized();
group('Authentication E2E Tests', () {
testWidgets('Complete user journey: signup β profile β logout β login', (tester) async {
// Launch app
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();
// Navigate to signup screen
await tester.tap(find.text('Sign Up'));
await tester.pumpAndSettle();
// Fill signup form
await tester.enterText(find.byKey(Key('email_field')), 'e2e-test@example.com');
await tester.enterText(find.byKey(Key('password_field')), 'testPassword123');
await tester.enterText(find.byKey(Key('name_field')), 'E2E Test User');
// Select role
await tester.tap(find.byKey(Key('role_dropdown')));
await tester.pumpAndSettle();
await tester.tap(find.text('Consultant'));
await tester.pumpAndSettle();
// Submit signup
await tester.tap(find.byKey(Key('signup_button')));
await tester.pumpAndSettle(Duration(seconds: 3));
// Verify navigation to profile/dashboard
expect(find.text('Welcome, E2E Test User'), findsOneWidget);
expect(find.text('Consultant'), findsOneWidget);
// Test profile functionality
await tester.tap(find.byKey(Key('edit_profile_button')));
await tester.pumpAndSettle();
// Update profile
await tester.enterText(find.byKey(Key('bio_field')), 'Updated bio from E2E test');
await tester.tap(find.byKey(Key('save_profile_button')));
await tester.pumpAndSettle();
// Verify profile update
expect(find.text('Profile updated successfully'), findsOneWidget);
// Test logout
await tester.tap(find.byKey(Key('logout_button')));
await tester.pumpAndSettle();
// Verify back to login screen
expect(find.text('Sign In'), findsOneWidget);
// Test login with same credentials
await tester.enterText(find.byKey(Key('login_email_field')), 'e2e-test@example.com');
await tester.enterText(find.byKey(Key('login_password_field')), 'testPassword123');
await tester.tap(find.byKey(Key('login_button')));
await tester.pumpAndSettle(Duration(seconds: 3));
// Verify successful login and data persistence
expect(find.text('Welcome back, E2E Test User'), findsOneWidget);
expect(find.text('Updated bio from E2E test'), findsOneWidget);
// Cleanup
await cleanupE2ETestUser('e2e-test@example.com');
});
testWidgets('OAuth signup flow works end-to-end', (tester) async {
// Launch app
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();
// Tap Google signup (this would open web view in real scenario)
await tester.tap(find.byKey(Key('google_signup_button')));
await tester.pumpAndSettle();
// In real E2E test, you'd need to handle OAuth flow
// This might involve web driver automation or mock responses
// Verify successful OAuth login
// expect(find.text('Welcome back'), findsOneWidget);
});
});
}import 'package:flutter_test/flutter_test.dart';
void main() {
group('Authentication Performance Tests', () {
late AuthCuidMappingService mappingService;
setUp(() {
mappingService = AuthCuidMappingService();
});
test('Email lookup performance under load', () async {
const testEmail = 'performance-test@example.com';
const iterations = 100;
// Create test user first
await mappingService.createOrGetUserByEmail(
email: testEmail,
name: 'Performance Test User',
role: UserRole.consultee,
);
// Measure lookup performance
final stopwatch = Stopwatch()..start();
for (int i = 0; i < iterations; i++) {
final user = await mappingService.getUserByEmail(testEmail);
expect(user, isNotNull);
}
stopwatch.stop();
final averageTime = stopwatch.elapsedMilliseconds / iterations;
print('Average email lookup time: ${averageTime}ms');
// Assert reasonable performance (adjust threshold as needed)
expect(averageTime, lessThan(50), // Should be under 50ms per lookup
reason: 'Email lookup should be fast with proper indexing');
});
test('Concurrent user creation performance', () async {
const concurrentUsers = 10;
final futures = <Future<String>>[];
final stopwatch = Stopwatch()..start();
// Create multiple users concurrently
for (int i = 0; i < concurrentUsers; i++) {
futures.add(
mappingService.createOrGetUserByEmail(
email: 'concurrent-$i@example.com',
name: 'Concurrent User $i',
role: UserRole.consultee,
)
);
}
final results = await Future.wait(futures);
stopwatch.stop();
// Verify all users created successfully
expect(results.length, equals(concurrentUsers));
for (final cuid in results) {
expect(cuid.startsWith('c'), isTrue);
}
final totalTime = stopwatch.elapsedMilliseconds;
final averageTime = totalTime / concurrentUsers;
print('Concurrent user creation total time: ${totalTime}ms');
print('Average time per user: ${averageTime}ms');
// Assert reasonable concurrent performance
expect(averageTime, lessThan(200), // Should be under 200ms per user
reason: 'Concurrent user creation should scale well');
});
test('Health check performance', () async {
final stopwatch = Stopwatch()..start();
final health = await mappingService.performHealthCheck();
stopwatch.stop();
expect(health.overallHealth, isTrue);
expect(stopwatch.elapsedMilliseconds, lessThan(1000), // Should be under 1 second
reason: 'Health check should complete quickly');
print('Health check completed in: ${stopwatch.elapsedMilliseconds}ms');
});
});
}-- Test script to verify RLS policies work correctly
-- Run this in Supabase SQL editor after setting up test data
-- Test 1: User can only see their own profile
-- Create test users first, then test access
-- Sign in as user1@example.com (simulate with auth.uid())
SELECT set_config('request.jwt.claims', '{"sub":"user1-uuid","email":"user1@example.com"}', true);
-- This should return the user's own profile
SELECT id, email, name FROM public.users WHERE email = 'user1@example.com';
-- Expected: 1 row returned
-- This should return empty (RLS blocks access to other users)
SELECT id, email, name FROM public.users WHERE email = 'user2@example.com';
-- Expected: 0 rows returned
-- Test 2: Consultant profile access
SELECT cp.id, cp.description, u.email
FROM public."ConsultantProfile" cp
JOIN public.users u ON u.id = cp."userId"
WHERE u.email = 'user1@example.com';
-- Expected: Returns profile if user1 is a consultant
-- Test 3: Cross-user profile access should be blocked
SELECT cp.id, cp.description, u.email
FROM public."ConsultantProfile" cp
JOIN public.users u ON u.id = cp."userId"
WHERE u.email = 'user2@example.com';
-- Expected: 0 rows (RLS should block this)
-- Reset config
SELECT set_config('request.jwt.claims', null, true);// Security test scenarios
void main() {
group('Authentication Security Tests', () {
test('Cannot access other users data with direct CUID', () async {
// Create two test users
final user1Cuid = await createTestUser('user1@example.com');
final user2Cuid = await createTestUser('user2@example.com');
// Sign in as user1
await signInAs('user1@example.com', 'password');
// Try to access user2's data directly (should fail)
expect(
() => getUserProfileDirectly(user2Cuid),
throwsA(isA<PostgrestException>()),
reason: 'RLS should prevent access to other users data',
);
});
test('Cannot create user with someone elses email', () async {
// Sign in as user1
await signInAs('user1@example.com', 'password');
// Try to create user with user2's email (should fail)
expect(
() => mappingService.createOrGetUserByEmail(
email: 'user2@example.com', // Different email than signed in user
name: 'Malicious User',
role: UserRole.admin, // Try to escalate privileges
),
throwsA(isA<PostgrestException>()),
reason: 'Should not allow creating users with different email',
);
});
test('Cannot update other users profiles', () async {
final user1Cuid = await createTestUser('user1@example.com');
final user2Cuid = await createTestUser('user2@example.com');
// Sign in as user1
await signInAs('user1@example.com', 'password');
// Try to update user2's profile (should fail)
expect(
() => updateUserProfileDirectly(user2Cuid, {'name': 'Hacked Name'}),
throwsA(isA<PostgrestException>()),
reason: 'RLS should prevent updating other users profiles',
);
});
test('Role escalation attempts are blocked', () async {
// Create regular user
await signInAs('regular@example.com', 'password');
// Try to update role to admin (should fail)
expect(
() => updateOwnProfile({'role': 'ADMIN'}),
throwsA(isA<Exception>()),
reason: 'Regular users should not be able to change their role',
);
});
test('SQL injection attempts are handled safely', () async {
final maliciousEmails = [
"user@example.com'; DROP TABLE users; --",
"user@example.com' OR '1'='1",
"user@example.com'; UPDATE users SET role = 'ADMIN'; --",
];
for (final maliciousEmail in maliciousEmails) {
expect(
() => mappingService.getUserByEmail(maliciousEmail),
throwsA(isA<Exception>()),
reason: 'Should handle SQL injection attempts safely',
);
}
});
});
}// Continuous health monitoring for production
class AuthHealthMonitor {
static const Duration _checkInterval = Duration(minutes: 5);
static Timer? _timer;
static void startMonitoring() {
_timer?.cancel();
_timer = Timer.periodic(_checkInterval, (_) => _performHealthCheck());
}
static Future<void> _performHealthCheck() async {
try {
final mappingService = AuthCuidMappingService();
final health = await mappingService.performHealthCheck();
if (health.overallHealth) {
logger.info('β
Auth system health check passed');
// Log key metrics
logger.info('π₯ Total users: ${health.totalUserCount}');
logger.info('π§ Email coverage: ${((health.usersWithEmail / health.totalUserCount) * 100).toStringAsFixed(1)}%');
} else {
logger.error('β Auth system health check failed');
logger.error('Errors: ${health.errors.join(", ")}');
// Alert monitoring system
await _sendHealthAlert(health);
}
// Record metrics for monitoring dashboard
await _recordHealthMetrics(health);
} catch (e) {
logger.error('π¨ Health check exception: $e');
await _sendCriticalAlert('Health check failed with exception: $e');
}
}
static Future<void> _sendHealthAlert(EmailBasedHealthCheck health) async {
// Send alert to monitoring system (Sentry, Slack, etc.)
// Implementation depends on your alerting setup
}
static Future<void> _recordHealthMetrics(EmailBasedHealthCheck health) async {
// Record metrics in time-series database for dashboards
// Implementation depends on your metrics system
}
}
// Usage in main app
void main() {
// ... app initialization
// Start health monitoring in production
if (kReleaseMode) {
AuthHealthMonitor.startMonitoring();
}
runApp(MyApp());
}// Load testing for authentication system
import 'dart:math';
class AuthLoadTester {
static final _random = Random();
static Future<void> runLoadTest({
int concurrentUsers = 50,
Duration testDuration = const Duration(minutes: 5),
int operationsPerUser = 10,
}) async {
print('π Starting auth load test:');
print(' Concurrent users: $concurrentUsers');
print(' Test duration: ${testDuration.inMinutes} minutes');
print(' Operations per user: $operationsPerUser');
final futures = <Future>[];
final stopwatch = Stopwatch()..start();
// Create concurrent user simulation
for (int i = 0; i < concurrentUsers; i++) {
futures.add(_simulateUser(i, operationsPerUser, testDuration));
}
// Wait for all users to complete or timeout
await Future.wait(futures, eagerError: false);
stopwatch.stop();
print('β
Load test completed in ${stopwatch.elapsedMilliseconds}ms');
}
static Future<void> _simulateUser(int userId, int operations, Duration maxDuration) async {
final mappingService = AuthCuidMappingService();
final userEmail = 'loadtest-$userId@example.com';
try {
// Simulate user operations
for (int i = 0; i < operations; i++) {
final operation = _random.nextInt(3);
switch (operation) {
case 0: // User lookup
await mappingService.getUserByEmail(userEmail);
break;
case 1: // User creation (idempotent)
await mappingService.createOrGetUserByEmail(
email: userEmail,
name: 'Load Test User $userId',
role: UserRole.consultee,
);
break;
case 2: // Health check
await mappingService.performHealthCheck();
break;
}
// Random delay between operations
await Future.delayed(Duration(milliseconds: _random.nextInt(1000)));
}
print('β
User $userId completed $operations operations');
} catch (e) {
print('β User $userId failed: $e');
}
}
}
// Run load test
void main() async {
await AuthLoadTester.runLoadTest(
concurrentUsers: 100,
testDuration: Duration(minutes: 10),
operationsPerUser: 20,
);
}class TestDataManager {
static const String _testPrefix = 'test-';
static const String _testDomain = 'example.com';
/// Generate test email that's easy to identify and cleanup
static String generateTestEmail([String? identifier]) {
final id = identifier ?? DateTime.now().millisecondsSinceEpoch.toString();
return '$_testPrefix$id@$_testDomain';
}
/// Create test user with automatic cleanup tracking
static Future<String> createTestUser({
String? email,
String? name,
UserRole role = UserRole.consultee,
}) async {
final testEmail = email ?? generateTestEmail();
final testName = name ?? 'Test User ${DateTime.now().millisecondsSinceEpoch}';
final mappingService = AuthCuidMappingService();
final cuid = await mappingService.createOrGetUserByEmail(
email: testEmail,
name: testName,
role: role,
);
// Track for cleanup
_testUsers.add(testEmail);
return cuid;
}
static final Set<String> _testUsers = <String>{};
/// Cleanup all test users created during test run
static Future<void> cleanupAllTestUsers() async {
for (final email in _testUsers) {
await _cleanupTestUser(email);
}
_testUsers.clear();
}
static Future<void> _cleanupTestUser(String email) async {
try {
// Remove from both Supabase auth and custom users table
// Implementation depends on your cleanup strategy
print('π§Ή Cleaned up test user: $email');
} catch (e) {
print('β οΈ Failed to cleanup test user $email: $e');
}
}
}
// Use in tests
void main() {
tearDownAll(() async {
await TestDataManager.cleanupAllTestUsers();
});
test('example test', () async {
final testCuid = await TestDataManager.createTestUser(
name: 'Specific Test User',
role: UserRole.consultant,
);
// Test code here...
// Cleanup happens automatically in tearDownAll
});
}// test_config.dart
class TestConfig {
static const bool useTestDatabase = true;
static const String testSupabaseUrl = 'https://test-project.supabase.co';
static const String testSupabaseKey = 'test-anon-key';
static Future<void> initializeForTesting() async {
await Supabase.initialize(
url: testSupabaseUrl,
anonKey: testSupabaseKey,
);
// Initialize logging for tests
LoggingService().initialize(level: LogLevel.debug);
// Other test setup...
}
}
// test_main.dart
void main() {
setUpAll(() async {
await TestConfig.initializeForTesting();
});
// Import all test files
// This ensures consistent test environment
}This comprehensive testing guide ensures your authentication system is:
- β Functionally correct - All business logic works as expected
- β Performant - Responds quickly under normal and high load
- β Secure - Properly enforces access controls and prevents attacks
- β Reliable - Handles errors gracefully and recovers well
- β Maintainable - Tests are clear, fast, and easy to update
Next Steps:
- Implement tests gradually, starting with core functionality
- Set up CI/CD pipeline to run tests automatically
- Monitor test results and system health in production
- Regularly review and update tests as system evolves
Remember: Good tests are an investment that pays dividends in confidence, speed of development, and system reliability.