Skip to main content
Version: 2.1

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​

  1. Always set context before running queries in tests
  2. Test policy edge cases (empty results, no access, etc.)
  3. Use transactions to isolate test data
  4. Test with production-like data volumes
  5. Verify both positive and negative cases
  6. 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