Skip to main content

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

  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