import shutil import tempfile from pathlib import Path from django.contrib.auth import get_user_model from django.test import TestCase, override_settings from django.urls import reverse from django.utils import timezone from artifacts.models import ApiKey, Artifact, Asset User = get_user_model() class SharingTestCase(TestCase): @classmethod def setUpClass(cls): super().setUpClass() cls.artifacts_root = Path(tempfile.mkdtemp(prefix="agentfeed-test-sharing-")) cls.settings_override = override_settings(ARTIFACTS_ROOT=cls.artifacts_root) cls.settings_override.enable() @classmethod def tearDownClass(cls): cls.settings_override.disable() shutil.rmtree(cls.artifacts_root, ignore_errors=True) super().tearDownClass() def setUp(self): self.owner = User.objects.create_user(username="owner", password="pw") self.other = User.objects.create_user(username="other", password="pw") self.artifact = Artifact.objects.create( id="shrtest1", owner=self.owner, source="share-test", title="Share Test", kind=Artifact.MARKDOWN, day=timezone.localdate(), path="share-test/shrtest1.html", original_kind=Artifact.MARKDOWN, size_bytes=10, ) target = self.artifacts_root / "share-test" target.mkdir(parents=True, exist_ok=True) (target / "shrtest1.html").write_text("

Shared content

", encoding="utf-8") def toggle_share(self, client): return client.post(reverse("artifact-toggle-share", args=[self.artifact.id])) # -- toggling --------------------------------------------------------- def test_toggle_share_requires_login(self): response = self.toggle_share(self.client) self.assertEqual(response.status_code, 302) def test_non_owner_cannot_toggle_share(self): self.client.force_login(self.other) response = self.toggle_share(self.client) self.assertEqual(response.status_code, 404) def test_toggle_share_on_creates_token_and_url(self): self.client.force_login(self.owner) response = self.toggle_share(self.client) self.assertEqual(response.status_code, 200) body = response.json() self.assertTrue(body["is_shared"]) self.assertIsNotNone(body["share_url"]) self.artifact.refresh_from_db() self.assertTrue(self.artifact.share_token) self.assertIn(self.artifact.share_token, body["share_url"]) def test_toggle_share_off_clears_token(self): self.client.force_login(self.owner) self.toggle_share(self.client) response = self.toggle_share(self.client) body = response.json() self.assertFalse(body["is_shared"]) self.assertIsNone(body["share_url"]) self.artifact.refresh_from_db() self.assertIsNone(self.artifact.share_token) # -- public access ------------------------------------------------------ def test_shared_artifact_viewable_without_login(self): self.client.force_login(self.owner) self.toggle_share(self.client) self.artifact.refresh_from_db() anon = self.client_class() response = anon.get(reverse("artifact-shared", args=[self.artifact.share_token])) self.assertEqual(response.status_code, 200) def test_unshared_artifact_share_route_404s(self): anon = self.client_class() response = anon.get(reverse("artifact-shared", args=["not-a-real-token"])) self.assertEqual(response.status_code, 404) def test_unsharing_invalidates_old_link(self): self.client.force_login(self.owner) first = self.toggle_share(self.client).json() self.toggle_share(self.client) # turn back off anon = self.client_class() response = anon.get(reverse("artifact-shared", args=[first["share_url"].rsplit("/", 1)[-1]])) self.assertEqual(response.status_code, 404) def test_owner_route_still_requires_login_even_when_shared(self): self.client.force_login(self.owner) self.toggle_share(self.client) anon = self.client_class() response = anon.get(reverse("artifact", args=[self.artifact.id])) self.assertEqual(response.status_code, 302) # redirected to login, not served # -- embedded assets ------------------------------------------------------ def test_embedded_asset_not_public_when_artifact_unshared(self): asset = Asset.objects.create( id="asseta1b2c3", owner=self.owner, artifact=self.artifact, path="share-test/pic.png", content_type="image/png", size_bytes=5, ) (self.artifacts_root / "share-test" / "pic.png").write_bytes(b"\x89PNG") anon = self.client_class() response = anon.get(reverse("asset", args=[asset.id])) self.assertEqual(response.status_code, 404) def test_embedded_asset_public_when_artifact_shared(self): asset = Asset.objects.create( id="asseta1b2c3", owner=self.owner, artifact=self.artifact, path="share-test/pic.png", content_type="image/png", size_bytes=5, ) (self.artifacts_root / "share-test" / "pic.png").write_bytes(b"\x89PNG") self.client.force_login(self.owner) self.toggle_share(self.client) anon = self.client_class() response = anon.get(reverse("asset", args=[asset.id])) self.assertEqual(response.status_code, 200) def test_unattached_asset_stays_private_regardless(self): """An asset with no parent artifact (orphaned upload) is never public, even if some unrelated artifact happens to be shared.""" asset = Asset.objects.create( id="orphanasset", owner=self.owner, artifact=None, path="share-test/orphan.png", content_type="image/png", size_bytes=5, ) (self.artifacts_root / "share-test" / "orphan.png").write_bytes(b"\x89PNG") self.client.force_login(self.owner) self.toggle_share(self.client) # shares self.artifact, unrelated to this asset anon = self.client_class() response = anon.get(reverse("asset", args=[asset.id])) self.assertEqual(response.status_code, 404) # -- API surface -------------------------------------------------------- def test_api_detail_reports_share_status(self): token = ApiKey.objects.create(user=self.owner, name="owner-key") self.client.force_login(self.owner) self.toggle_share(self.client) self.artifact.refresh_from_db() response = self.client.get( reverse("artifact-detail-api", args=[self.artifact.id]), HTTP_AUTHORIZATION=f"Bearer {token.key}", ) self.assertTrue(response.data["is_shared"]) self.assertIn(self.artifact.share_token, response.data["share_url"])