Testing
Testing RLS-enabled models requires real PostgreSQL. Policies are enforced at the database level — SQLite and mocked connections do not exercise RLS.
Running the test suite
make test # starts docker compose Postgres, runs full suite
make test-security # security regression tests only
make test-cov # with coverage
See Local Testing for scripts/run-tests.sh, environment variables, and troubleshooting.
Basic testing setup (1.0.0+)
Use system_rls_context() when switching identity. Identity keys are immutable once set unless you use a privileged scope.
from django.test import TestCase
from django.contrib.auth.models import User
from django_rls.context import system_rls_context, set_rls_context
from myapp.models import Document
class RLSTestCase(TestCase):
def setUp(self):
self.user1 = User.objects.create_user('user1')
self.user2 = User.objects.create_user('user2')
with system_rls_context(user_id=self.user1.id):
self.doc1 = Document.objects.create(
title='User 1 Doc',
owner=self.user1
)
with system_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):
with system_rls_context(user_id=self.user1.id):
docs = Document.objects.all()
self.assertEqual(docs.count(), 1)
self.assertEqual(docs.first().owner, self.user1)
with system_rls_context(user_id=self.user2.id):
docs = Document.objects.all()
self.assertEqual(docs.count(), 1)
self.assertEqual(docs.first().owner, self.user2)
def test_user_cannot_update_others_documents(self):
with system_rls_context(user_id=self.user1.id):
updated = Document.objects.filter(id=self.doc2.id).update(title='Hacked!')
self.assertEqual(updated, 0)
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
Security regression tests
The tests/security/ package runs against live PostgreSQL and verifies SQL injection resistance, identity immutability, middleware trust boundaries, and connection hygiene. Run via:
make test-security
Prefer asserting get_rls_context() round-trips over mocking django_rls.context.connection.
CI/CD testing
GitHub Actions runs the full pytest tree against PostgreSQL 17 on every PR:
- name: Run tests with PostgreSQL
run: poetry run pytest -xvs --cov=django_rls
env:
USE_POSTGRESQL: "true"
DB_HOST: localhost
DB_PORT: 5432
DB_USER: rls_test_user
DB_PASSWORD: testpass
Locally, make test mirrors this using docker compose on port 5433.
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