Skip to content

Commit 907ea84

Browse files
committed
Split up the enormous test suite a bit
1 parent c620c45 commit 907ea84

File tree

5 files changed

+622
-592
lines changed

5 files changed

+622
-592
lines changed

tests/testapp/test_applications.py

Lines changed: 399 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,399 @@
1+
import sys
2+
from types import SimpleNamespace
3+
4+
import django
5+
import pytest
6+
from django.core.checks import Error
7+
from django.core.exceptions import ValidationError
8+
from django.template import Context, Template, TemplateSyntaxError
9+
from django.test.utils import isolate_apps
10+
from django.urls import NoReverseMatch, reverse
11+
from django.utils.translation import deactivate_all, override
12+
from pytest_django.asserts import assertContains, assertRedirects
13+
14+
from feincms3 import applications
15+
from feincms3.applications import (
16+
ApplicationType,
17+
PageTypeMixin,
18+
_del_apps_urlconf_cache,
19+
apps_urlconf,
20+
reverse_app,
21+
)
22+
from feincms3.pages import AbstractPage
23+
from testapp.models import Article, Page
24+
from testapp.utils import override_urlconf
25+
26+
27+
@pytest.mark.django_db
28+
def test_apps(client):
29+
"""Article app test (two instance namespaces, two languages)"""
30+
home_de = Page.objects.create(
31+
title="home",
32+
slug="home",
33+
path="/de/",
34+
static_path=True,
35+
language_code="de",
36+
is_active=True,
37+
menu="main",
38+
)
39+
home_en = Page.objects.create(
40+
title="home",
41+
slug="home",
42+
path="/en/",
43+
static_path=True,
44+
language_code="en",
45+
is_active=True,
46+
menu="main",
47+
)
48+
49+
for root in (home_de, home_en):
50+
for app in ("blog", "publications"):
51+
Page.objects.create(
52+
title=app,
53+
slug=app,
54+
static_path=False,
55+
language_code=root.language_code,
56+
is_active=True,
57+
page_type=app,
58+
parent_id=root.pk,
59+
)
60+
61+
for i in range(7):
62+
for category in ("publications", "blog"):
63+
Article.objects.create(title=f"{category} {i}", category=category)
64+
65+
assertContains(client.get("/de/blog/all/"), 'class="article"', 7)
66+
assertContains(client.get("/de/blog/?page=2"), 'class="article"', 2)
67+
assertContains(
68+
client.get("/de/blog/?page=42"),
69+
'class="article"',
70+
2, # Last page with instances (2nd)
71+
)
72+
assertContains(
73+
client.get("/de/blog/?page=invalid"),
74+
'class="article"',
75+
5, # First page
76+
)
77+
78+
response = client.get("/de/blog/")
79+
assertContains(response, 'class="article"', 5)
80+
81+
response = client.get("/en/publications/")
82+
assertContains(response, 'class="article"', 5)
83+
84+
with override_urlconf(apps_urlconf()):
85+
article = Article.objects.order_by("pk").first()
86+
with override("de"):
87+
assert article.get_absolute_url() == f"/de/publications/{article.pk}/"
88+
89+
with override("en"):
90+
assert article.get_absolute_url() == f"/en/publications/{article.pk}/"
91+
92+
# The german URL is returned when specifying the ``languages``
93+
# list explicitly.
94+
assert (
95+
reverse_app(
96+
(article.category, "articles"),
97+
"article-detail",
98+
kwargs={"pk": article.pk},
99+
languages=["de", "en"],
100+
)
101+
== f"/de/publications/{article.pk}/"
102+
)
103+
104+
if django.VERSION >= (5, 2):
105+
# Forwarding query and fragment params to reverse() works
106+
assert (
107+
reverse_app(
108+
(article.category, "articles"),
109+
"article-detail",
110+
kwargs={"pk": article.pk},
111+
languages=["de", "en"],
112+
query={"a": 3},
113+
fragment="world",
114+
)
115+
== "/de/publications/%s/?a=3#world" % article.pk
116+
)
117+
118+
response = client.get(f"/de/publications/{article.pk}/")
119+
assertContains(response, "<h1>publications 0</h1>", 1)
120+
121+
# The exact value of course does not matter, just the fact that the
122+
# value does not change all the time.
123+
_del_apps_urlconf_cache()
124+
assert apps_urlconf() == "urlconf_fe9552a8363ece1f7fcf4970bf575a47"
125+
126+
updated = Page.objects.filter(page_type="blog").update(
127+
page_type="invalid", app_namespace="invalid"
128+
)
129+
assert updated == 2
130+
131+
_del_apps_urlconf_cache()
132+
assert apps_urlconf() == "urlconf_4bacbaf40cbe7e198373fd8d629e819c"
133+
134+
# Blog and publications
135+
assert (
136+
len(
137+
sys.modules["urlconf_fe9552a8363ece1f7fcf4970bf575a47"]
138+
.urlpatterns[0]
139+
.url_patterns
140+
)
141+
== 2
142+
)
143+
# Only publications, invalid apps are filtered out
144+
assert (
145+
len(
146+
sys.modules["urlconf_4bacbaf40cbe7e198373fd8d629e819c"]
147+
.urlpatterns[0]
148+
.url_patterns
149+
)
150+
== 1
151+
)
152+
153+
154+
@pytest.fixture
155+
def apps_validation_models():
156+
home = Page.objects.create(
157+
title="home",
158+
slug="home",
159+
path="/en/",
160+
static_path=True,
161+
language_code="en",
162+
is_active=True,
163+
menu="main",
164+
)
165+
blog = Page.objects.create(
166+
title="blog",
167+
slug="blog",
168+
language_code="en",
169+
is_active=True,
170+
menu="main",
171+
page_type="blog",
172+
parent=home,
173+
)
174+
return home, blog
175+
176+
177+
@pytest.mark.django_db
178+
def test_apps_duplicate(apps_validation_models):
179+
"""Test that apps cannot be added twice with the exact same configuration"""
180+
deactivate_all()
181+
182+
home, blog = apps_validation_models
183+
184+
home2 = Page.objects.create(
185+
title="home",
186+
slug="home",
187+
path="/en2/",
188+
static_path=True,
189+
language_code="en",
190+
is_active=True,
191+
menu="main",
192+
)
193+
blog2 = Page.objects.create(
194+
title="blog",
195+
slug="blog",
196+
language_code="en",
197+
is_active=True,
198+
menu="main",
199+
page_type="blog",
200+
parent=home2,
201+
)
202+
203+
with pytest.raises(ValidationError) as cm:
204+
blog2.full_clean()
205+
206+
assert cm.value.error_dict["page_type"][0].message == (
207+
'The page type "blog" with the specified configuration exists already.'
208+
)
209+
210+
211+
@pytest.mark.django_db
212+
def test_apps_required_fields():
213+
"""Apps can have required fields"""
214+
deactivate_all()
215+
216+
home = Page(
217+
title="home",
218+
slug="home",
219+
path="/en/",
220+
static_path=True,
221+
language_code="en",
222+
is_active=True,
223+
menu="main",
224+
page_type="importable_module",
225+
)
226+
with pytest.raises(ValidationError) as cm:
227+
home.full_clean(exclude=["not_editable"])
228+
229+
assert cm.value.error_dict["optional"][0].message == (
230+
'This field is required for the page type "importable_module".'
231+
)
232+
233+
home.optional = 1
234+
home.not_editable = 2
235+
home.full_clean(exclude=["not_editable"])
236+
237+
238+
@pytest.mark.django_db
239+
def test_apps_cloning_validation(admin_client, apps_validation_models):
240+
"""Checks that the target is properly validated when cloning"""
241+
deactivate_all()
242+
243+
home, blog = apps_validation_models
244+
245+
clone_url = reverse("admin:testapp_page_clone", args=(blog.pk,))
246+
247+
response = admin_client.get(clone_url)
248+
assertContains(response, "_set_content")
249+
assertContains(response, "set_page_type")
250+
251+
response = admin_client.post(clone_url, {"target": home.pk, "set_page_type": True})
252+
assertContains(
253+
response,
254+
"The page type &quot;blog&quot; with the specified configuration exists already.",
255+
)
256+
257+
# The other way round works
258+
clone_url = reverse("admin:testapp_page_clone", args=(home.pk,))
259+
260+
response = admin_client.post(clone_url, {"target": blog.pk, "set_page_type": True})
261+
assertRedirects(response, reverse("admin:testapp_page_change", args=(blog.pk,)))
262+
263+
# No apps in tree anymore
264+
assert Page.objects.filter(page_type="blog").count() == 0
265+
266+
267+
@pytest.mark.django_db
268+
def test_reverse_app_tag():
269+
"""Exercise the {% reverse_app %} template tag"""
270+
Page.objects.create(
271+
title="blog",
272+
slug="blog",
273+
static_path=False,
274+
language_code="en",
275+
is_active=True,
276+
page_type="blog",
277+
)
278+
279+
tests = [
280+
("{% reverse_app 'blog' 'article-detail' pk=42 %}", "/blog/42/", {}),
281+
(
282+
"{% reverse_app 'blog' 'article-detail' pk=42 fallback='/a/' %}",
283+
"/blog/42/",
284+
{},
285+
),
286+
(
287+
"{% reverse_app namespaces 'article-detail' pk=42 fallback='/a/' as a %}{{ a }}",
288+
"/blog/42/",
289+
{"namespaces": ["stuff", "blog"]},
290+
),
291+
("{% reverse_app 'bla' 'bla' fallback='/test/' %}", "/test/", {}),
292+
(
293+
"{% reverse_app 'bla' 'bla' fallback='/test/' as t %}{{ t }}",
294+
"/test/",
295+
{},
296+
),
297+
("{% reverse_app 'bla' 'bla' as t %}{{ t|default:'blub' }}", "blub", {}),
298+
]
299+
300+
with override_urlconf(apps_urlconf()):
301+
for tpl, out, ctx in tests:
302+
t = Template("{% load feincms3 %}" + tpl)
303+
assert t.render(Context(ctx)).strip() == out
304+
305+
with pytest.raises(NoReverseMatch):
306+
Template("{% load feincms3 %}{% reverse_app 'a' 'a' 42 %}").render(
307+
Context()
308+
)
309+
310+
311+
def test_reverse_app_failures():
312+
"""Invalid parameters to {% reverse_app %}"""
313+
with pytest.raises(TemplateSyntaxError) as cm:
314+
Template("{% load feincms3 %}{% reverse_app %}")
315+
assert str(cm.value) == (
316+
"'reverse_app' takes at least two arguments, a namespace and a URL pattern name."
317+
)
318+
319+
320+
def test_apps_urlconf_no_apps():
321+
"""apps_urlconf returns the ROOT_URLCONF when there are no apps at all"""
322+
assert apps_urlconf(apps=[]) == "testapp.urls"
323+
324+
325+
def test_application_type():
326+
"""Overriding ``app_namespace`` should be possible"""
327+
328+
with pytest.raises(TypeError):
329+
ApplicationType()
330+
331+
at = ApplicationType(
332+
key="test",
333+
title="test",
334+
urlconf="test",
335+
app_namespace=lambda page: f"{page.page_type}-{page.category_id}",
336+
)
337+
338+
assert (
339+
at.app_namespace(SimpleNamespace(page_type="blog", category_id=3)) == "blog-3"
340+
)
341+
342+
at = ApplicationType(
343+
key="test",
344+
title="test",
345+
urlconf="test",
346+
)
347+
assert at.app_namespace(SimpleNamespace(page_type="blog", category_id=3)) == "blog"
348+
349+
350+
@isolate_apps("testapp")
351+
def test_importable_page_types():
352+
"""Applications require an importable URLconf module"""
353+
354+
apps_model = applications._APPS_MODEL
355+
try:
356+
357+
class Page(AbstractPage, PageTypeMixin):
358+
TYPES = [ApplicationType(key="app", title="app", urlconf="does-not-exist")]
359+
360+
errors = Page.check()
361+
expected = [
362+
Error(
363+
"The application type 'app' has an unimportable"
364+
" URLconf value 'does-not-exist': No module named 'does-not-exist'",
365+
obj=Page,
366+
id="feincms3.E003",
367+
),
368+
]
369+
assert errors == expected
370+
371+
finally:
372+
applications._APPS_MODEL = apps_model
373+
374+
375+
@isolate_apps("testapp")
376+
def test_unique_page_types():
377+
"""Page types must have unique keys"""
378+
379+
apps_model = applications._APPS_MODEL
380+
try:
381+
382+
class Page(AbstractPage, PageTypeMixin):
383+
TYPES = [
384+
ApplicationType(key="app", title="a", urlconf="importable_module"),
385+
ApplicationType(key="app", title="a", urlconf="importable_module"),
386+
]
387+
388+
errors = Page.check()
389+
expected = [
390+
Error(
391+
"Page type keys are used more than once: app.",
392+
obj=Page,
393+
id="feincms3.E006",
394+
),
395+
]
396+
assert errors == expected
397+
398+
finally:
399+
applications._APPS_MODEL = apps_model

0 commit comments

Comments
 (0)