You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

258 lines
11 KiB

from django.test import TestCase
from django.core.files.uploadedfile import SimpleUploadedFile
from django.contrib.auth import get_user_model
from rest_framework.test import APIClient
from rest_framework import status
from apps.projects.models.project import (
Project,
KanbanColumn,
KanbanTask,
ProjectDocument,
)
from apps.profiles.models.institution import Institution, InstitutionMember
User = get_user_model()
class Phase5ProjectsTests(TestCase):
"""
Automated test suite for Phase 5:
- Project Creation & Default Kanban Column Generation
- Scoped Project Listing & Filtering (institution, category, status, search)
- Project Detail View with Multi-Institution Collaborators
- Drag-and-Drop Kanban Board View & Task Hierarchy
- Task Creation, Priority, Column Migration & Reordering
- Project Updates & Role-Based Permissions
- Project Document Uploads & Deletions
"""
def setUp(self):
self.client = APIClient()
# Users
self.lead_admin = User.objects.create_user(
email="lead.director@mashhadcenter.org",
password="SecurePassword123!",
fullname="Hassan Razavi Director",
user_type=User.UserType.INSTITUTION_ADMIN,
country="Iran",
city="Mashhad"
)
self.collab_editor = User.objects.create_user(
email="collab.editor@berlincenter.de",
password="SecurePassword123!",
fullname="Fatima Berlin Coordinator",
user_type=User.UserType.EDITOR,
country="Germany",
city="Berlin"
)
self.unrelated_user = User.objects.create_user(
email="outsider@community.org",
password="SecurePassword123!",
fullname="Ali Outsider",
user_type=User.UserType.CLIENT,
country="France",
city="Paris"
)
# Institutions
self.owner_inst = Institution.objects.create(
name="Astan Quds International Relations",
type=Institution.InstitutionType.INSTITUTE,
country="Iran",
city="Mashhad"
)
InstitutionMember.objects.create(
institution=self.owner_inst,
user=self.lead_admin,
role=InstitutionMember.MemberRole.ADMIN
)
self.collab_inst = Institution.objects.create(
name="Imam Ali Islamic Center Berlin",
type=Institution.InstitutionType.CULTURAL_CENTER,
country="Germany",
city="Berlin"
)
InstitutionMember.objects.create(
institution=self.collab_inst,
user=self.collab_editor,
role=InstitutionMember.MemberRole.EDITOR
)
# Sample Project
self.project = Project.objects.create(
title="International Encyclopedia of Razavi Manuscript Heritage",
description="Joint digital restoration and multilingual publication of historic manuscripts.",
owner_institution=self.owner_inst,
category="Cultural Diplomacy",
status=Project.Status.ACTIVE,
progress_percentage=35,
budget="€120,000",
team_size=8,
created_by=self.lead_admin
)
self.project.collaborating_institutions.add(self.collab_inst)
def test_project_creation_and_auto_columns(self):
self.client.force_authenticate(user=self.lead_admin)
payload = {
"title": "Inter-Center Youth Leadership Summit 2026",
"description": "Annual multilateral youth exchange program.",
"owner_institution": self.owner_inst.id,
"collaborating_institutions": [self.collab_inst.id],
"category": "Educational Academy",
"status": "active",
"progress_percentage": 10,
"budget": "€45,000",
"team_size": 4
}
response = self.client.post("/api/v1/projects/", payload, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["title"], payload["title"])
self.assertTrue(response.data["slug"])
self.assertEqual(len(response.data["collaborating_institutions"]), 1)
# Verify auto-generated 4 default Kanban columns
project_id = response.data["id"]
columns_count = KanbanColumn.objects.filter(project_id=project_id).count()
self.assertEqual(columns_count, 4)
def test_project_listing_and_filtering(self):
# 1. Base list
response = self.client.get("/api/v1/projects/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertGreaterEqual(len(response.data["results"]), 1)
# 2. Filter by owner institution
resp_inst = self.client.get(f"/api/v1/projects/?institution_id={self.owner_inst.id}")
self.assertEqual(len(resp_inst.data["results"]), 1)
# 3. Filter by collaborating institution
resp_collab = self.client.get(f"/api/v1/projects/?institution_id={self.collab_inst.id}")
self.assertEqual(len(resp_collab.data["results"]), 1)
# 4. Filter by category
resp_cat = self.client.get("/api/v1/projects/?category=Cultural Diplomacy")
self.assertEqual(len(resp_cat.data["results"]), 1)
# 5. Search
resp_search = self.client.get("/api/v1/projects/?search=Manuscript")
self.assertEqual(len(resp_search.data["results"]), 1)
def test_project_detail_and_collaboration_links(self):
self.client.force_authenticate(user=self.lead_admin)
response = self.client.get(f"/api/v1/projects/{self.project.slug}/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["title"], self.project.title)
self.assertEqual(response.data["owner_institution"]["name"], self.owner_inst.name)
self.assertEqual(len(response.data["collaborating_institutions"]), 1)
self.assertEqual(response.data["collaborating_institutions"][0]["name"], self.collab_inst.name)
self.assertTrue(response.data["can_edit"])
def test_kanban_board_hierarchy(self):
response = self.client.get(f"/api/v1/projects/{self.project.id}/kanban/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 4)
column_names = [col["name"] for col in response.data]
self.assertIn("To Do", column_names)
self.assertIn("In Progress", column_names)
self.assertIn("Review", column_names)
self.assertIn("Done", column_names)
def test_kanban_task_creation_and_reordering_across_columns(self):
self.client.force_authenticate(user=self.lead_admin)
todo_column = self.project.columns.get(code="todo")
in_progress_column = self.project.columns.get(code="in_progress")
done_column = self.project.columns.get(code="done")
# 1. Create task in To Do
task_payload = {
"column": todo_column.id,
"title": "Digitize 15th Century Manuscript Folios",
"description": "High-resolution 1200 DPI archival scans.",
"priority": "high",
"assignee": self.collab_editor.id,
"labels": ["Archival", "Digital Preservation"],
"order": 1
}
create_task_resp = self.client.post(f"/api/v1/projects/{self.project.id}/tasks/", task_payload, format="json")
self.assertEqual(create_task_resp.status_code, status.HTTP_201_CREATED)
task_id = create_task_resp.data["id"]
self.assertEqual(create_task_resp.data["column_code"], "todo")
# 2. Move task to In Progress (Drag-and-Drop Simulation)
move_resp = self.client.patch(
f"/api/v1/projects/{self.project.id}/tasks/{task_id}/",
{"column": in_progress_column.id, "order": 2},
format="json"
)
self.assertEqual(move_resp.status_code, status.HTTP_200_OK)
self.assertEqual(move_resp.data["column_code"], "in_progress")
# 3. Complete task -> Move to Done
done_resp = self.client.patch(
f"/api/v1/projects/{self.project.id}/tasks/{task_id}/",
{"column": done_column.id},
format="json"
)
self.assertEqual(done_resp.status_code, status.HTTP_200_OK)
self.assertEqual(done_resp.data["column_code"], "done")
# 4. Delete task
del_resp = self.client.delete(f"/api/v1/projects/{self.project.id}/tasks/{task_id}/")
self.assertEqual(del_resp.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(KanbanTask.objects.filter(id=task_id).count(), 0)
def test_project_patch_update_and_permissions(self):
patch_payload = {
"progress_percentage": 50,
"status": "active"
}
# 1. Unauthenticated user gets 401
unauth_resp = self.client.patch(f"/api/v1/projects/{self.project.id}/", patch_payload, format="json")
self.assertEqual(unauth_resp.status_code, status.HTTP_401_UNAUTHORIZED)
# 2. Unrelated user gets 403
self.client.force_authenticate(user=self.unrelated_user)
forbid_resp = self.client.patch(f"/api/v1/projects/{self.project.id}/", patch_payload, format="json")
self.assertEqual(forbid_resp.status_code, status.HTTP_403_FORBIDDEN)
# 3. Collaborating institution editor can update progress
self.client.force_authenticate(user=self.collab_editor)
collab_resp = self.client.patch(f"/api/v1/projects/{self.project.id}/", patch_payload, format="json")
self.assertEqual(collab_resp.status_code, status.HTTP_200_OK)
self.assertEqual(collab_resp.data["progress_percentage"], 50)
def test_project_document_upload_and_delete(self):
self.client.force_authenticate(user=self.collab_editor)
mock_doc = SimpleUploadedFile("bilateral_mou_2026.pdf", b"%PDF-1.4 Mock Bilateral MOU", content_type="application/pdf")
upload_resp = self.client.post(
f"/api/v1/projects/{self.project.id}/documents/",
{
"title": "Bilateral Cooperation MOU Astan-Berlin",
"doc_type": "mou",
"file": mock_doc
},
format="multipart"
)
self.assertEqual(upload_resp.status_code, status.HTTP_201_CREATED)
doc_id = upload_resp.data["id"]
# List documents
list_resp = self.client.get(f"/api/v1/projects/{self.project.id}/documents/")
self.assertEqual(list_resp.status_code, status.HTTP_200_OK)
self.assertEqual(len(list_resp.data), 1)
# Delete document
del_resp = self.client.delete(f"/api/v1/projects/{self.project.id}/documents/{doc_id}/")
self.assertEqual(del_resp.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(ProjectDocument.objects.filter(id=doc_id).count(), 0)