import io import uuid from unittest.mock import patch import pytest from django.core.files.uploadedfile import SimpleUploadedFile from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient from accounts.tests.factories import AccountUserFactory from cms.models import BlockType, ContentBlock, MediaAsset, ScanStatus from cms.upload import UploadValidationError, validate_upload from .factories import ContentBlockFactory, CourseThemeFactory, MediaAssetFactory, PageFactory @pytest.fixture def api_client(): return APIClient() @pytest.fixture def auth_client(): user = AccountUserFactory() client = APIClient() client.force_authenticate(user=user) return client, user # ── Upload validation ────────────────────────────────────────────────────────── class TestValidateUpload: def _make_file(self, name, size=1024, content=b"x" * 1024): return SimpleUploadedFile(name, content) def test_accepts_allowed_extension(self): f = self._make_file("photo.jpg") validate_upload(f) # should not raise def test_rejects_disallowed_extension(self): f = self._make_file("script.exe", size=100, content=b"x" * 100) with pytest.raises(UploadValidationError, match="extension"): validate_upload(f) def test_rejects_oversized_file(self): big = SimpleUploadedFile("big.jpg", b"x" * (101 * 1024 * 1024)) with pytest.raises(UploadValidationError, match="size"): validate_upload(big) # ── MediaUploadView ──────────────────────────────────────────────────────────── @pytest.mark.django_db class TestMediaUploadView: url = "/api/cms/upload/" def test_unauthenticated_returns_401(self, api_client): f = SimpleUploadedFile("img.jpg", b"data", content_type="image/jpeg") resp = api_client.post(self.url, {"file": f}, format="multipart") assert resp.status_code == status.HTTP_401_UNAUTHORIZED def test_upload_creates_asset(self, auth_client): client, user = auth_client content = b"fake image data" f = SimpleUploadedFile("test.jpg", content, content_type="image/jpeg") with patch("cms.views.save_upload") as mock_save: asset = MediaAssetFactory(uploaded_by=user) mock_save.return_value = asset resp = client.post(self.url, {"file": f, "org_id": str(uuid.uuid4())}, format="multipart") assert resp.status_code == status.HTTP_201_CREATED assert "id" in resp.data def test_missing_file_returns_400(self, auth_client): client, _ = auth_client resp = client.post(self.url, {}, format="multipart") assert resp.status_code == status.HTTP_400_BAD_REQUEST def test_invalid_file_returns_400(self, auth_client): client, _ = auth_client f = SimpleUploadedFile("malware.exe", b"MZ", content_type="application/octet-stream") resp = client.post(self.url, {"file": f}, format="multipart") assert resp.status_code == status.HTTP_400_BAD_REQUEST # ── PageContentBlocksView ────────────────────────────────────────────────────── @pytest.mark.django_db class TestPageContentBlocksView: def test_list_blocks_for_page(self, auth_client): client, _ = auth_client page = PageFactory() b1 = ContentBlockFactory(page=page, order=0) b2 = ContentBlockFactory(page=page, order=1) url = f"/api/cms/pages/{page.pk}/blocks/" resp = client.get(url) assert resp.status_code == status.HTTP_200_OK ids = [b["id"] for b in resp.data] assert str(b1.pk) in ids assert str(b2.pk) in ids def test_create_block(self, auth_client): client, _ = auth_client page = PageFactory() url = f"/api/cms/pages/{page.pk}/blocks/" payload = {"block_type": "richtext", "body": "
Hello
", "order": 0} resp = client.post(url, payload, format="json") assert resp.status_code == status.HTTP_201_CREATED assert ContentBlock.objects.filter(page=page).count() == 1 def test_create_block_unknown_page_returns_404(self, auth_client): client, _ = auth_client url = f"/api/cms/pages/{uuid.uuid4()}/blocks/" resp = client.post(url, {"block_type": "richtext", "order": 0}, format="json") assert resp.status_code == status.HTTP_404_NOT_FOUND # ── ContentBlockDetailView ───────────────────────────────────────────────────── @pytest.mark.django_db class TestContentBlockDetailView: def test_patch_block(self, auth_client): client, _ = auth_client block = ContentBlockFactory(body="original") url = f"/api/cms/blocks/{block.pk}/" resp = client.patch(url, {"body": "updated"}, format="json") assert resp.status_code == status.HTTP_200_OK block.refresh_from_db() assert block.body == "updated" def test_delete_block(self, auth_client): client, _ = auth_client block = ContentBlockFactory() url = f"/api/cms/blocks/{block.pk}/" resp = client.delete(url) assert resp.status_code == status.HTTP_204_NO_CONTENT assert not ContentBlock.objects.filter(pk=block.pk).exists() def test_patch_nonexistent_returns_404(self, auth_client): client, _ = auth_client url = f"/api/cms/blocks/{uuid.uuid4()}/" resp = client.patch(url, {"body": "x"}, format="json") assert resp.status_code == status.HTTP_404_NOT_FOUND # ── CourseTheme ──────────────────────────────────────────────────────────────── @pytest.mark.django_db class TestCourseTheme: def test_theme_created_with_defaults(self): theme = CourseThemeFactory() assert theme.primary_color == "#1a1a1a" assert theme.logo is None def test_theme_with_logo(self): user = AccountUserFactory() asset = MediaAssetFactory(uploaded_by=user) theme = CourseThemeFactory(logo=asset) assert theme.logo == asset # ── scan_media_asset_task ────────────────────────────────────────────────────── @pytest.mark.django_db class TestScanMediaAssetTask: def test_marks_asset_clean(self): from cms.tasks import scan_media_asset_task user = AccountUserFactory() asset = MediaAssetFactory(uploaded_by=user, scan_status=ScanStatus.PENDING) result = scan_media_asset_task(str(asset.pk)) asset.refresh_from_db() assert asset.scan_status == ScanStatus.CLEAN assert result["scan_status"] == "clean" def test_missing_asset_returns_error(self): from cms.tasks import scan_media_asset_task result = scan_media_asset_task(str(uuid.uuid4())) assert result["error"] == "not found"