Integration Testing Document
Parent Document: TEST_PLAN.mdVersion: 1.2 | Date: January 5, 2026
1. Overview
1.1 Purpose
This document defines the integration testing strategy for the Dustac Environmental Monitoring Dashboard. Integration tests validate interactions between components, services, APIs, and the database.
1.2 Scope
- Service-to-database interactions (Supabase)
- API endpoint integration
- Authentication flow integration
- Storage operations (file upload/download)
- Cross-feature module interactions
- External API integrations (BOM, Dustac Scraper)
1.3 Tools & Framework
| Tool | Version | Purpose |
|---|---|---|
| Vitest | ^2.x | Test runner |
| Supabase Local | Latest | Local database instance |
| MSW | ^2.x | External API mocking |
| Docker | Latest | Supabase containers |
2. Test Environment Setup
2.1 Local Supabase
bash
# Start local Supabase
pnpm supabase:start
# Verify status
pnpm supabase:status
# Reset database with seed data
pnpm supabase:db:reset2.2 Environment Variables
bash
# .env.test.local
VITE_SUPABASE_URL=http://localhost:54321
VITE_SUPABASE_ANON_KEY=<local-anon-key>
SUPABASE_SERVICE_ROLE_KEY=<local-service-role-key>2.3 Test Database Seeding
sql
-- supabase/seed.sql
INSERT INTO auth.users (id, email) VALUES
('user-001', 'test.user@dustac-test.com'),
('user-002', 'test.admin@dustac-test.com');
INSERT INTO cfg_mine_sites (id, site_name, user_id) VALUES
('site-001', 'Test Site Alpha', 'user-001'),
('site-002', 'Test Site Beta', 'user-002');
INSERT INTO cfg_devices (id, device_name, site_id) VALUES
('device-001', 'Device Alpha 1', 'site-001'),
('device-002', 'Device Beta 1', 'site-002');3. Test Commands
bash
# Run integration tests
pnpm test:integration
# Run with local Supabase (requires Docker)
pnpm supabase:start && pnpm test:integration
# Run specific integration test
pnpm test:integration -- src/features/reports/services/reportDataService.integration.test.ts
# Run with verbose output
pnpm test:integration -- --reporter=verbose4. Integration Test Cases
4.1 Database Integration
TC-INT-DB-001: RLS Policy Enforcement
| Field | Value |
|---|---|
| Test ID | TC-INT-DB-001 |
| Priority | P0 |
| Type | Security Integration |
Test Code:
typescript
describe('RLS Policy Enforcement', () => {
let user1Client: SupabaseClient;
let user2Client: SupabaseClient;
beforeAll(async () => {
// Create authenticated clients for different users
user1Client = await createAuthenticatedClient('user-001');
user2Client = await createAuthenticatedClient('user-002');
});
it('should allow user to read own site data', async () => {
const { data, error } = await user1Client
.from('cfg_mine_sites')
.select('*')
.eq('id', 'site-001');
expect(error).toBeNull();
expect(data).toHaveLength(1);
expect(data[0].site_name).toBe('Test Site Alpha');
});
it('should prevent user from reading other user site data', async () => {
const { data, error } = await user1Client
.from('cfg_mine_sites')
.select('*')
.eq('id', 'site-002'); // belongs to user-002
expect(error).toBeNull();
expect(data).toHaveLength(0); // RLS filters out
});
it('should prevent user from modifying other user data', async () => {
const { error } = await user1Client
.from('cfg_mine_sites')
.update({ site_name: 'Hacked' })
.eq('id', 'site-002');
// Should either return error or affect 0 rows
expect(error || true).toBeTruthy();
});
it('should enforce RLS on upload_measurements', async () => {
// User 1 creates upload
const { data: upload } = await user1Client
.from('upload_sessions')
.insert({ site_id: 'site-001' })
.select()
.single();
// User 2 should not see it
const { data } = await user2Client
.from('upload_sessions')
.select('*')
.eq('id', upload.id);
expect(data).toHaveLength(0);
});
});TC-INT-DB-002: Upload Session Creation
| Field | Value |
|---|---|
| Test ID | TC-INT-DB-002 |
| Priority | P0 |
| Type | Database Integration |
Test Code:
typescript
describe('Upload Session Integration', () => {
it('should create upload session with measurements', async () => {
const client = await createAuthenticatedClient('user-001');
// Create upload session
const { data: session, error: sessionError } = await client
.from('upload_sessions')
.insert({
site_id: 'site-001',
status: 'pending'
})
.select()
.single();
expect(sessionError).toBeNull();
expect(session.id).toBeDefined();
// Insert measurements
const measurements = generateMeasurements({ count: 100, device: 'device-001' });
const { error: measurementError } = await client
.from('upload_measurements')
.insert(measurements.map(m => ({
...m,
upload_id: session.id
})));
expect(measurementError).toBeNull();
// Verify count
const { count } = await client
.from('upload_measurements')
.select('*', { count: 'exact', head: true })
.eq('upload_id', session.id);
expect(count).toBe(100);
});
it('should cascade delete measurements when upload deleted', async () => {
const client = await createAuthenticatedClient('user-001');
// Create session with measurements
const { data: session } = await client
.from('upload_sessions')
.insert({ site_id: 'site-001' })
.select()
.single();
await client.from('upload_measurements').insert([
{ upload_id: session.id, time: new Date().toISOString(), loc: 'device-001' }
]);
// Delete session
await client.from('upload_sessions').delete().eq('id', session.id);
// Verify measurements deleted
const { data } = await client
.from('upload_measurements')
.select('*')
.eq('upload_id', session.id);
expect(data).toHaveLength(0);
});
});TC-INT-DB-003: Report Generation with Data
| Field | Value |
|---|---|
| Test ID | TC-INT-DB-003 |
| Priority | P0 |
| Type | Database Integration |
Test Code:
typescript
describe('Report Generation Integration', () => {
let uploadId: string;
beforeAll(async () => {
// Setup: Create upload with measurements
const client = await createServiceRoleClient();
const { data: session } = await client
.from('upload_sessions')
.insert({ site_id: 'site-001', user_id: 'user-001' })
.select()
.single();
uploadId = session.id;
// Insert test measurements with gaps
const measurements = [
...generateMeasurements({ device: 'DEVICE-001', count: 50, startTime: '2025-01-01T00:00:00Z' }),
...generateMeasurements({ device: 'DEVICE-001', count: 50, startTime: '2025-01-01T05:00:00Z' }), // 3hr gap
...generateMeasurements({ device: 'DEVICE-002', count: 100, startTime: '2025-01-01T00:00:00Z' })
];
await client.from('upload_measurements').insert(
measurements.map(m => ({ ...m, upload_id: uploadId }))
);
});
it('should fetch all measurements for report', async () => {
const measurements = await ReportDataService.getMeasurements(uploadId);
expect(measurements.length).toBe(200);
});
it('should calculate correct statistics', async () => {
const measurements = await ReportDataService.getMeasurements(uploadId);
const stats = ReportDataService.calculateStatistics(measurements);
expect(stats.totalMeasurements).toBe(200);
expect(stats.devices).toContain('DEVICE-001');
expect(stats.devices).toContain('DEVICE-002');
});
it('should detect data gaps in timeline', async () => {
const measurements = await ReportDataService.getMeasurements(uploadId);
const timeline = ReportDataService.prepareDeviceTimeline(measurements);
const device1 = timeline.find(d => d.device === 'DEVICE-001');
const device2 = timeline.find(d => d.device === 'DEVICE-002');
expect(device1?.dataGaps).toBe(1); // Has 3hr gap
expect(device1?.segments).toHaveLength(2);
expect(device2?.dataGaps).toBe(0); // Continuous
expect(device2?.segments).toHaveLength(1);
});
it('should generate complete report template data', async () => {
const templateData = await ReportDataService.generateReportTemplateData(uploadId);
expect(templateData.reportInfo).toBeDefined();
expect(templateData.statistics).toBeDefined();
expect(templateData.chartData.deviceTimeline).toHaveLength(2);
expect(templateData.chartData.timeSeries.length).toBeGreaterThan(0);
});
});4.2 Storage Integration
TC-INT-STORAGE-001: CSV File Upload
| Field | Value |
|---|---|
| Test ID | TC-INT-STORAGE-001 |
| Priority | P0 |
| Type | Storage Integration |
Test Code:
typescript
describe('Storage Integration', () => {
it('should upload CSV file to storage', async () => {
const client = await createAuthenticatedClient('user-001');
const csvContent = 'time,loc,value\n2025-01-01,DEVICE,10';
const file = new File([csvContent], 'test.csv', { type: 'text/csv' });
const { data, error } = await client.storage
.from('csv-uploads')
.upload(`user-001/test-${Date.now()}.csv`, file);
expect(error).toBeNull();
expect(data?.path).toContain('user-001');
});
it('should generate signed download URL', async () => {
const client = await createAuthenticatedClient('user-001');
// Upload file first
const { data: uploadData } = await client.storage
.from('csv-uploads')
.upload(`user-001/signed-test.csv`, 'test content');
// Get signed URL
const { data: urlData, error } = await client.storage
.from('csv-uploads')
.createSignedUrl(uploadData.path, 3600); // 1 hour expiry
expect(error).toBeNull();
expect(urlData?.signedUrl).toContain('token=');
});
it('should prevent access to other user files', async () => {
const user1Client = await createAuthenticatedClient('user-001');
const user2Client = await createAuthenticatedClient('user-002');
// User 1 uploads file
await user1Client.storage
.from('csv-uploads')
.upload('user-001/private.csv', 'secret');
// User 2 tries to download
const { error } = await user2Client.storage
.from('csv-uploads')
.download('user-001/private.csv');
expect(error).not.toBeNull();
});
});4.3 Authentication Integration
TC-INT-AUTH-001: Login Flow
| Field | Value |
|---|---|
| Test ID | TC-INT-AUTH-001 |
| Priority | P0 |
| Type | Auth Integration |
Test Code:
typescript
describe('Authentication Integration', () => {
it('should login with valid credentials', async () => {
const { data, error } = await supabase.auth.signInWithPassword({
email: 'test.user@dustac-test.com',
password: 'testpassword123'
});
expect(error).toBeNull();
expect(data.session).toBeDefined();
expect(data.session.access_token).toBeDefined();
});
it('should reject invalid credentials', async () => {
const { error } = await supabase.auth.signInWithPassword({
email: 'test.user@dustac-test.com',
password: 'wrongpassword'
});
expect(error).not.toBeNull();
expect(error.message).toContain('Invalid');
});
it('should refresh session with valid refresh token', async () => {
// Login first
const { data: loginData } = await supabase.auth.signInWithPassword({
email: 'test.user@dustac-test.com',
password: 'testpassword123'
});
// Refresh session
const { data: refreshData, error } = await supabase.auth.refreshSession({
refresh_token: loginData.session.refresh_token
});
expect(error).toBeNull();
expect(refreshData.session.access_token).not.toBe(loginData.session.access_token);
});
it('should logout and invalidate session', async () => {
await supabase.auth.signInWithPassword({
email: 'test.user@dustac-test.com',
password: 'testpassword123'
});
await supabase.auth.signOut();
const { data } = await supabase.auth.getSession();
expect(data.session).toBeNull();
});
});4.4 External API Integration
TC-INT-API-001: BOM Weather API
| Field | Value |
|---|---|
| Test ID | TC-INT-API-001 |
| Priority | P1 |
| Type | External API Integration |
Test Code:
typescript
describe('BOM Weather API Integration', () => {
// Use MSW to mock external API
beforeAll(() => {
server.use(
http.get('http://www.bom.gov.au/fwo/*', () => {
return HttpResponse.json({
observations: {
data: [
{ local_date_time: '01/01:00pm', air_temp: 35.5, rel_hum: 45 }
]
}
});
})
);
});
it('should fetch weather data for station', async () => {
const data = await WeatherService.fetchStationData('IDW60901');
expect(data).toBeDefined();
expect(data.observations).toHaveLength(1);
expect(data.observations[0].air_temp).toBe(35.5);
});
it('should handle API timeout gracefully', async () => {
server.use(
http.get('http://www.bom.gov.au/fwo/*', async () => {
await delay(10000); // Simulate timeout
return HttpResponse.json({});
})
);
await expect(WeatherService.fetchStationData('IDW60901'))
.rejects.toThrow('timeout');
});
it('should cache weather data', async () => {
// First call
await WeatherService.fetchStationData('IDW60901');
// Second call should use cache
const startTime = Date.now();
await WeatherService.fetchStationData('IDW60901');
const duration = Date.now() - startTime;
expect(duration).toBeLessThan(50); // Cached response is fast
});
});4.5 Cross-Feature Integration
TC-INT-CROSS-001: Upload to Report Flow
| Field | Value |
|---|---|
| Test ID | TC-INT-CROSS-001 |
| Priority | P0 |
| Type | Cross-Feature Integration |
Test Code:
typescript
describe('Upload to Report Integration', () => {
it('should generate report from uploaded CSV', async () => {
const client = await createAuthenticatedClient('user-001');
// Step 1: Upload CSV
const csvContent = generateTestCSV({ devices: 2, recordsPerDevice: 50 });
const parseResult = await CsvParser.parse(csvContent);
expect(parseResult.success).toBe(true);
// Step 2: Create upload session
const { data: session } = await client
.from('upload_sessions')
.insert({ site_id: 'site-001', status: 'processing' })
.select()
.single();
// Step 3: Insert measurements
await client.from('upload_measurements').insert(
parseResult.data.map(m => ({ ...m, upload_id: session.id }))
);
// Step 4: Generate report data
const reportData = await ReportDataService.generateReportTemplateData(session.id);
expect(reportData.statistics.totalMeasurements).toBe(100);
expect(reportData.chartData.deviceTimeline).toHaveLength(2);
// Step 5: Create report record
const { data: report } = await client
.from('reports')
.insert({
upload_id: session.id,
site_name: 'Test Site Alpha',
status: 'completed',
metadata: {
statistics: reportData.statistics
}
})
.select()
.single();
expect(report.status).toBe('completed');
});
});5. Test Configuration
5.1 Integration Test Config
typescript
// vitest.integration.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
include: ['src/**/*.integration.test.ts'],
environment: 'node',
testTimeout: 30000,
hookTimeout: 30000,
setupFiles: ['./src/test/integration-setup.ts'],
globalSetup: './src/test/global-setup.ts',
pool: 'forks', // Isolate tests
poolOptions: {
forks: {
singleFork: true // Run sequentially for DB tests
}
}
}
});5.2 Global Setup
typescript
// src/test/global-setup.ts
export async function setup() {
// Ensure Supabase is running
console.log('Starting Supabase...');
execSync('pnpm supabase:start', { stdio: 'inherit' });
// Reset database
console.log('Resetting database...');
execSync('pnpm supabase:db:reset', { stdio: 'inherit' });
}
export async function teardown() {
// Optional: Stop Supabase after tests
// execSync('pnpm supabase:stop');
}6. Test Utilities
6.1 Authenticated Client Helper
typescript
// src/test/helpers/supabase.ts
export async function createAuthenticatedClient(userId: string): Promise<SupabaseClient> {
const { data } = await supabase.auth.signInWithPassword({
email: `${userId}@dustac-test.com`,
password: 'testpassword123'
});
return createClient(
process.env.VITE_SUPABASE_URL!,
process.env.VITE_SUPABASE_ANON_KEY!,
{
global: {
headers: {
Authorization: `Bearer ${data.session.access_token}`
}
}
}
);
}
export async function createServiceRoleClient(): Promise<SupabaseClient> {
return createClient(
process.env.VITE_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
}Related Documents:
- TEST_PLAN.md - Main test plan
- TEST_UNIT.md - Unit tests
- TEST_E2E.md - End-to-end tests