loko/streetup/documents/tests.py
2026-07-22 14:48:40 +02:00

663 lines
No EOL
27 KiB
Python

from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from django.urls import reverse
from common.models import Role, UserConfig
from interventions.models import Intervention
from projects.models import Project
from .models import DocumentAttachment, DocumentFolder, DocumentFolderShare, DocumentShare, ManagedDocument
from .views import ensure_document_deletable
from .permissions import (
filter_documents_for_user,
filter_folders_for_user,
get_user_visible_folder_ids,
user_has_document_permission,
)
User = get_user_model()
class DocumentPermissionTests(TestCase):
def setUp(self):
self.owner = User.objects.create_user(username="owner", password="owner")
self.shared_user = User.objects.create_user(username="shared", password="shared")
self.folder_user = User.objects.create_user(username="folder", password="folder")
self.other_user = User.objects.create_user(username="other", password="other")
self.superuser = User.objects.create_superuser(username="admin", password="admin")
self.document = ManagedDocument.objects.create(title="Doc", created_by=self.owner)
self.folder = DocumentFolder.objects.create(
name="Folder", slug="folder", created_by=self.owner
)
self.parent_folder = DocumentFolder.objects.create(
name="Parent", slug="parent-folder", created_by=self.owner
)
self.folder.parent_folders.add(self.parent_folder)
self.document.folders.add(self.folder)
DocumentShare.objects.create(
document=self.document,
user=self.shared_user,
permission=ManagedDocument.PERMISSION_COMMENT,
)
DocumentFolderShare.objects.create(
folder=self.folder,
user=self.folder_user,
permission=ManagedDocument.PERMISSION_VIEW,
)
def test_owner_has_full_access(self):
self.assertTrue(user_has_document_permission(self.owner, self.document))
self.assertTrue(
user_has_document_permission(
self.owner,
self.document,
required_permission=ManagedDocument.PERMISSION_APPROVE,
)
)
def test_share_permission_respected(self):
self.assertTrue(user_has_document_permission(self.shared_user, self.document))
self.assertFalse(
user_has_document_permission(
self.shared_user,
self.document,
required_permission=ManagedDocument.PERMISSION_EDIT,
)
)
def test_folder_share_grants_access(self):
self.assertTrue(user_has_document_permission(self.folder_user, self.document))
def test_superuser_has_access(self):
self.assertTrue(user_has_document_permission(self.superuser, self.document))
def test_anonymous_user_has_no_access(self):
self.assertFalse(user_has_document_permission(AnonymousUser(), self.document))
def test_filter_documents_for_user(self):
queryset = ManagedDocument.objects.order_by("pk")
shared_ids = set(filter_documents_for_user(queryset, self.shared_user).values_list("pk", flat=True))
folder_ids = set(filter_documents_for_user(queryset, self.folder_user).values_list("pk", flat=True))
other_ids = set(filter_documents_for_user(queryset, self.other_user).values_list("pk", flat=True))
self.assertEqual(shared_ids, {self.document.pk})
self.assertEqual(folder_ids, {self.document.pk})
self.assertEqual(other_ids, set())
def test_filter_documents_for_superuser_returns_all(self):
queryset = ManagedDocument.objects.order_by("pk")
filtered = filter_documents_for_user(queryset, self.superuser)
self.assertEqual(set(filtered.values_list("pk", flat=True)), set(queryset.values_list("pk", flat=True)))
def test_filter_folders_for_user(self):
queryset = DocumentFolder.objects.order_by("pk")
owner_ids = set(filter_folders_for_user(queryset, self.owner).values_list("pk", flat=True))
shared_ids = set(filter_folders_for_user(queryset, self.folder_user).values_list("pk", flat=True))
other_ids = set(filter_folders_for_user(queryset, self.other_user).values_list("pk", flat=True))
self.assertIn(self.folder.pk, owner_ids)
self.assertIn(self.folder.pk, shared_ids)
self.assertEqual(other_ids, set())
def test_visible_folder_ids_include_ancestors(self):
visible = get_user_visible_folder_ids(self.folder_user)
self.assertIn(self.folder.pk, visible)
self.assertIn(self.parent_folder.pk, visible)
class DocumentListViewTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(username="viewer", password="viewer")
UserConfig.objects.create(user=self.user, is_intern=True)
self.client.force_login(self.user)
self.parent = DocumentFolder.objects.create(
name="Parent", slug="parent", created_by=self.user
)
self.child = DocumentFolder.objects.create(
name="Child", slug="child", created_by=self.user
)
self.grandchild = DocumentFolder.objects.create(
name="Grandchild", slug="grandchild", created_by=self.user
)
self.child.parent_folders.add(self.parent)
self.grandchild.parent_folders.add(self.child)
self.parent_doc = ManagedDocument.objects.create(
title="Parent document",
created_by=self.user,
)
self.parent_doc.folders.add(self.parent)
self.child_doc = ManagedDocument.objects.create(
title="Child note",
created_by=self.user,
)
self.child_doc.folders.add(self.child)
self.grandchild_doc = ManagedDocument.objects.create(
title="Grand strategy",
created_by=self.user,
)
self.grandchild_doc.folders.add(self.grandchild)
self.outside_folder = DocumentFolder.objects.create(name="External", slug="external")
self.outside_doc = ManagedDocument.objects.create(
title="Grand outside",
created_by=self.user,
)
self.outside_doc.folders.add(self.outside_folder)
def test_folder_filter_only_includes_direct_documents(self):
response = self.client.get(reverse("documents:list"), {"folder": self.parent.slug})
self.assertEqual(response.status_code, 200)
documents = list(response.context["documents"])
document_ids = {document.pk for document in documents}
self.assertSetEqual(
document_ids,
{self.parent_doc.pk},
)
self.assertNotIn(self.outside_doc.pk, document_ids)
def test_search_returns_descendant_folders(self):
response = self.client.get(
reverse("documents:list"),
{"folder": self.parent.slug, "search": "Grand"},
)
self.assertEqual(response.status_code, 200)
visible_folders = response.context["visible_folders"]
self.assertIn(self.grandchild, visible_folders)
self.assertNotIn(self.outside_folder, visible_folders)
self.assertNotIn(self.child, visible_folders)
listing_items = response.context["listing_items"]
self.assertTrue(
any(
item["type"] == "folder" and item["object"].pk == self.grandchild.pk
for item in listing_items
)
)
def test_search_limits_documents_to_folder_descendants(self):
response = self.client.get(
reverse("documents:list"),
{"folder": self.parent.slug, "search": "Grand"},
)
self.assertEqual(response.status_code, 200)
documents = list(response.context["documents"])
document_ids = {document.pk for document in documents}
self.assertSetEqual(document_ids, {self.grandchild_doc.pk})
self.assertNotIn(self.outside_doc.pk, document_ids)
class DocumentFolderManagementTests(TestCase):
def setUp(self):
self.owner = User.objects.create_user(username="folder-owner", password="owner")
UserConfig.objects.create(user=self.owner, is_intern=True)
self.other = User.objects.create_user(username="folder-other", password="other")
UserConfig.objects.create(user=self.other, is_intern=True)
self.admin = User.objects.create_user(username="folder-admin", password="admin")
admin_role, _ = Role.objects.get_or_create(name="admin")
admin_config = UserConfig.objects.create(user=self.admin, is_intern=True)
admin_config.roles.add(admin_role)
self.folder = DocumentFolder.objects.create(
name="Root",
slug="root",
created_by=self.owner,
)
def test_owner_can_update_folder(self):
self.client.force_login(self.owner)
response = self.client.post(
reverse("documents:folder_edit", args=[self.folder.pk]),
{
"name": "Updated",
"description": "",
"application_label": "",
"parent_folders": [],
},
)
self.assertRedirects(response, f"{reverse('documents:list')}?folder={self.folder.slug}")
self.folder.refresh_from_db()
self.assertEqual(self.folder.name, "Updated")
self.assertEqual(self.folder.created_by, self.owner)
def test_non_owner_cannot_update_folder(self):
self.client.force_login(self.other)
response = self.client.post(
reverse("documents:folder_edit", args=[self.folder.pk]),
{
"name": "Blocked",
"description": "",
"application_label": "",
"parent_folders": [],
},
)
self.assertEqual(response.status_code, 403)
def test_predefined_folder_cannot_be_modified(self):
self.folder.is_predefined = True
self.folder.save()
self.client.force_login(self.owner)
def test_admin_can_change_owner(self):
new_owner = User.objects.create_user(username="new-owner", password="new")
self.client.force_login(self.admin)
response = self.client.post(
reverse("documents:folder_edit", args=[self.folder.pk]),
{
"name": "Admin updated",
"description": "",
"application_label": "",
"parent_folders": [],
"created_by": new_owner.pk,
},
)
self.assertRedirects(response, f"{reverse('documents:list')}?folder={self.folder.slug}")
self.folder.refresh_from_db()
self.assertEqual(self.folder.created_by, new_owner)
def test_owner_can_access_share_page(self):
self.client.force_login(self.owner)
response = self.client.get(
reverse("documents:folder_share", args=[self.folder.pk])
)
self.assertEqual(response.status_code, 200)
def test_owner_can_create_share(self):
target = User.objects.create_user(username="share-target", password="pass")
self.client.force_login(self.owner)
response = self.client.post(
reverse("documents:folder_share_add", args=[self.folder.pk]),
{"user": target.pk, "permission": ManagedDocument.PERMISSION_VIEW},
)
self.assertRedirects(
response, reverse("documents:folder_share", args=[self.folder.pk])
)
self.assertTrue(
DocumentFolderShare.objects.filter(folder=self.folder, user=target).exists()
)
def test_owner_can_create_access_link(self):
self.client.force_login(self.owner)
response = self.client.post(
reverse("documents:folder_access_link", args=[self.folder.pk]),
{"permission": ManagedDocument.PERMISSION_VIEW},
)
self.assertRedirects(
response, reverse("documents:folder_share", args=[self.folder.pk])
)
self.assertEqual(self.folder.access_links.count(), 1)
def test_owner_can_delete_folder(self):
self.client.force_login(self.owner)
response = self.client.post(
reverse("documents:folder_delete", args=[self.folder.pk]),
{},
)
self.assertRedirects(response, reverse("documents:list"))
self.assertFalse(DocumentFolder.objects.filter(pk=self.folder.pk).exists())
def test_non_owner_cannot_delete_folder(self):
self.client.force_login(self.other)
response = self.client.post(
reverse("documents:folder_delete", args=[self.folder.pk]),
{},
)
self.assertEqual(response.status_code, 403)
def test_admin_can_delete_folder(self):
self.client.force_login(self.admin)
response = self.client.post(
reverse("documents:folder_delete", args=[self.folder.pk]),
{},
)
self.assertRedirects(response, reverse("documents:list"))
self.assertFalse(DocumentFolder.objects.filter(pk=self.folder.pk).exists())
def test_predefined_folder_cannot_be_deleted(self):
self.folder.is_predefined = True
self.folder.save()
self.client.force_login(self.owner)
response = self.client.post(
reverse("documents:folder_delete", args=[self.folder.pk]),
{},
)
self.assertEqual(response.status_code, 403)
def test_cannot_set_folder_as_child_of_descendant(self):
child = DocumentFolder.objects.create(name="Child", slug="child", created_by=self.owner)
child.parent_folders.add(self.folder)
self.client.force_login(self.owner)
response = self.client.post(
reverse("documents:folder_edit", args=[self.folder.pk]),
{
"name": self.folder.name,
"description": "",
"application_label": "",
"parent_folders": [child.pk],
},
)
self.assertEqual(response.status_code, 200)
self.assertIn("parent_folders", response.context["form"].errors)
def test_folder_creation_assigns_owner(self):
self.client.force_login(self.other)
response = self.client.post(
reverse("documents:folder_create"),
{
"name": "New Folder",
"description": "",
"application_label": "",
"parent_folders": [],
},
)
created_folder = DocumentFolder.objects.get(name="New Folder")
expected_redirect = f"{reverse('documents:list')}?folder={created_folder.slug}"
self.assertRedirects(response, expected_redirect)
self.assertEqual(created_folder.created_by, self.other)
class DocumentDeletionPermissionTests(TestCase):
def setUp(self):
self.owner = User.objects.create_user(username="doc-owner", password="owner")
UserConfig.objects.create(user=self.owner, is_intern=True)
self.manager = User.objects.create_user(username="doc-manager", password="manager")
manager_role, _ = Role.objects.get_or_create(name="manager")
self.manager_config = UserConfig.objects.create(user=self.manager, is_intern=True)
self.manager_config.roles.add(manager_role)
self.project = Project.objects.create(name="Project linked doc", created_by=self.owner)
self.intervention = Intervention.objects.create(title="Intervention linked doc")
self.folder = DocumentFolder.objects.create(
name="Root",
slug="root",
created_by=self.owner,
)
def test_internal_manager_can_delete_project_document(self):
document = ManagedDocument.objects.create(title="Project document", created_by=self.owner)
DocumentAttachment.objects.create(
document=document,
content_type=ContentType.objects.get_for_model(Project),
object_id=self.project.pk,
)
ensure_document_deletable(document, self.manager)
def test_internal_manager_can_delete_intervention_document(self):
document = ManagedDocument.objects.create(title="Intervention document", created_by=self.owner)
DocumentAttachment.objects.create(
document=document,
content_type=ContentType.objects.get_for_model(Intervention),
object_id=self.intervention.pk,
)
ensure_document_deletable(document, self.manager)
self.client.force_login(self.manager)
response = self.client.post(
reverse("documents:folder_edit", args=[self.folder.pk]),
{
"name": "Still locked",
"description": "",
"application_label": "",
"parent_folders": [],
},
)
self.assertEqual(response.status_code, 403)
class SearchObjectsViewTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(username="testuser", password="testpassword")
UserConfig.objects.create(user=self.user, is_intern=True)
self.document = ManagedDocument.objects.create(title="Test Document", created_by=self.user)
self.project = Project.objects.create(name="Awesome project name", created_by=self.user)
self.content_type = ContentType.objects.get_for_model(Project)
def test_search_objects_anonymous(self):
url = reverse("documents:search_objects", args=[self.document.pk])
response = self.client.get(url, {"content_type": self.content_type.pk, "q": "Awesome"})
self.assertEqual(response.status_code, 302)
def test_search_objects_authenticated(self):
self.client.force_login(self.user)
url = reverse("documents:search_objects", args=[self.document.pk])
response = self.client.get(url, {"content_type": self.content_type.pk, "q": "Awesome"})
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(len(data["results"]), 1)
self.assertEqual(data["results"][0]["id"], self.project.pk)
self.assertIn("Awesome project name", data["results"][0]["text"])
class DocumentGeoreferenceTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(username="geo_user", password="password")
UserConfig.objects.create(user=self.user, is_intern=True)
self.document = ManagedDocument.objects.create(title="Geo Doc", created_by=self.user)
def test_georeference_view_denied_for_anonymous(self):
url = reverse("documents:georeference", args=[self.document.pk])
response = self.client.get(url)
self.assertEqual(response.status_code, 302) # Redirect to login
def test_georeference_view_allowed_for_owner(self):
self.client.force_login(self.user)
url = reverse("documents:georeference", args=[self.document.pk])
response = self.client.get(url, {"mode": "overlay"})
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Superposition")
def test_georeference_save_success(self):
self.client.force_login(self.user)
url = reverse("documents:georeference_save", args=[self.document.pk])
coords = [[4.1, 50.1], [4.2, 50.1], [4.2, 50.2], [4.1, 50.2]]
response = self.client.post(
url,
{"coordinates": coords, "page": 1},
content_type="application/json"
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["status"], "success")
# Verify model updated
self.document.refresh_from_db()
self.assertEqual(self.document.georeference_coords, coords)
self.assertEqual(self.document.georeference_page, 1)
def test_georeference_save_invalid_format(self):
self.client.force_login(self.user)
url = reverse("documents:georeference_save", args=[self.document.pk])
# 3 points instead of 4
coords = [[4.1, 50.1], [4.2, 50.1], [4.2, 50.2]]
response = self.client.post(
url,
{"coordinates": coords},
content_type="application/json"
)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()["status"], "error")
def test_map_view_not_georeferenced(self):
self.client.force_login(self.user)
url = reverse("documents:map_view", args=[self.document.pk])
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_map_view_georeferenced(self):
self.client.force_login(self.user)
self.document.georeference_coords = [[4.1, 50.1], [4.2, 50.1], [4.2, 50.2], [4.1, 50.2]]
self.document.save()
url = reverse("documents:map_view", args=[self.document.pk])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Visualisation sur la carte")
from unittest.mock import patch
from django.core.files.uploadedfile import SimpleUploadedFile
from documents.models import DocumentVersion
import os
class DocumentCadPreviewTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(username="cad_user", password="password")
UserConfig.objects.create(user=self.user, is_intern=True)
self.document = ManagedDocument.objects.create(title="Cad Doc", created_by=self.user)
# Create a version with a mock DXF file
self.version = DocumentVersion.objects.create(
document=self.document,
file=SimpleUploadedFile("plan.dxf", b"DUMMY DXF CONTENT"),
uploaded_by=self.user,
version_number=1
)
@patch("documents.utils.render_cad_file_to_image")
def test_dxf_preview_rendering_and_serving(self, mock_render):
# We simulate the render_cad_file_to_image writing a mock file
def fake_render(input_path, output_path):
with open(output_path, "wb") as f:
f.write(b"MOCK PNG CONTENT")
mock_render.side_effect = fake_render
self.client.force_login(self.user)
url = reverse("documents:page_image", args=[self.document.pk, 1])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-Type"], "image/png")
self.assertEqual(b"".join(response.streaming_content), b"MOCK PNG CONTENT")
# Verify preview file was created on disk
preview_path = self.version.file.path + ".preview.png"
self.assertTrue(os.path.exists(preview_path))
# Clean up preview file after test
if os.path.exists(preview_path):
os.remove(preview_path)
@patch("documents.utils.render_cad_file_to_image")
def test_dwg_preview_rendering_and_serving(self, mock_render):
# Create a document and version with .dwg file
doc_dwg = ManagedDocument.objects.create(title="Dwg Doc", created_by=self.user)
version_dwg = DocumentVersion.objects.create(
document=doc_dwg,
file=SimpleUploadedFile("plan.dwg", b"DUMMY DWG CONTENT"),
uploaded_by=self.user,
version_number=1
)
def fake_render(input_path, output_path):
with open(output_path, "wb") as f:
f.write(b"MOCK DWG PNG CONTENT")
mock_render.side_effect = fake_render
self.client.force_login(self.user)
url = reverse("documents:page_image", args=[doc_dwg.pk, 1])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-Type"], "image/png")
self.assertEqual(b"".join(response.streaming_content), b"MOCK DWG PNG CONTENT")
# Clean up preview file after test
preview_path = version_dwg.file.path + ".preview.png"
if os.path.exists(preview_path):
os.remove(preview_path)
from assets.models import NatureLocation
from documents.models import DocumentFolderAttachment
class DocumentGeoreferenceAutoZoomTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(username="geo_user", password="password")
UserConfig.objects.create(user=self.user, is_intern=True)
self.document = ManagedDocument.objects.create(
title="Auto Zoom Doc",
created_by=self.user
)
self.location = NatureLocation.objects.create(
code="LOC1",
name_fr="Location 1",
lon=4.3517,
lat=50.8503
)
def test_direct_attachment_coords(self):
# Create direct attachment to the geolocated asset
DocumentAttachment.objects.create(
document=self.document,
content_type=ContentType.objects.get_for_model(self.location),
object_id=self.location.pk
)
# Test helper function
from .views import _get_associated_asset_coords
coords = _get_associated_asset_coords(self.document)
self.assertEqual(coords, [4.3517, 50.8503])
# Test that view injects associated_coords into template context
self.client.force_login(self.user)
url = reverse("documents:georeference", args=[self.document.pk])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context["associated_coords"], [4.3517, 50.8503])
def test_folder_attachment_coords(self):
# Create folder and associate with document
folder = DocumentFolder.objects.create(name="Geo Folder", slug="geo-folder")
self.document.folders.add(folder)
# Create folder attachment to the geolocated asset
DocumentFolderAttachment.objects.create(
folder=folder,
content_type=ContentType.objects.get_for_model(self.location),
object_id=self.location.pk
)
from .views import _get_associated_asset_coords
coords = _get_associated_asset_coords(self.document)
self.assertEqual(coords, [4.3517, 50.8503])
def test_parent_folder_attachment_coords(self):
# Create parent folder, child folder, and associate with document
parent_folder = DocumentFolder.objects.create(name="Parent Geo Folder", slug="parent-geo-folder")
child_folder = DocumentFolder.objects.create(name="Child Folder", slug="child-folder")
child_folder.parent_folders.add(parent_folder)
self.document.folders.add(child_folder)
# Create folder attachment to the parent folder
DocumentFolderAttachment.objects.create(
folder=parent_folder,
content_type=ContentType.objects.get_for_model(self.location),
object_id=self.location.pk
)
from .views import _get_associated_asset_coords
coords = _get_associated_asset_coords(self.document)
self.assertEqual(coords, [4.3517, 50.8503])