Testing
Testing RLS-enabled models requires special consideration since policies are enforced at the database level.
Basic Testing Setup
from django.test import TestCase
from django.contrib.auth.models import User
from django_rls.db.functions import set_rls_context
from myapp.models import Document
class RLSTestCase(TestCase):
def setUp(self):
# Create test users
self.user1 = User.objects.create_user('user1')
self.user2 = User.objects.create_user('user2')
# Set RLS context for user1
set_rls_context('user_id', self.user1.id)
# Create test data
self.doc1 = Document.objects.create(
title='User 1 Doc',
owner=self.user1
)
# Switch to user2
set_rls_context('user_id', self.user2.id)
self.doc2 = Document.objects.create(
title='User 2 Doc',
owner=self.user2
)
Testing Policy Enforcement
def test_user_can_only_see_own_documents(self):
# Set context to user1
set_rls_context('user_id', self.user1.id)
# User1 should only see their document
docs = Document.objects.all()
self.assertEqual(docs.count(), 1)
self.assertEqual(docs.first().owner, self.user1)
# Switch to user2
set_rls_context('user_id', self.user2.id)
# User2 should only see their document
docs = Document.objects.all()
self.assertEqual(docs.count(), 1)
self.assertEqual(docs.first().owner, self.user2)
def test_user_cannot_update_others_documents(self):
# Try to update user2's document as user1
set_rls_context('user_id', self.user1.id)
# This should return 0 rows updated
updated = Document.objects.filter(
id=self.doc2.id
).update(title='Hacked!')
self.assertEqual(updated, 0)
# Verify the document wasn't changed
self.doc2.refresh_from_db()
self.assertEqual(self.doc2.title, 'User 2 Doc')
Test Utilities
Django RLS provides test utilities to make testing easier:
from django_rls.test import RLSTestMixin
class DocumentTestCase(RLSTestMixin, TestCase):
def test_with_rls_disabled(self):
# Temporarily disable RLS
with self.disable_rls(Document):
# Can see all documents
docs = Document.objects.all()
self.assertEqual(docs.count(), 2)
def test_as_different_user(self):
# Test as user1
with self.as_user(self.user1):
docs = Document.objects.all()
self.assertEqual(docs.count(), 1)
# Test as user2
with self.as_user(self.user2):
docs = Document.objects.all()
self.assertEqual(docs.count(), 1)
Testing Multi-Tenant Applications
class TenantTestCase(RLSTestMixin, TestCase):
def setUp(self):
# Create tenants
self.tenant1 = Tenant.objects.create(name='Tenant 1')
self.tenant2 = Tenant.objects.create(name='Tenant 2')
# Create users in different tenants
self.user1 = User.objects.create_user('user1')
self.user1.profile.tenant = self.tenant1
self.user1.profile.save()
def test_tenant_isolation(self):
# Set context for tenant1
with self.with_context(user_id=self.user1.id,
tenant_id=self.tenant1.id):
# Create data in tenant1
TenantModel.objects.create(
name='Tenant 1 Data',
tenant=self.tenant1
)
# Switch to tenant2
with self.with_context(user_id=self.user2.id,
tenant_id=self.tenant2.id):
# Should not see tenant1's data
data = TenantModel.objects.all()
self.assertEqual(data.count(), 0)
Testing with Fixtures
# fixtures/test_rls_data.json
[
{
"model": "myapp.document",
"pk": 1,
"fields": {
"title": "Public Document",
"is_public": true,
"owner": 1
}
}
]
class FixtureTestCase(TestCase):
fixtures = ['test_rls_data.json']
def test_fixtures_respect_rls(self):
# Set context
set_rls_context('user_id', '1')
# Verify fixture data is filtered
docs = Document.objects.all()
# Only see documents based on policy
Testing Custom Policies
def test_complex_policy(self):
# Create test data
project = Project.objects.create(
name='Test Project',
owner=self.user1,
is_public=False,
tenant=self.tenant1
)
project.team.add(self.user2)
# Test owner access
with self.as_user(self.user1, tenant=self.tenant1):
projects = Project.objects.all()
self.assertIn(project, projects)
# Test team member access
with self.as_user(self.user2, tenant=self.tenant1):
projects = Project.objects.all()
self.assertIn(project, projects)
# Test no access from different tenant
with self.as_user(self.user3, tenant=self.tenant2):
projects = Project.objects.all()
self.assertNotIn(project, projects)
Performance Testing
from django.test import TransactionTestCase
from django.test.utils import override_settings
import time
class RLSPerformanceTest(TransactionTestCase):
def test_query_performance_with_rls(self):
# Create large dataset
for i in range(1000):
Document.objects.create(
title=f'Doc {i}',
owner=self.user1 if i % 2 == 0 else self.user2
)
# Test query time with RLS
set_rls_context('user_id', self.user1.id)
start = time.time()
docs = list(Document.objects.all())
rls_time = time.time() - start
self.assertEqual(len(docs), 500) # Half the documents
self.assertLess(rls_time, 0.1) # Should be fast
Testing Without PostgreSQL
For unit tests that don't need actual RLS:
from unittest.mock import patch
class MockRLSTest(TestCase):
databases = {'default'} # Use default test database
@patch('django_rls.db.functions.set_rls_context')
def test_middleware_sets_context(self, mock_set_context):
# Test that middleware calls set_rls_context
response = self.client.get('/')
mock_set_context.assert_called_with(
'user_id',
self.user.id
)
CI/CD Testing
# .github/workflows/test.yml
- name: Run tests with PostgreSQL
run: |
pytest --postgresql
env:
DATABASE_URL: postgresql://postgres:postgres@localhost/test_db
- name: Run unit tests with SQLite
run: |
pytest -m "not postgresql"
Best Practices
- Always set context before running queries in tests
- Test policy edge cases (empty results, no access, etc.)
- Use transactions to isolate test data
- Test with production-like data volumes
- Verify both positive and negative cases
- Test policy combinations if using multiple policies
Common Testing Patterns
Factory Pattern
import factory
from django_rls.test import RLSFactory
class DocumentFactory(RLSFactory):
class Meta:
model = Document
title = factory.Faker('sentence')
owner = factory.SubFactory(UserFactory)
@classmethod
def _create(cls, model_class, *args, **kwargs):
# Automatically set RLS context from owner
owner = kwargs.get('owner')
if owner:
set_rls_context('user_id', owner.id)
return super()._create(model_class, *args, **kwargs)
Pytest Fixtures
import pytest
from django_rls.db.functions import set_rls_context
@pytest.fixture
def user_context(user):
"""Set RLS context for a user."""
set_rls_context('user_id', user.id)
yield user
# Cleanup if needed
@pytest.mark.django_db
def test_document_list(user_context, client):
response = client.get('/documents/')
assert response.status_code == 200