initial commit
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
import io
|
||||
import zipfile
|
||||
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import Client, TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
from PIL import Image
|
||||
|
||||
from apps.pages.contact_uploads import (
|
||||
MAX_ATTACHMENTS,
|
||||
MAX_FILE_SIZE,
|
||||
validate_contact_attachments,
|
||||
)
|
||||
from apps.pages.models import ContactSubmission, ContactSubmissionAttachment
|
||||
|
||||
|
||||
def _png_file(name="screenshot.png"):
|
||||
buffer = io.BytesIO()
|
||||
Image.new("RGB", (8, 8), color="red").save(buffer, format="PNG")
|
||||
buffer.seek(0)
|
||||
return SimpleUploadedFile(name, buffer.read(), content_type="image/png")
|
||||
|
||||
|
||||
def _zip_file(name="logs.zip", entries=None):
|
||||
buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive:
|
||||
for entry_name, content in (entries or {"app.log": "line one\n"}).items():
|
||||
archive.writestr(entry_name, content)
|
||||
buffer.seek(0)
|
||||
return SimpleUploadedFile(name, buffer.read(), content_type="application/zip")
|
||||
|
||||
|
||||
class ContactUploadValidationTest(TestCase):
|
||||
def test_accepts_valid_png(self):
|
||||
validated = validate_contact_attachments([_png_file()])
|
||||
self.assertEqual(len(validated), 1)
|
||||
self.assertEqual(validated[0][1], "screenshot.png")
|
||||
|
||||
def test_accepts_valid_zip(self):
|
||||
validated = validate_contact_attachments([_zip_file()])
|
||||
self.assertEqual(len(validated), 1)
|
||||
|
||||
def test_accepts_valid_text_log(self):
|
||||
uploaded = SimpleUploadedFile(
|
||||
"error.log",
|
||||
b"2026-06-07 ERROR something failed\n",
|
||||
content_type="text/plain",
|
||||
)
|
||||
validated = validate_contact_attachments([uploaded])
|
||||
self.assertEqual(validated[0][1], "error.log")
|
||||
|
||||
def test_rejects_executable_extension(self):
|
||||
uploaded = SimpleUploadedFile(
|
||||
"malware.exe",
|
||||
b"MZfake",
|
||||
content_type="application/octet-stream",
|
||||
)
|
||||
with self.assertRaises(Exception):
|
||||
validate_contact_attachments([uploaded])
|
||||
|
||||
def test_rejects_php_disguised_as_png(self):
|
||||
uploaded = SimpleUploadedFile(
|
||||
"image.png",
|
||||
b"<?php echo 'bad'; ?>",
|
||||
content_type="image/png",
|
||||
)
|
||||
with self.assertRaises(Exception):
|
||||
validate_contact_attachments([uploaded])
|
||||
|
||||
def test_rejects_oversized_file(self):
|
||||
uploaded = SimpleUploadedFile(
|
||||
"big.log",
|
||||
b"x" * (MAX_FILE_SIZE + 1),
|
||||
content_type="text/plain",
|
||||
)
|
||||
with self.assertRaises(Exception):
|
||||
validate_contact_attachments([uploaded])
|
||||
|
||||
def test_rejects_too_many_files(self):
|
||||
files = [_png_file(f"shot-{index}.png") for index in range(MAX_ATTACHMENTS + 1)]
|
||||
with self.assertRaises(Exception):
|
||||
validate_contact_attachments(files)
|
||||
|
||||
def test_rejects_zip_with_path_traversal(self):
|
||||
buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(buffer, "w") as archive:
|
||||
archive.writestr("../escape.txt", "bad")
|
||||
buffer.seek(0)
|
||||
uploaded = SimpleUploadedFile(
|
||||
"bad.zip",
|
||||
buffer.read(),
|
||||
content_type="application/zip",
|
||||
)
|
||||
with self.assertRaises(Exception):
|
||||
validate_contact_attachments([uploaded])
|
||||
|
||||
|
||||
@override_settings(
|
||||
CONTACT_UPLOAD_ROOT=__import__("pathlib").Path(__file__).resolve().parents[3]
|
||||
/ "test_private_uploads"
|
||||
)
|
||||
class ContactViewUploadTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client(enforce_csrf_checks=True)
|
||||
self.url = reverse("pages:contact")
|
||||
|
||||
def _start_session(self):
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.csrf_token = response.cookies["csrftoken"].value
|
||||
captcha_question = response.context["captcha_question"]
|
||||
left, right = captcha_question.split(" + ")
|
||||
return int(left) + int(right)
|
||||
|
||||
def _post_contact(self, captcha_answer, attachments=None, extra=None):
|
||||
payload = {
|
||||
"name": "Test User",
|
||||
"title": "Upload test",
|
||||
"description": "Testing attachments",
|
||||
"email": "test@example.com",
|
||||
"captcha_answer": captcha_answer,
|
||||
}
|
||||
if attachments is not None:
|
||||
payload["attachments"] = attachments
|
||||
if extra:
|
||||
payload.update(extra)
|
||||
return self.client.post(
|
||||
self.url,
|
||||
data=payload,
|
||||
HTTP_X_CSRFTOKEN=self.csrf_token,
|
||||
)
|
||||
|
||||
def test_contact_submission_with_png_attachment(self):
|
||||
captcha_answer = self._start_session()
|
||||
response = self._post_contact(captcha_answer, attachments=_png_file())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(response.json()["success"])
|
||||
submission = ContactSubmission.objects.get(title="Upload test")
|
||||
self.assertEqual(submission.attachments.count(), 1)
|
||||
attachment = submission.attachments.first()
|
||||
self.assertEqual(attachment.original_filename, "screenshot.png")
|
||||
self.assertTrue(attachment.file.storage.exists(attachment.file.name))
|
||||
|
||||
def test_contact_submission_rejects_invalid_attachment(self):
|
||||
captcha_answer = self._start_session()
|
||||
response = self._post_contact(
|
||||
captcha_answer,
|
||||
attachments=SimpleUploadedFile(
|
||||
"bad.exe",
|
||||
b"MZ",
|
||||
content_type="application/octet-stream",
|
||||
),
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertFalse(response.json()["success"])
|
||||
self.assertIn("attachments", response.json()["errors"])
|
||||
self.assertEqual(ContactSubmission.objects.count(), 0)
|
||||
self.assertEqual(ContactSubmissionAttachment.objects.count(), 0)
|
||||
@@ -0,0 +1,94 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.pages.models import CustomPage, CustomPageSection, CustomPageSectionItem
|
||||
|
||||
|
||||
class CustomPageViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.page = CustomPage.objects.create(
|
||||
title="Resources",
|
||||
slug="resources",
|
||||
show_in_nav=True,
|
||||
menu_order=5,
|
||||
is_published=True,
|
||||
)
|
||||
CustomPageSection.objects.create(
|
||||
page=self.page,
|
||||
section_type=CustomPageSection.TYPE_HERO,
|
||||
title="Resources",
|
||||
badge="Docs",
|
||||
is_active=True,
|
||||
)
|
||||
CustomPage.objects.create(
|
||||
title="Draft Page",
|
||||
slug="draft",
|
||||
is_published=False,
|
||||
)
|
||||
|
||||
def test_published_page_returns_200(self):
|
||||
response = self.client.get(reverse("pages:custom_page", kwargs={"slug": "resources"}))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, "pages/custom_page.html")
|
||||
self.assertEqual(response.context["custom_page"], self.page)
|
||||
|
||||
def test_unpublished_page_returns_404(self):
|
||||
response = self.client.get(reverse("pages:custom_page", kwargs={"slug": "draft"}))
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_unknown_slug_returns_404(self):
|
||||
response = self.client.get(reverse("pages:custom_page", kwargs={"slug": "missing"}))
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_section_item_with_url_renders_as_link(self):
|
||||
section = CustomPageSection.objects.create(
|
||||
page=self.page,
|
||||
section_type=CustomPageSection.TYPE_FEATURES,
|
||||
title="Highlights",
|
||||
is_active=True,
|
||||
)
|
||||
CustomPageSectionItem.objects.create(
|
||||
page=self.page,
|
||||
section=section,
|
||||
title="Documentation",
|
||||
content="Read the docs.",
|
||||
url="/about/",
|
||||
order=1,
|
||||
)
|
||||
response = self.client.get(reverse("pages:custom_page", kwargs={"slug": "resources"}))
|
||||
self.assertContains(response, 'href="/about/"')
|
||||
self.assertContains(response, "section-item-link")
|
||||
self.assertContains(response, "Documentation")
|
||||
|
||||
|
||||
class CustomPageNavTest(TestCase):
|
||||
def test_nav_custom_pages_in_context(self):
|
||||
CustomPage.objects.create(
|
||||
title="Visible",
|
||||
slug="visible",
|
||||
show_in_nav=True,
|
||||
menu_order=1,
|
||||
is_published=True,
|
||||
)
|
||||
CustomPage.objects.create(
|
||||
title="Hidden Nav",
|
||||
slug="hidden-nav",
|
||||
show_in_nav=False,
|
||||
is_published=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:home"))
|
||||
pages = list(response.context["nav_custom_pages"])
|
||||
self.assertEqual(len(pages), 1)
|
||||
self.assertEqual(pages[0].slug, "visible")
|
||||
|
||||
def test_nav_link_rendered(self):
|
||||
CustomPage.objects.create(
|
||||
title="Team",
|
||||
slug="team",
|
||||
menu_label="Our Team",
|
||||
show_in_nav=True,
|
||||
is_published=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:home"))
|
||||
self.assertContains(response, "Our Team")
|
||||
self.assertContains(response, reverse("pages:custom_page", kwargs={"slug": "team"}))
|
||||
@@ -0,0 +1,187 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.core.video import parse_youtube_video_id, youtube_embed_url
|
||||
from apps.pages.models import (
|
||||
AboutSection,
|
||||
CustomPage,
|
||||
CustomPageSection,
|
||||
HomepageSection,
|
||||
PageVideo,
|
||||
)
|
||||
from apps.products.models import MainProduct, ProductVideo
|
||||
|
||||
|
||||
class YouTubeParsingTest(TestCase):
|
||||
def test_watch_url(self):
|
||||
self.assertEqual(
|
||||
parse_youtube_video_id("https://www.youtube.com/watch?v=dQw4w9WgXcQ"),
|
||||
"dQw4w9WgXcQ",
|
||||
)
|
||||
|
||||
def test_short_url(self):
|
||||
self.assertEqual(
|
||||
parse_youtube_video_id("https://youtu.be/dQw4w9WgXcQ"),
|
||||
"dQw4w9WgXcQ",
|
||||
)
|
||||
|
||||
def test_embed_url(self):
|
||||
url = youtube_embed_url("https://youtu.be/dQw4w9WgXcQ")
|
||||
self.assertEqual(url, "https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ")
|
||||
|
||||
|
||||
class HomepageVideoSectionTest(TestCase):
|
||||
def test_video_section_renders_youtube_embed(self):
|
||||
HomepageSection.objects.create(
|
||||
section_type=HomepageSection.TYPE_VIDEO,
|
||||
title="Demo",
|
||||
video_source="youtube",
|
||||
video_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:home"))
|
||||
self.assertContains(response, "youtube-nocookie.com/embed/dQw4w9WgXcQ")
|
||||
self.assertContains(response, "video-block--md")
|
||||
|
||||
|
||||
class AboutVideoSectionTest(TestCase):
|
||||
def test_video_section_renders_on_about_page(self):
|
||||
AboutSection.objects.create(
|
||||
section_type=AboutSection.TYPE_VIDEO,
|
||||
title="Overview",
|
||||
video_source="youtube",
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
video_size="lg",
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:about"))
|
||||
self.assertContains(response, "video-block--lg")
|
||||
self.assertContains(response, "Overview")
|
||||
|
||||
|
||||
class CustomPageVideoSectionTest(TestCase):
|
||||
def test_video_section_renders_on_custom_page(self):
|
||||
page = CustomPage.objects.create(
|
||||
title="Media",
|
||||
slug="media-page",
|
||||
is_published=True,
|
||||
)
|
||||
CustomPageSection.objects.create(
|
||||
page=page,
|
||||
section_type=CustomPageSection.TYPE_VIDEO,
|
||||
title="Walkthrough",
|
||||
video_source="youtube",
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:custom_page", kwargs={"slug": "media-page"}))
|
||||
self.assertContains(response, "Walkthrough")
|
||||
self.assertContains(response, "iframe")
|
||||
|
||||
|
||||
class PageVideoTest(TestCase):
|
||||
def test_contact_page_video(self):
|
||||
PageVideo.objects.create(
|
||||
page=PageVideo.PAGE_CONTACT,
|
||||
title="Intro",
|
||||
video_source="youtube",
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:contact"))
|
||||
self.assertContains(response, "Intro")
|
||||
self.assertContains(response, "youtube-nocookie.com/embed/dQw4w9WgXcQ")
|
||||
|
||||
def test_faq_page_video(self):
|
||||
PageVideo.objects.create(
|
||||
page=PageVideo.PAGE_FAQ,
|
||||
title="Tutorial",
|
||||
video_source="youtube",
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:faq"))
|
||||
self.assertContains(response, "Tutorial")
|
||||
|
||||
|
||||
class ProductVideoTest(TestCase):
|
||||
def setUp(self):
|
||||
self.main_product = MainProduct.objects.create(
|
||||
name="Tecvico",
|
||||
slug="tecvico",
|
||||
short_description="Short",
|
||||
description="Long",
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
def test_product_video_renders_on_detail_page(self):
|
||||
ProductVideo.objects.create(
|
||||
main_product=self.main_product,
|
||||
title="Product Demo",
|
||||
video_source="youtube",
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
video_size="full",
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(
|
||||
reverse("products:main_product_detail", kwargs={"main_slug": "tecvico"})
|
||||
)
|
||||
self.assertContains(response, "Product Demo")
|
||||
self.assertContains(response, "video-block--full")
|
||||
|
||||
|
||||
class VideoStyledBackgroundTest(TestCase):
|
||||
def test_styled_background_renders_panel(self):
|
||||
HomepageSection.objects.create(
|
||||
section_type=HomepageSection.TYPE_VIDEO,
|
||||
title="Styled Demo",
|
||||
video_source="youtube",
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
video_styled_background=True,
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:home"))
|
||||
self.assertContains(response, "video-panel--styled")
|
||||
self.assertContains(response, "video-panel-blob")
|
||||
|
||||
def test_plain_background_without_panel(self):
|
||||
HomepageSection.objects.create(
|
||||
section_type=HomepageSection.TYPE_VIDEO,
|
||||
title="Plain Demo",
|
||||
video_source="youtube",
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
video_styled_background=False,
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:home"))
|
||||
self.assertNotContains(response, "video-panel--styled")
|
||||
self.assertContains(response, "Plain Demo")
|
||||
|
||||
|
||||
class VideoDescriptionFormatTest(TestCase):
|
||||
def test_plain_description_preserves_line_breaks(self):
|
||||
HomepageSection.objects.create(
|
||||
section_type=HomepageSection.TYPE_VIDEO,
|
||||
title="Demo",
|
||||
description="First line\nSecond line",
|
||||
video_source="youtube",
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:home"))
|
||||
self.assertContains(response, "First line")
|
||||
self.assertContains(response, "Second line")
|
||||
self.assertContains(response, "<br>")
|
||||
|
||||
def test_markdown_description_renders(self):
|
||||
PageVideo.objects.create(
|
||||
page=PageVideo.PAGE_FAQ,
|
||||
title="Guide",
|
||||
description="**Bold** intro",
|
||||
description_format="markdown",
|
||||
video_source="youtube",
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:faq"))
|
||||
self.assertContains(response, "<strong>Bold</strong>")
|
||||
@@ -0,0 +1,125 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.pages.models import FAQEntry
|
||||
|
||||
|
||||
class HomeViewTest(TestCase):
|
||||
def test_home_returns_200(self):
|
||||
response = self.client.get(reverse("pages:home"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_home_uses_correct_template(self):
|
||||
response = self.client.get(reverse("pages:home"))
|
||||
self.assertTemplateUsed(response, "pages/home.html")
|
||||
|
||||
def test_home_renders_dynamic_showcase_sections(self):
|
||||
from apps.pages.models import HomepageSection
|
||||
|
||||
projects = HomepageSection.objects.create(
|
||||
section_type=HomepageSection.TYPE_PROJECTS,
|
||||
title="Our Journey in the Realm of",
|
||||
title_highlight="Outstanding Projects",
|
||||
description="Explore our standout projects.",
|
||||
order=50,
|
||||
is_active=True,
|
||||
)
|
||||
HomepageSection.objects.create(
|
||||
section_type=HomepageSection.TYPE_EXPERIENCE,
|
||||
title="Experience Leading",
|
||||
title_highlight="the Way in Development",
|
||||
description="Embark on a journey of accelerated product development.",
|
||||
order=51,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("pages:home"))
|
||||
content = response.content.decode()
|
||||
self.assertIn("Our Journey in the Realm of", content)
|
||||
self.assertIn("Outstanding Projects", content)
|
||||
self.assertIn("Experience Leading", content)
|
||||
self.assertIn("the Way in Development", content)
|
||||
self.assertIn('data-project-filter="all"', content)
|
||||
|
||||
|
||||
class AboutViewTest(TestCase):
|
||||
def test_about_returns_200(self):
|
||||
response = self.client.get(reverse("pages:about"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_about_uses_correct_template(self):
|
||||
response = self.client.get(reverse("pages:about"))
|
||||
self.assertTemplateUsed(response, "pages/about.html")
|
||||
|
||||
|
||||
class FAQViewTest(TestCase):
|
||||
def setUp(self):
|
||||
FAQEntry.objects.create(
|
||||
question="What is the license?",
|
||||
answer="It is CC BY-NC-SA.",
|
||||
order=1,
|
||||
is_active=True,
|
||||
)
|
||||
FAQEntry.objects.create(
|
||||
question="Hidden question",
|
||||
answer="Hidden answer",
|
||||
order=2,
|
||||
is_active=False,
|
||||
)
|
||||
|
||||
def test_faq_returns_200(self):
|
||||
response = self.client.get(reverse("pages:faq"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_faq_uses_correct_template(self):
|
||||
response = self.client.get(reverse("pages:faq"))
|
||||
self.assertTemplateUsed(response, "pages/faq.html")
|
||||
|
||||
def test_faq_only_shows_active_entries(self):
|
||||
response = self.client.get(reverse("pages:faq"))
|
||||
entries = response.context["faq_entries"]
|
||||
self.assertEqual(entries.count(), 1)
|
||||
self.assertEqual(entries.first().question, "What is the license?")
|
||||
|
||||
|
||||
class ContactViewTest(TestCase):
|
||||
def test_contact_returns_200(self):
|
||||
response = self.client.get(reverse("pages:contact"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_contact_uses_correct_template(self):
|
||||
response = self.client.get(reverse("pages:contact"))
|
||||
self.assertTemplateUsed(response, "pages/contact.html")
|
||||
|
||||
|
||||
class NavigationContextTest(TestCase):
|
||||
def test_nav_main_products_in_context_on_all_pages(self):
|
||||
urls = [
|
||||
reverse("pages:home"),
|
||||
reverse("pages:about"),
|
||||
reverse("pages:faq"),
|
||||
reverse("pages:contact"),
|
||||
reverse("products:overview"),
|
||||
]
|
||||
for url in urls:
|
||||
response = self.client.get(url)
|
||||
self.assertIn(
|
||||
"nav_main_products",
|
||||
response.context,
|
||||
f"Missing nav_main_products at {url}",
|
||||
)
|
||||
|
||||
def test_nav_custom_pages_in_context_on_all_pages(self):
|
||||
urls = [
|
||||
reverse("pages:home"),
|
||||
reverse("pages:about"),
|
||||
reverse("pages:faq"),
|
||||
reverse("pages:contact"),
|
||||
]
|
||||
for url in urls:
|
||||
response = self.client.get(url)
|
||||
self.assertIn(
|
||||
"nav_custom_pages",
|
||||
response.context,
|
||||
f"Missing nav_custom_pages at {url}",
|
||||
)
|
||||
Reference in New Issue
Block a user