initial commit

This commit is contained in:
mohamad
2026-06-21 17:54:19 +03:30
commit 20d6df3b27
191 changed files with 14362 additions and 0 deletions
View File
+77
View File
@@ -0,0 +1,77 @@
from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse
from apps.products.models import Article, ArticleSection, MainProduct, SubProduct
class AdminAccessTest(TestCase):
def setUp(self):
self.superuser = User.objects.create_superuser(
username="admin",
email="admin@example.com",
password="securepassword123",
)
self.client.login(username="admin", password="securepassword123")
self.main_product = MainProduct.objects.create(
name="Test Product",
short_description="Short",
description="Description",
)
self.sub_product = SubProduct.objects.create(
main_product=self.main_product,
name="Sub Product",
short_description="Short sub",
description="Sub description",
)
self.article = Article.objects.create(
sub_product=self.sub_product,
title="Test Article",
description="Article body",
)
self.section = ArticleSection.objects.create(
article=self.article,
title="Link",
value="https://example.com",
)
def test_main_product_changelist_accessible(self):
url = reverse("admin:products_mainproduct_changelist")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_sub_product_changelist_accessible(self):
url = reverse("admin:products_subproduct_changelist")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_article_changelist_accessible(self):
url = reverse("admin:products_article_changelist")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_article_section_changelist_accessible(self):
url = reverse("admin:products_articlesection_changelist")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_main_product_change_accessible(self):
url = reverse("admin:products_mainproduct_change", args=[self.main_product.pk])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_sub_product_change_accessible(self):
url = reverse("admin:products_subproduct_change", args=[self.sub_product.pk])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_article_change_accessible(self):
url = reverse("admin:products_article_change", args=[self.article.pk])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_admin_requires_authentication(self):
self.client.logout()
url = reverse("admin:products_mainproduct_changelist")
response = self.client.get(url)
self.assertNotEqual(response.status_code, 200)
+258
View File
@@ -0,0 +1,258 @@
from django.test import TestCase
from django.urls import reverse
from django.core.exceptions import ValidationError
from apps.products.models import (
Article,
ArticleSection,
MainProduct,
SubProduct,
SubProductVersion,
)
class MainProductModelTest(TestCase):
def setUp(self):
self.product = MainProduct.objects.create(
name="Test Product",
short_description="Short description",
description="Full description of the test product.",
)
def test_str_representation(self):
self.assertEqual(str(self.product), "Test Product")
def test_slug_auto_generated_on_create(self):
self.assertEqual(self.product.slug, "test-product")
def test_slug_not_overwritten_on_update(self):
self.product.name = "Changed Name"
self.product.save()
self.assertEqual(self.product.slug, "test-product")
def test_default_order_is_zero(self):
self.assertEqual(self.product.order, 0)
def test_default_is_active_is_true(self):
self.assertTrue(self.product.is_active)
def test_get_absolute_url(self):
url = self.product.get_absolute_url()
self.assertEqual(url, reverse("products:main_product_detail", kwargs={"main_slug": "test-product"}))
def test_slug_uniqueness(self):
from django.db import IntegrityError
with self.assertRaises(IntegrityError):
MainProduct.objects.create(
name="Test Product",
slug="test-product",
short_description="Another",
description="Another",
)
def test_timestamps_set_on_create(self):
self.assertIsNotNone(self.product.created_at)
self.assertIsNotNone(self.product.updated_at)
class SubProductModelTest(TestCase):
def setUp(self):
self.main_product = MainProduct.objects.create(
name="Main Product",
short_description="Short",
description="Description",
)
self.sub_product = SubProduct.objects.create(
main_product=self.main_product,
name="Sub Product",
short_description="Sub short",
description="Sub description.",
)
def test_str_representation(self):
self.assertIn("Main Product", str(self.sub_product))
self.assertIn("Sub Product", str(self.sub_product))
def test_slug_auto_generated(self):
self.assertEqual(self.sub_product.slug, "sub-product")
def test_get_absolute_url(self):
url = self.sub_product.get_absolute_url()
expected = reverse(
"products:sub_product_detail",
kwargs={"main_slug": "main-product", "sub_slug": "sub-product"},
)
self.assertEqual(url, expected)
def test_cascade_delete_with_main_product(self):
main_pk = self.main_product.pk
sub_pk = self.sub_product.pk
self.main_product.delete()
self.assertFalse(SubProduct.objects.filter(pk=sub_pk).exists())
self.assertFalse(MainProduct.objects.filter(pk=main_pk).exists())
class ArticleModelTest(TestCase):
def setUp(self):
self.main_product = MainProduct.objects.create(
name="Main",
short_description="Short",
description="Desc",
)
self.sub_product = SubProduct.objects.create(
main_product=self.main_product,
name="Sub",
short_description="Short",
description="Desc",
)
self.article = Article.objects.create(
sub_product=self.sub_product,
title="Test Article",
description="Article body.",
)
def test_str_representation(self):
self.assertEqual(str(self.article), "Test Article")
def test_cascade_delete_with_sub_product(self):
article_pk = self.article.pk
self.sub_product.delete()
self.assertFalse(Article.objects.filter(pk=article_pk).exists())
def test_main_product_article(self):
article = Article.objects.create(
main_product=self.main_product,
title="Product Article",
description="Body.",
)
self.assertEqual(article.main_product, self.main_product)
self.assertIsNone(article.sub_product_id)
def test_cascade_delete_with_main_product(self):
article = Article.objects.create(
main_product=self.main_product,
title="Product Article",
description="Body.",
)
article_pk = article.pk
self.main_product.delete()
self.assertFalse(Article.objects.filter(pk=article_pk).exists())
def test_requires_exactly_one_parent(self):
article = Article(
main_product=self.main_product,
sub_product=self.sub_product,
title="Invalid",
description="Body.",
)
with self.assertRaises(ValidationError):
article.full_clean()
def test_requires_a_parent(self):
article = Article(title="Orphan", description="Body.")
with self.assertRaises(ValidationError):
article.full_clean()
class ReleaseFileStorageTest(TestCase):
def test_save_overwrites_existing_release_file(self):
from django.core.files.uploadedfile import SimpleUploadedFile
from apps.products.models import MainProduct, SubProductVersion
main = MainProduct.objects.create(
name="Storage Test",
short_description="s",
description="d",
)
version = SubProductVersion.objects.create(
main_product=main,
version="1.0",
windows_download_file=SimpleUploadedFile("setup.exe", b"v1"),
is_active=True,
)
version.windows_download_file.save(
"setup.exe",
SimpleUploadedFile("setup.exe", b"v2"),
save=True,
)
version.refresh_from_db()
with version.windows_download_file.open("rb") as handle:
self.assertEqual(handle.read(), b"v2")
class SubProductVersionModelTest(TestCase):
def setUp(self):
self.main_product = MainProduct.objects.create(
name="Main",
short_description="Short",
description="Desc",
)
self.sub_product = SubProduct.objects.create(
main_product=self.main_product,
name="Sub",
short_description="Short",
description="Desc",
)
def test_main_product_version(self):
version = SubProductVersion.objects.create(
main_product=self.main_product,
version="1.0.0",
)
self.assertEqual(version.main_product, self.main_product)
self.assertIsNone(version.sub_product_id)
def test_requires_exactly_one_parent(self):
version = SubProductVersion(
main_product=self.main_product,
sub_product=self.sub_product,
version="1.0.0",
)
with self.assertRaises(ValidationError):
version.full_clean()
def test_display_channel_label_uses_custom_text(self):
from apps.products.models import RELEASE_CHANNEL_BETA
version = SubProductVersion.objects.create(
main_product=self.main_product,
version="2.0",
release_channel=RELEASE_CHANNEL_BETA,
channel_label="Preview build",
)
self.assertEqual(version.display_channel_label, "Preview build")
self.assertEqual(version.display_channel_css_modifier, "custom")
def test_display_channel_label_uses_preset(self):
from apps.products.models import RELEASE_CHANNEL_PREVIOUS
version = SubProductVersion.objects.create(
sub_product=self.sub_product,
version="1.0",
release_channel=RELEASE_CHANNEL_PREVIOUS,
)
self.assertEqual(version.display_channel_label, "Previous")
self.assertTrue(version.is_previous_channel)
class ArticleSectionModelTest(TestCase):
def setUp(self):
main = MainProduct.objects.create(name="M", short_description="s", description="d")
sub = SubProduct.objects.create(main_product=main, name="S", short_description="s", description="d")
self.article = Article.objects.create(sub_product=sub, title="Article", description="Desc")
self.section = ArticleSection.objects.create(
article=self.article,
title="Link",
value="https://example.com",
)
def test_str_representation(self):
self.assertIn("Article", str(self.section))
self.assertIn("Link", str(self.section))
def test_cascade_delete_with_article(self):
section_pk = self.section.pk
self.article.delete()
self.assertFalse(ArticleSection.objects.filter(pk=section_pk).exists())
+350
View File
@@ -0,0 +1,350 @@
import os
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase, override_settings
from django.urls import reverse
from apps.products.models import Article, ArticleSection, MainProduct, SubProduct, SubProductVersion
class ProductViewsSetup(TestCase):
def setUp(self):
self.main_product = MainProduct.objects.create(
name="Tecvico",
slug="tecvico",
short_description="Radiomics software",
description="Full description.",
)
self.sub_product = SubProduct.objects.create(
main_product=self.main_product,
name="Image Processing",
slug="image-processing",
short_description="Filtering and registration",
description="Sub description.",
)
self.article = Article.objects.create(
sub_product=self.sub_product,
title="Filtering",
description="Article body.",
)
ArticleSection.objects.create(
article=self.article,
title="Standard",
value="IBSI 2.0",
)
class ProductOverviewViewTest(ProductViewsSetup):
def test_overview_returns_200(self):
response = self.client.get(reverse("products:overview"))
self.assertEqual(response.status_code, 200)
def test_overview_uses_correct_template(self):
response = self.client.get(reverse("products:overview"))
self.assertTemplateUsed(response, "products/overview.html")
def test_overview_contains_main_product(self):
response = self.client.get(reverse("products:overview"))
self.assertIn(self.main_product, response.context["main_products"])
def test_inactive_product_excluded(self):
inactive = MainProduct.objects.create(
name="Inactive",
short_description="s",
description="d",
is_active=False,
)
response = self.client.get(reverse("products:overview"))
self.assertNotIn(inactive, response.context["main_products"])
class MainProductDetailViewTest(ProductViewsSetup):
def test_detail_returns_200(self):
url = reverse("products:main_product_detail", kwargs={"main_slug": "tecvico"})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_detail_uses_correct_template(self):
url = reverse("products:main_product_detail", kwargs={"main_slug": "tecvico"})
response = self.client.get(url)
self.assertTemplateUsed(response, "products/main_detail.html")
def test_detail_contains_product_in_context(self):
url = reverse("products:main_product_detail", kwargs={"main_slug": "tecvico"})
response = self.client.get(url)
self.assertEqual(response.context["main_product"], self.main_product)
def test_nonexistent_slug_returns_404(self):
url = reverse("products:main_product_detail", kwargs={"main_slug": "nonexistent"})
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_inactive_product_returns_404(self):
self.main_product.is_active = False
self.main_product.save()
url = reverse("products:main_product_detail", kwargs={"main_slug": "tecvico"})
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_detail_articles_in_context(self):
main_article = Article.objects.create(
main_product=self.main_product,
title="Overview",
description="Main product article.",
)
url = reverse("products:main_product_detail", kwargs={"main_slug": "tecvico"})
response = self.client.get(url)
self.assertIn("articles", response.context)
self.assertIn(main_article, list(response.context["articles"]))
self.assertNotIn(self.article, list(response.context["articles"]))
class SubProductDetailViewTest(ProductViewsSetup):
def test_sub_detail_returns_200(self):
url = reverse(
"products:sub_product_detail",
kwargs={"main_slug": "tecvico", "sub_slug": "image-processing"},
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_sub_detail_uses_correct_template(self):
url = reverse(
"products:sub_product_detail",
kwargs={"main_slug": "tecvico", "sub_slug": "image-processing"},
)
response = self.client.get(url)
self.assertTemplateUsed(response, "products/sub_detail.html")
def test_sub_detail_context_keys(self):
url = reverse(
"products:sub_product_detail",
kwargs={"main_slug": "tecvico", "sub_slug": "image-processing"},
)
response = self.client.get(url)
self.assertIn("main_product", response.context)
self.assertIn("sub_product", response.context)
self.assertIn("articles", response.context)
self.assertIn("siblings", response.context)
def test_sub_detail_articles_in_context(self):
url = reverse(
"products:sub_product_detail",
kwargs={"main_slug": "tecvico", "sub_slug": "image-processing"},
)
response = self.client.get(url)
self.assertIn(self.article, list(response.context["articles"]))
def test_nonexistent_sub_slug_returns_404(self):
url = reverse(
"products:sub_product_detail",
kwargs={"main_slug": "tecvico", "sub_slug": "does-not-exist"},
)
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_mismatched_main_slug_returns_404(self):
url = reverse(
"products:sub_product_detail",
kwargs={"main_slug": "wrong-product", "sub_slug": "image-processing"},
)
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
class SubProductOlderVersionsViewTest(ProductViewsSetup):
def test_versions_returns_200(self):
from apps.products.models import SubProductVersion
SubProductVersion.objects.create(
sub_product=self.sub_product,
version="9.9",
is_featured=True,
release_channel="stable",
windows_download_url="https://example.com/w",
is_active=True,
)
SubProductVersion.objects.create(
sub_product=self.sub_product,
version="9.8",
is_featured=False,
windows_download_url="https://example.com/w2",
is_active=True,
)
url = reverse(
"products:sub_product_versions",
kwargs={"main_slug": "tecvico", "sub_slug": "image-processing"},
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "products/versions_archive.html")
def test_main_versions_returns_200(self):
from apps.products.models import SubProductVersion
SubProductVersion.objects.create(
main_product=self.main_product,
version="9.9",
is_featured=True,
release_channel="stable",
windows_download_url="https://example.com/win.exe",
is_active=True,
)
SubProductVersion.objects.create(
main_product=self.main_product,
version="9.8",
is_active=True,
)
url = reverse(
"products:main_product_versions",
kwargs={"main_slug": "tecvico"},
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "products/versions_archive.html")
def test_main_detail_releases_in_context(self):
from apps.products.models import SubProductVersion
SubProductVersion.objects.create(
main_product=self.main_product,
version="1.0",
windows_download_url="https://example.com/win.exe",
is_active=True,
)
url = reverse("products:main_product_detail", kwargs={"main_slug": "tecvico"})
response = self.client.get(url)
self.assertTrue(response.context["show_releases_section"])
def test_installable_shows_release_channel_badges(self):
from apps.products.models import RELEASE_CHANNEL_BETA, RELEASE_CHANNEL_PREVIOUS, SubProductVersion
SubProductVersion.objects.create(
main_product=self.main_product,
version="2.0",
release_channel=RELEASE_CHANNEL_BETA,
is_featured=True,
windows_download_url="https://example.com/beta.exe",
is_active=True,
)
SubProductVersion.objects.create(
main_product=self.main_product,
version="1.9",
release_channel=RELEASE_CHANNEL_PREVIOUS,
show_on_product_page=True,
windows_download_url="https://example.com/prev.exe",
is_active=True,
)
url = reverse("products:main_product_detail", kwargs={"main_slug": "tecvico"})
response = self.client.get(url)
self.assertContains(response, "Beta")
self.assertContains(response, "Previous")
self.assertEqual(len(response.context["inline_download_channels"]), 1)
def test_custom_channel_label_overrides_preset(self):
from apps.products.models import RELEASE_CHANNEL_STABLE, SubProductVersion
SubProductVersion.objects.create(
main_product=self.main_product,
version="3.0",
release_channel=RELEASE_CHANNEL_STABLE,
channel_label="Early Access",
is_featured=True,
windows_download_url="https://example.com/ea.exe",
is_active=True,
)
url = reverse("products:main_product_detail", kwargs={"main_slug": "tecvico"})
response = self.client.get(url)
self.assertContains(response, "Early Access")
self.assertNotContains(response, ">Stable<")
@override_settings(MEDIA_ROOT=settings.BASE_DIR / "test_media_releases")
class ReleaseAssetDownloadViewTest(ProductViewsSetup):
def test_uploaded_file_download(self):
release_file = SimpleUploadedFile("setup.exe", b"binary-payload")
version = SubProductVersion.objects.create(
main_product=self.main_product,
version="2.0",
windows_download_file=release_file,
is_active=True,
)
url = reverse(
"products:release_asset_download",
kwargs={"version_id": version.pk, "asset": "windows"},
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(b"".join(response.streaming_content), b"binary-payload")
self.assertIn("attachment", response["Content-Disposition"])
self.assertIn("setup.exe", response["Content-Disposition"])
def test_external_url_blocks_file_download(self):
version = SubProductVersion.objects.create(
main_product=self.main_product,
version="2.1",
windows_download_url="https://example.com/win.exe",
windows_download_file=SimpleUploadedFile("local.exe", b"x"),
is_active=True,
)
url = reverse(
"products:release_asset_download",
kwargs={"version_id": version.pk, "asset": "windows"},
)
self.assertEqual(self.client.get(url).status_code, 404)
def test_product_page_uses_file_when_no_url(self):
version = SubProductVersion.objects.create(
main_product=self.main_product,
version="3.0",
windows_download_file=SimpleUploadedFile("win.tar.gz", b"gz"),
is_active=True,
)
detail_url = reverse("products:main_product_detail", kwargs={"main_slug": "tecvico"})
response = self.client.get(detail_url)
download_url = reverse(
"products:release_asset_download",
kwargs={"version_id": version.pk, "asset": "windows"},
)
self.assertContains(response, download_url)
def test_inactive_version_returns_404(self):
version = SubProductVersion.objects.create(
main_product=self.main_product,
version="0.1",
windows_download_file=SimpleUploadedFile("a.exe", b"a"),
is_active=False,
)
url = reverse(
"products:release_asset_download",
kwargs={"version_id": version.pk, "asset": "windows"},
)
self.assertEqual(self.client.get(url).status_code, 404)
def test_clearing_file_removes_from_disk(self):
version = SubProductVersion.objects.create(
main_product=self.main_product,
version="4.0",
windows_download_file=SimpleUploadedFile("remove-me.exe", b"data"),
is_active=True,
)
stored_path = version.windows_download_file.path
self.assertTrue(stored_path)
version.windows_download_file = ""
version.save()
version.refresh_from_db()
self.assertFalse(version.windows_download_file)
self.assertEqual(version.windows_download_filename, "")
self.assertFalse(os.path.exists(stored_path))
def test_deleting_version_removes_files_from_disk(self):
version = SubProductVersion.objects.create(
main_product=self.main_product,
version="5.0",
windows_download_file=SimpleUploadedFile("gone.exe", b"data"),
is_active=True,
)
stored_path = version.windows_download_file.path
version.delete()
self.assertFalse(os.path.exists(stored_path))