diff --git a/django/meleager/fixtures/README.md b/django/meleager/fixtures/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..7521eaefdee23f93c20b55add47f279bef96333f
--- /dev/null
+++ b/django/meleager/fixtures/README.md
@@ -0,0 +1,7 @@
+## `test_data.json`
+
+* Work: no parameters
+* Book: number 12
+* Passage
+  * fragment 42, no sub_fragment ('')
+  * fragment 12, sub_fragment abc
\ No newline at end of file
diff --git a/django/meleager/fixtures/test_data.json b/django/meleager/fixtures/test_data.json
new file mode 100644
index 0000000000000000000000000000000000000000..43c751c1dc69b128ae4edde0dd53af8269f569c3
--- /dev/null
+++ b/django/meleager/fixtures/test_data.json
@@ -0,0 +1 @@
+[{"model": "meleager.work", "pk": 1, "fields": {"created_at": "2020-11-14T02:33:59.818Z", "updated_at": "2020-11-14T02:33:59.818Z", "creator": null, "last_editor": null, "urn": null, "descriptions": [], "alternative_urns": [], "names": []}}, {"model": "meleager.book", "fields": {"created_at": "2020-11-14T02:34:10.052Z", "updated_at": "2020-11-14T02:34:10.052Z", "creator": null, "last_editor": null, "work": 1, "number": 12, "descriptions": []}}, {"model": "meleager.passage", "fields": {"created_at": "2020-11-14T02:34:28.654Z", "updated_at": "2020-11-14T02:34:28.654Z", "creator": null, "last_editor": null, "urn": null, "validation": 0, "city": null, "book": [12, 1], "fragment": 42, "sub_fragment": "", "descriptions": [], "alternative_urns": [], "authors": [], "images": [], "comments": [], "external_references": [], "internal_references": [], "manuscripts": [], "keywords": []}}, {"model": "meleager.passage", "fields": {"created_at": "2020-11-14T02:34:49.041Z", "updated_at": "2020-11-14T02:34:49.041Z", "creator": null, "last_editor": null, "urn": null, "validation": 0, "city": null, "book": [12, 1], "fragment": 12, "sub_fragment": "abc", "descriptions": [], "alternative_urns": [], "authors": [], "images": [], "comments": [], "external_references": [], "internal_references": [], "manuscripts": [], "keywords": []}}]
\ No newline at end of file
diff --git a/django/meleager/models/book.py b/django/meleager/models/book.py
index 7583c2978c03277e37f5aa4882a1e60dc2095342..b3927ab0eaf13630a6414662697b32ea8484e957 100644
--- a/django/meleager/models/book.py
+++ b/django/meleager/models/book.py
@@ -3,16 +3,27 @@
 Mahuf.
 
 """
+from django.apps import apps
 from django.db import models
 
 from .mixins import DescriptableResourceMixin, EditableResourceMixin
 
+class BookManager(models.Manager):
+    def get_by_natural_key(self, number, work_pk):
+        work = apps.get_model("meleager", "Work").objects.get(pk=work_pk)
+        return self.get(number=number, work=work)
 
 class Book(DescriptableResourceMixin, EditableResourceMixin, models.Model):
     """ Book model """
 
+    objects = BookManager()
+
     work = models.ForeignKey("meleager.Work", on_delete=models.CASCADE)
     number = models.IntegerField("Book number", null=False, db_index=True)
 
+    def natural_key(self):
+        return (self.number, self.work.pk)
+    natural_key.dependencies = ['meleager.Work']
+
     def __str__(self):
         return f"Book {self.number}"
diff --git a/django/meleager/models/passage.py b/django/meleager/models/passage.py
index d41dd1a315612808b731901a1dfbd164e0491fc5..2f2f71f572ed2f36af511ecdf52223c0c230c2e7 100644
--- a/django/meleager/models/passage.py
+++ b/django/meleager/models/passage.py
@@ -1,6 +1,7 @@
 import re
 from urllib.parse import urlparse
 
+from django.apps import apps
 from django.conf import settings
 from django.db import models
 from django.http import HttpResponseBadRequest
@@ -14,6 +15,10 @@ from .mixins import (
 )
 from .urn import URN
 
+class PassageManager(models.Manager):
+    def get_by_natural_key(self, fragment, sub_fragment, *book_natural_key):
+        book = apps.get_model("meleager", "Book").objects.get_by_natural_key(*book_natural_key)
+        return self.get(fragment=fragment, sub_fragment=sub_fragment, book=book)
 
 class Passage(
     EditableResourceMixin,
@@ -24,6 +29,8 @@ class Passage(
 ):
     """ Passage model """
 
+    objects = PassageManager()
+
     authors = models.ManyToManyField("meleager.Author")
     city = models.ForeignKey(
         "meleager.City",
@@ -81,5 +88,9 @@ class Passage(
         help_text="The keywords that tag this resource.",
     )
 
+    def natural_key(self):
+        return (self.fragment, self.sub_fragment) + self.book.natural_key()
+    natural_key.dependencies = ['meleager.Work', 'meleager.Book']
+
     def __str__(self):
         return f"Passage {self.book.number}.{self.fragment}{self.sub_fragment}"
diff --git a/django/meleager/models/work.py b/django/meleager/models/work.py
index 2fc384a684f97c80e90709c296be358a34957455..be9b439a8d8b8fc3a02750f34abe4345fd9503fa 100644
--- a/django/meleager/models/work.py
+++ b/django/meleager/models/work.py
@@ -6,13 +6,13 @@ from .mixins import (
     AlternativeURNResourceMixin,
 )
 
-
 class Work(
     EditableResourceMixin,
     DescriptableResourceMixin,
     AlternativeURNResourceMixin,
     models.Model,
 ):
+
     names = models.ManyToManyField(
         "meleager.Name",
         related_name="mlgr_works",
diff --git a/django/user/fixtures/test_admin.json b/django/user/fixtures/test_admin.json
new file mode 100644
index 0000000000000000000000000000000000000000..97df4b486f2fa0b7d65208a55bff44dfc3fd8ae4
--- /dev/null
+++ b/django/user/fixtures/test_admin.json
@@ -0,0 +1 @@
+[{"model": "user.user", "pk": 1, "fields": {"password": "!xeTTOcyuHKdRJyOPkNaiXosuRkntzKOZKV26QBVo", "last_login": null, "is_superuser": false, "username": "AnonymousUser", "first_name": "", "last_name": "", "email": "", "is_staff": false, "is_active": true, "date_joined": "2020-11-14T02:31:42.180Z", "institution": "", "groups": [], "user_permissions": []}}, {"model": "user.user", "pk": 2, "fields": {"password": "pbkdf2_sha256$216000$1NDKDHZcziZa$sxZD899PegMg0IOiWOFso+F+gmSF5RXBPOkyrC6OvZ4=", "last_login": null, "is_superuser": true, "username": "test_admin", "first_name": "", "last_name": "", "email": "admin@admin.com", "is_staff": true, "is_active": true, "date_joined": "2020-11-14T02:32:13.175Z", "institution": "", "groups": [], "user_permissions": []}}]
\ No newline at end of file
diff --git a/django/user/models.py b/django/user/models.py
index 16023ce7ead9ba197d9ba0e0ee75e2ac3804d553..b3cf202999f34ed3cd1e271ba2e49c50eaf1a2ae 100644
--- a/django/user/models.py
+++ b/django/user/models.py
@@ -4,3 +4,6 @@ from django.db import models
 
 class User(AbstractUser):
     institution = models.TextField(help_text="The institution this user belongs to.")
+
+    def natural_key(self):
+        return (self.username,)
diff --git a/django/web/tests/test_add_comment_passage.py b/django/web/tests/test_add_comment_passage.py
index 3656f5b36728535dae253327372e0b5f56d5a05f..f84f0e8bc3f152c58c908805a8b0c8debdb69316 100644
--- a/django/web/tests/test_add_comment_passage.py
+++ b/django/web/tests/test_add_comment_passage.py
@@ -1,4 +1,5 @@
 from django.test import TestCase
+from django.urls import reverse
 
 from meleager.models import Work, Book, Passage, Description
 from meleager.models import Comment
@@ -8,14 +9,15 @@ from web.forms.description import DescriptionForm
 
 
 class AddCommentFormPassage(TestCase):
+    fixtures = ['test_data.json']
+
     def setUp(self):
-        self.work = Work.objects.create()
-        self.book = Book.objects.create(work=self.work, number=12)
-        self.passage = Passage.objects.create(book=self.book, fragment=42)
+        book = Book.objects.first()
+        self.passage = Passage.objects.get(book=book, fragment=42)
 
     def test_form(self):
         response = self.client.post(
-            f"/comments/add/passage/{self.passage.pk}",
+            reverse("web:comment-add", args=(self.passage.pk,)),
             data={
                 "comment_title": "TEST COMMENT",
                 "description": "TEST DESCRIPTION",
diff --git a/django/web/tests/test_passage.py b/django/web/tests/test_passage.py
new file mode 100644
index 0000000000000000000000000000000000000000..4a15bbcb40fbce9785a812d7e557da7838fd61b7
--- /dev/null
+++ b/django/web/tests/test_passage.py
@@ -0,0 +1,22 @@
+from django.test import TestCase
+
+from meleager.models import Book, Work
+from web.models import APPassage, APScholium
+
+
+class TestURNPassage(TestCase):
+    fixtures = ["test_data.json"]
+
+    def test_homepage_ok(self):
+        resp = self.client.get("/")
+        self.assertEqual(resp.status_code, 200)
+        self.assertContains(resp, "12.42")
+        self.assertContains(resp, "12.12abc")
+
+    def test_passage1_ok(self):
+        resp = self.client.get("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:12.42")
+        self.assertEqual(resp.status_code, 200)
+
+    def test_passage2_ok(self):
+        resp = self.client.get("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:12.12abc")
+        self.assertEqual(resp.status_code, 200)
diff --git a/django/web/tests/test_urn_reverse.py b/django/web/tests/test_urn_reverse.py
deleted file mode 100644
index 8bbaeda8f1019a713733a2b319b721db407caf1c..0000000000000000000000000000000000000000
--- a/django/web/tests/test_urn_reverse.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from django.test import TestCase
-
-from meleager.models import Book, Work
-from web.models import APPassage, APScholium
-
-
-class TestURNPassage(TestCase):
-    def setUp(self):
-        self.work = Work.objects.create()
-        self.book = Book.objects.create(work=self.work, number=12)
-        self.passage = APPassage.objects.create(book=self.book, fragment=42)
-
-    def test_homepage_ok(self):
-        resp = self.client.get("/")
-        self.assertEqual(resp.status_code, 200)
-        self.assertContains(resp, "12.42")
-
-    def test_passage_ok(self):
-        resp = self.client.get("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:12.42")
-        self.assertEqual(resp.status_code, 200)
-
-    def test_urn_value_passage(self):
-        self.assertEquals(
-            self.passage.urn_value, "urn:cts:greekLit:tlg7000.tlg001.ag:12.42"
-        )
-
-        self.passage.sub_fragment = "b"
-        self.assertEquals(
-            self.passage.urn_value, "urn:cts:greekLit:tlg7000.tlg001.ag:12.42b"
-        )