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))