|
4ce269c…
|
ragelink
|
1 |
import pytest |
|
4ce269c…
|
ragelink
|
2 |
from django.contrib.auth.models import User |
|
4ce269c…
|
ragelink
|
3 |
from django.test import TestCase |
|
4ce269c…
|
ragelink
|
4 |
from django.urls import reverse |
|
4ce269c…
|
ragelink
|
5 |
|
|
4ce269c…
|
ragelink
|
6 |
from .permissions import P |
|
4ce269c…
|
ragelink
|
7 |
|
|
4ce269c…
|
ragelink
|
8 |
|
|
4ce269c…
|
ragelink
|
9 |
class TrackingModelTest(TestCase): |
|
4ce269c…
|
ragelink
|
10 |
"""Test the Tracking abstract model via a concrete model that uses it.""" |
|
4ce269c…
|
ragelink
|
11 |
|
|
4ce269c…
|
ragelink
|
12 |
def setUp(self): |
|
c588255…
|
ragelink
|
13 |
from organization.models import Organization |
|
c588255…
|
ragelink
|
14 |
from projects.models import Project |
|
4ce269c…
|
ragelink
|
15 |
|
|
4ce269c…
|
ragelink
|
16 |
self.user = User.objects.create_superuser(username="test", password="x") |
|
c588255…
|
ragelink
|
17 |
self.org = Organization.objects.create(name="Test Org", created_by=self.user) |
|
c588255…
|
ragelink
|
18 |
self.project = Project.objects.create(name="Test Project", organization=self.org, created_by=self.user) |
|
4ce269c…
|
ragelink
|
19 |
|
|
4ce269c…
|
ragelink
|
20 |
def test_version_increments_on_save(self): |
|
c588255…
|
ragelink
|
21 |
initial_version = self.project.version |
|
c588255…
|
ragelink
|
22 |
self.project.name = "Updated Project" |
|
c588255…
|
ragelink
|
23 |
self.project.save() |
|
c588255…
|
ragelink
|
24 |
self.project.refresh_from_db() |
|
c588255…
|
ragelink
|
25 |
self.assertEqual(self.project.version, initial_version + 1) |
|
4ce269c…
|
ragelink
|
26 |
|
|
4ce269c…
|
ragelink
|
27 |
def test_soft_delete_sets_deleted_at(self): |
|
c588255…
|
ragelink
|
28 |
self.project.soft_delete(user=self.user) |
|
c588255…
|
ragelink
|
29 |
self.project.refresh_from_db() |
|
c588255…
|
ragelink
|
30 |
self.assertIsNotNone(self.project.deleted_at) |
|
c588255…
|
ragelink
|
31 |
self.assertEqual(self.project.deleted_by, self.user) |
|
c588255…
|
ragelink
|
32 |
self.assertTrue(self.project.is_deleted) |
|
4ce269c…
|
ragelink
|
33 |
|
|
4ce269c…
|
ragelink
|
34 |
def test_created_at_auto_set(self): |
|
c588255…
|
ragelink
|
35 |
self.assertIsNotNone(self.project.created_at) |
|
4ce269c…
|
ragelink
|
36 |
|
|
4ce269c…
|
ragelink
|
37 |
def test_updated_at_auto_set(self): |
|
c588255…
|
ragelink
|
38 |
self.assertIsNotNone(self.project.updated_at) |
|
4ce269c…
|
ragelink
|
39 |
|
|
4ce269c…
|
ragelink
|
40 |
|
|
4ce269c…
|
ragelink
|
41 |
class BaseCoreModelTest(TestCase): |
|
4ce269c…
|
ragelink
|
42 |
"""Test BaseCoreModel slug generation and UUID.""" |
|
4ce269c…
|
ragelink
|
43 |
|
|
4ce269c…
|
ragelink
|
44 |
def setUp(self): |
|
c588255…
|
ragelink
|
45 |
from organization.models import Organization |
|
c588255…
|
ragelink
|
46 |
from projects.models import Project |
|
4ce269c…
|
ragelink
|
47 |
|
|
4ce269c…
|
ragelink
|
48 |
self.user = User.objects.create_superuser(username="test", password="x") |
|
c588255…
|
ragelink
|
49 |
self.org = Organization.objects.create(name="Test Org", created_by=self.user) |
|
c588255…
|
ragelink
|
50 |
self.project = Project.objects.create(name="My Project", organization=self.org, created_by=self.user) |
|
4ce269c…
|
ragelink
|
51 |
|
|
4ce269c…
|
ragelink
|
52 |
def test_slug_auto_generated(self): |
|
c588255…
|
ragelink
|
53 |
self.assertEqual(self.project.slug, "my-project") |
|
4ce269c…
|
ragelink
|
54 |
|
|
4ce269c…
|
ragelink
|
55 |
def test_guid_is_uuid(self): |
|
4ce269c…
|
ragelink
|
56 |
import uuid |
|
4ce269c…
|
ragelink
|
57 |
|
|
c588255…
|
ragelink
|
58 |
self.assertIsInstance(self.project.guid, uuid.UUID) |
|
4ce269c…
|
ragelink
|
59 |
|
|
4ce269c…
|
ragelink
|
60 |
def test_slug_uniqueness(self): |
|
c588255…
|
ragelink
|
61 |
from projects.models import Project |
|
4ce269c…
|
ragelink
|
62 |
|
|
c588255…
|
ragelink
|
63 |
p2 = Project.objects.create(name="My Project", organization=self.org, created_by=self.user) |
|
c588255…
|
ragelink
|
64 |
self.assertNotEqual(self.project.slug, p2.slug) |
|
c588255…
|
ragelink
|
65 |
self.assertTrue(p2.slug.startswith("my-project")) |
|
4ce269c…
|
ragelink
|
66 |
|
|
4ce269c…
|
ragelink
|
67 |
def test_str_returns_name(self): |
|
c588255…
|
ragelink
|
68 |
self.assertEqual(str(self.project), "My Project") |
|
4ce269c…
|
ragelink
|
69 |
|
|
4ce269c…
|
ragelink
|
70 |
|
|
4ce269c…
|
ragelink
|
71 |
class PermissionsTest(TestCase): |
|
4ce269c…
|
ragelink
|
72 |
"""Test the P permission enum.""" |
|
4ce269c…
|
ragelink
|
73 |
|
|
4ce269c…
|
ragelink
|
74 |
def setUp(self): |
|
4ce269c…
|
ragelink
|
75 |
self.superuser = User.objects.create_superuser(username="super", password="x") |
|
4ce269c…
|
ragelink
|
76 |
self.regular = User.objects.create_user(username="regular", password="x") |
|
4ce269c…
|
ragelink
|
77 |
|
|
4ce269c…
|
ragelink
|
78 |
def test_superuser_passes_all_checks(self): |
|
c588255…
|
ragelink
|
79 |
self.assertTrue(P.PROJECT_VIEW.check(self.superuser)) |
|
c588255…
|
ragelink
|
80 |
self.assertTrue(P.PROJECT_ADD.check(self.superuser)) |
|
4ce269c…
|
ragelink
|
81 |
|
|
4ce269c…
|
ragelink
|
82 |
def test_regular_user_without_perm_denied(self): |
|
4ce269c…
|
ragelink
|
83 |
from django.core.exceptions import PermissionDenied |
|
4ce269c…
|
ragelink
|
84 |
|
|
4ce269c…
|
ragelink
|
85 |
with self.assertRaises(PermissionDenied): |
|
c588255…
|
ragelink
|
86 |
P.PROJECT_ADD.check(self.regular) |
|
4ce269c…
|
ragelink
|
87 |
|
|
4ce269c…
|
ragelink
|
88 |
def test_regular_user_without_perm_returns_false(self): |
|
c588255…
|
ragelink
|
89 |
self.assertFalse(P.PROJECT_ADD.check(self.regular, raise_error=False)) |
|
4ce269c…
|
ragelink
|
90 |
|
|
4ce269c…
|
ragelink
|
91 |
def test_unauthenticated_user_denied(self): |
|
4ce269c…
|
ragelink
|
92 |
from django.contrib.auth.models import AnonymousUser |
|
4ce269c…
|
ragelink
|
93 |
from django.core.exceptions import PermissionDenied |
|
4ce269c…
|
ragelink
|
94 |
|
|
4ce269c…
|
ragelink
|
95 |
with self.assertRaises(PermissionDenied): |
|
c588255…
|
ragelink
|
96 |
P.PROJECT_VIEW.check(AnonymousUser()) |
|
4ce269c…
|
ragelink
|
97 |
|
|
4ce269c…
|
ragelink
|
98 |
|
|
4ce269c…
|
ragelink
|
99 |
@pytest.mark.django_db |
|
4ce269c…
|
ragelink
|
100 |
class TestDashboard: |
|
4ce269c…
|
ragelink
|
101 |
def test_dashboard_requires_login(self, client): |
|
4ce269c…
|
ragelink
|
102 |
response = client.get(reverse("dashboard")) |
|
4ce269c…
|
ragelink
|
103 |
assert response.status_code == 302 |
|
4ce269c…
|
ragelink
|
104 |
assert "/auth/login/" in response.url |
|
4ce269c…
|
ragelink
|
105 |
|
|
4ce269c…
|
ragelink
|
106 |
def test_dashboard_renders_for_authenticated_user(self, admin_client): |
|
4ce269c…
|
ragelink
|
107 |
response = admin_client.get(reverse("dashboard")) |
|
4ce269c…
|
ragelink
|
108 |
assert response.status_code == 200 |
|
4ce269c…
|
ragelink
|
109 |
assert b"Dashboard" in response.content |
|
4ce269c…
|
ragelink
|
110 |
|
|
4ce269c…
|
ragelink
|
111 |
|
|
4ce269c…
|
ragelink
|
112 |
@pytest.mark.django_db |
|
4ce269c…
|
ragelink
|
113 |
class TestHealthCheck: |
|
4ce269c…
|
ragelink
|
114 |
def test_health_returns_ok(self, client): |
|
4ce269c…
|
ragelink
|
115 |
response = client.get(reverse("health")) |
|
4ce269c…
|
ragelink
|
116 |
assert response.status_code == 200 |
|
4ce269c…
|
ragelink
|
117 |
data = response.json() |
|
4ce269c…
|
ragelink
|
118 |
assert data["status"] == "ok" |
|
4ce269c…
|
ragelink
|
119 |
assert data["service"] == "fossilrepo-django-htmx" |
|
4ce269c…
|
ragelink
|
120 |
assert "version" in data |
|
4ce269c…
|
ragelink
|
121 |
assert "uptime" in data |
|
4ce269c…
|
ragelink
|
122 |
assert "timestamp" in data |
|
4ce269c…
|
ragelink
|
123 |
assert data["checks"]["database"] == "ok" |
|
4ce269c…
|
ragelink
|
124 |
assert data["links"]["status"] == "/status/" |
|
4ce269c…
|
ragelink
|
125 |
|
|
4ce269c…
|
ragelink
|
126 |
|
|
4ce269c…
|
ragelink
|
127 |
@pytest.mark.django_db |
|
4ce269c…
|
ragelink
|
128 |
class TestStatusPage: |
|
4ce269c…
|
ragelink
|
129 |
def test_status_page_accessible_unauthenticated(self, client): |
|
4ce269c…
|
ragelink
|
130 |
response = client.get(reverse("status")) |
|
4ce269c…
|
ragelink
|
131 |
assert response.status_code == 200 |
|
4ce269c…
|
ragelink
|
132 |
assert b"Fossilrepo" in response.content |
|
4ce269c…
|
ragelink
|
133 |
assert b"Server-rendered Django + HTMX." in response.content |
|
4ce269c…
|
ragelink
|
134 |
|
|
4ce269c…
|
ragelink
|
135 |
def test_status_page_contains_links(self, client): |
|
4ce269c…
|
ragelink
|
136 |
response = client.get(reverse("status")) |
|
4ce269c…
|
ragelink
|
137 |
content = response.content.decode() |
|
4ce269c…
|
ragelink
|
138 |
assert "/dashboard/" in content |
|
4ce269c…
|
ragelink
|
139 |
assert "/admin/" in content |
|
4ce269c…
|
ragelink
|
140 |
assert "/health/" in content |
|
4ce269c…
|
ragelink
|
141 |
assert "/auth/login/" in content |
|
4ce269c…
|
ragelink
|
142 |
|
|
4ce269c…
|
ragelink
|
143 |
def test_status_page_contains_meta(self, client): |
|
4ce269c…
|
ragelink
|
144 |
response = client.get(reverse("status")) |
|
4ce269c…
|
ragelink
|
145 |
content = response.content.decode() |
|
4ce269c…
|
ragelink
|
146 |
assert "fossilrepo-django-htmx" in content |
|
4ce269c…
|
ragelink
|
147 |
assert "All systems operational" in content |