Skip to content

Commit ffd5955

Browse files
committed
projects(import): add tests for import command
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
1 parent 23651c0 commit ffd5955

4 files changed

Lines changed: 218 additions & 1 deletion

File tree

rdmo/projects/management/commands/import_projects.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def handle(self, *args, **options):
9696
create_new_users = options["create_new_users"]
9797
as_user_id = options.get("as_user_id")
9898
as_username = options.get("as_username")
99-
project_ids = sorted(set(options.get("projects")))
99+
project_ids = sorted(set(options.get("projects") or []))
100100

101101
# sanity-check plugin key
102102
if get_plugin("PROJECT_IMPORTS", import_format) is None:

rdmo/projects/tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from pathlib import Path
2+
13
import pytest
24

35
from django.apps import apps
@@ -13,3 +15,9 @@ def enable_project_views_sync(settings):
1315
def enable_project_tasks_sync(settings):
1416
settings.PROJECT_TASKS_SYNC = True
1517
apps.get_app_config('projects').ready()
18+
19+
@pytest.fixture
20+
def project_xml(settings):
21+
xml_file = Path(settings.BASE_DIR) / 'xml/project.xml'
22+
assert xml_file.exists(), f"Missing test XML at {xml_file}"
23+
return xml_file

rdmo/projects/tests/helpers/xml.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import io
2+
import xml.etree.ElementTree as ET
3+
from xml.sax.saxutils import XMLGenerator
4+
5+
from rdmo.projects.renderers import XMLRenderer
6+
7+
8+
def add_memberships_to_xml(xml_path: str, members: list[dict]) -> None:
9+
"""
10+
Replace or add <memberships> in the given project.xml using the real XMLRenderer.
11+
`members` is a list of {"role": ..., "user": {...}} dicts.
12+
"""
13+
# 1) render just the <memberships> fragment with the real renderer
14+
buf = io.StringIO()
15+
xml = XMLGenerator(buf, encoding="utf-8")
16+
xml.startDocument()
17+
xml.startElement("root", {}) # wrapper so we can parse as a fragment
18+
xml.startElement("memberships", {})
19+
20+
renderer = XMLRenderer()
21+
for m in members:
22+
renderer.render_member(xml, m)
23+
24+
xml.endElement("memberships")
25+
xml.endElement("root")
26+
xml.endDocument()
27+
28+
frag_root = ET.fromstring(buf.getvalue())
29+
memberships_fragment = frag_root.find("memberships")
30+
31+
# 2) load target project.xml and replace existing <memberships>
32+
tree = ET.parse(xml_path)
33+
root = tree.getroot()
34+
old = root.find("memberships")
35+
if old is not None:
36+
root.remove(old)
37+
root.append(memberships_fragment)
38+
39+
tree.write(xml_path, encoding="utf-8", xml_declaration=True)
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import shutil
2+
3+
import pytest
4+
5+
from django.core.management import call_command
6+
7+
from rdmo.projects.models import Membership, Project
8+
from rdmo.projects.tests.helpers.xml import add_memberships_to_xml
9+
10+
11+
@pytest.mark.django_db
12+
@pytest.mark.parametrize("include_memberships", [True, False])
13+
@pytest.mark.parametrize("xml_has_memberships", [True, False])
14+
def test_import_projects_memberships_toggle(tmp_path, project_xml, capsys, include_memberships, xml_has_memberships):
15+
16+
xml_path = tmp_path / "project_with_members.xml"
17+
shutil.copyfile(project_xml, xml_path)
18+
19+
if xml_has_memberships:
20+
add_memberships_to_xml(
21+
str(xml_path),
22+
members=[
23+
{"role": "owner", "user": {"username": "owner"}},
24+
{"role": "author", "user": {"username": "author"}},
25+
{"role": "guest", "user": {"username": "guest"}},
26+
],
27+
)
28+
29+
before = Project.objects.count()
30+
31+
args = ["import_projects", "--files", str(xml_path)]
32+
if include_memberships:
33+
args.append("--include-memberships")
34+
35+
call_command(*args)
36+
out = capsys.readouterr().out
37+
assert "→ Importing" in out
38+
assert "imported successfully" in out
39+
40+
assert Project.objects.count() == before + 1
41+
new_project = Project.objects.order_by("-pk").first()
42+
43+
expected = {("owner", "owner"), ("author", "author"), ("guest", "guest")}
44+
if include_memberships and xml_has_memberships:
45+
actual = {
46+
(m.user.username, m.role)
47+
for m in Membership.objects.filter(project=new_project).select_related("user")
48+
}
49+
assert actual == expected
50+
else:
51+
assert not Membership.objects.filter(project=new_project).exists()
52+
53+
54+
@pytest.mark.django_db
55+
def test_import_projects_from_files_explicit(tmp_path, project_xml, capsys):
56+
"""Import a project via explicit --files path (no memberships)."""
57+
xml_path = tmp_path / "project.xml"
58+
shutil.copyfile(project_xml, xml_path)
59+
60+
before = Project.objects.count()
61+
call_command("import_projects", "--files", str(xml_path))
62+
63+
out = capsys.readouterr().out
64+
assert "→ Importing" in out
65+
assert "imported successfully" in out
66+
assert Project.objects.count() == before + 1
67+
68+
69+
@pytest.mark.django_db
70+
def test_import_projects_dir_scan_with_filter_and_missing(tmp_path, project_xml, capsys):
71+
"""
72+
Import via --dir with one valid numeric folder and one missing.
73+
Expect a warning and a successful import of the existing one.
74+
"""
75+
good_id = 42
76+
(tmp_path / str(good_id)).mkdir(parents=True)
77+
shutil.copyfile(project_xml, tmp_path / str(good_id) / "project.xml")
78+
(tmp_path / "999999").mkdir() # empty folder
79+
80+
before = Project.objects.count()
81+
call_command("import_projects", "--dir", str(tmp_path), "--projects", str(good_id), "999999")
82+
83+
out = capsys.readouterr().out
84+
assert ("Some projects could not be found" in out) or ('No XML file found' in out)
85+
assert "imported successfully" in out
86+
assert Project.objects.count() == before + 1
87+
88+
89+
@pytest.mark.django_db
90+
def test_import_projects_files_skips_non_xml(tmp_path, project_xml, capsys):
91+
"""Passing a non-XML alongside a valid XML: skip with warning, still import XML."""
92+
good_xml = tmp_path / "project.xml"
93+
shutil.copyfile(project_xml, good_xml)
94+
95+
junk = tmp_path / "notes.txt"
96+
junk.write_text("hello")
97+
98+
before = Project.objects.count()
99+
call_command("import_projects", "--files", str(junk), str(good_xml))
100+
101+
out = capsys.readouterr().out
102+
assert "Skipping non-XML file" in out
103+
assert "imported successfully" in out
104+
assert Project.objects.count() == before + 1
105+
106+
107+
@pytest.mark.django_db
108+
@pytest.mark.parametrize("export_include_memberships", [True, False])
109+
@pytest.mark.parametrize("import_include_memberships", [True, False])
110+
def test_roundtrip_export_then_import_memberships_toggle(
111+
tmp_path, capsys, export_include_memberships, import_include_memberships
112+
):
113+
"""
114+
Round-trip: export project id=1 to XML, then import it back.
115+
Memberships in the imported project should exist iff BOTH:
116+
- export included memberships, and
117+
- import requested memberships.
118+
"""
119+
project = Project.objects.get(id=1)
120+
src_members = set(
121+
Membership.objects.filter(project=project)
122+
.values_list("user_id", "role")
123+
)
124+
# --- export ---
125+
export_args = [
126+
"export_projects",
127+
"--projects", str(project.id),
128+
"--export-mode", "project",
129+
"--format", "xml",
130+
"--path", str(tmp_path),
131+
]
132+
if export_include_memberships:
133+
assert src_members, "should not be empty here"
134+
export_args.append("--include-memberships")
135+
136+
call_command(*export_args)
137+
138+
out = capsys.readouterr().out
139+
assert "Exported 1 project(s) to" in out
140+
141+
# --- import ---
142+
import_args = ["import_projects", "--dir", str(tmp_path)]
143+
if import_include_memberships:
144+
import_args.append("--include-memberships")
145+
146+
before_count = Project.objects.count()
147+
call_command(*import_args)
148+
149+
out = capsys.readouterr().out
150+
assert "→ Importing" in out
151+
assert "imported successfully" in out
152+
153+
# a new project must have been created
154+
assert Project.objects.count() == before_count + 1
155+
imported = Project.objects.exclude(pk=project.pk).order_by("-pk").first()
156+
157+
imported_members = set(
158+
Membership.objects.filter(project=imported)
159+
.values_list("user_id", "role")
160+
)
161+
162+
expect_members = export_include_memberships and import_include_memberships
163+
164+
if expect_members:
165+
# imported memberships should match the source memberships (set compare)
166+
assert imported_members, "should not be empty here"
167+
assert imported_members == src_members
168+
else:
169+
# no memberships should have been imported
170+
assert imported_members == set()

0 commit comments

Comments
 (0)