test_project_import_folders.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. """Project import and export: linked folders and their files."""
  2. from __future__ import annotations
  3. import io
  4. import json
  5. import zipfile
  6. import pytest
  7. from httpx import AsyncClient
  8. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  9. STL = b"solid probe\nendsolid probe\n"
  10. def _zip(folder: str, files: dict[str, bytes]) -> bytes:
  11. buffer = io.BytesIO()
  12. with zipfile.ZipFile(buffer, "w") as zf:
  13. zf.writestr("project.json", json.dumps({"name": "P", "linked_folders": [{"name": folder}]}))
  14. for name, content in files.items():
  15. zf.writestr(f"files/{folder}/{name}", content)
  16. return buffer.getvalue()
  17. async def _import_zip(client: AsyncClient, data: bytes, headers=None):
  18. files = {"file": ("p.zip", io.BytesIO(data), "application/zip")}
  19. return await client.post("/api/v1/projects/import/file", files=files, headers=headers or {})
  20. async def _folder_files(client: AsyncClient, folder_id: int, headers=None) -> list[dict]:
  21. listed = await client.get("/api/v1/library/files", params={"folder_id": folder_id}, headers=headers or {})
  22. return listed.json()
  23. async def test_an_imported_file_can_be_downloaded(async_client: AsyncClient):
  24. imported = await _import_zip(async_client, _zip("Models", {"part.stl": STL}))
  25. assert imported.status_code == 200, imported.text
  26. folders = (await async_client.get("/api/v1/library/folders/by-project/" + str(imported.json()["id"]))).json()
  27. files = await _folder_files(async_client, folders[0]["id"])
  28. downloaded = await async_client.get(f"/api/v1/library/files/{files[0]['id']}/download")
  29. assert [f["filename"] for f in files] == ["part.stl"]
  30. assert (downloaded.status_code, downloaded.content) == (200, STL)
  31. async def test_an_export_carries_the_folder_files(async_client: AsyncClient):
  32. project = (await async_client.post("/api/v1/projects/", json={"name": "P"})).json()
  33. folder = (await async_client.post("/api/v1/library/folders/", json={"name": "Parts"})).json()
  34. await async_client.put(f"/api/v1/library/folders/{folder['id']}", json={"project_id": project["id"]})
  35. uploaded = await async_client.post(
  36. "/api/v1/library/files",
  37. params={"folder_id": folder["id"], "generate_stl_thumbnails": False},
  38. files={"file": ("bracket.stl", io.BytesIO(STL), "model/stl")},
  39. )
  40. assert uploaded.status_code == 200, uploaded.text
  41. exported = await async_client.get(f"/api/v1/projects/{project['id']}/export")
  42. with zipfile.ZipFile(io.BytesIO(exported.content)) as zf:
  43. assert zf.read("files/Parts/bracket.stl") == STL
  44. PERMS = ["projects:read", "projects:create", "library:read_own", "library:upload", "library:update_own"]
  45. async def _users(client: AsyncClient):
  46. from backend.tests.integration.test_printer_scope_1727 import _admin_token, _auth, _group, _user
  47. token = await _admin_token(client)
  48. group = await _group(client, token, "makers", permissions=PERMS)
  49. a = _auth((await _user(client, token, "anna", [group]))[0])
  50. b = _auth((await _user(client, token, "ben", [group]))[0])
  51. return _auth(token), a, b
  52. async def test_an_import_leaves_another_user_folder_of_the_same_name_alone(async_client: AsyncClient):
  53. admin, a, b = await _users(async_client)
  54. bens = (await async_client.post("/api/v1/library/folders/", headers=b, json={"name": "Models"})).json()
  55. from_zip = await _import_zip(async_client, _zip("Models", {"part.stl": STL}), headers=a)
  56. from_json = await async_client.post(
  57. "/api/v1/projects/import", headers=a, json={"name": "J", "linked_folders": [{"name": "Models"}]}
  58. )
  59. assert (from_zip.status_code, from_json.status_code) == (200, 200)
  60. kept = (await async_client.get(f"/api/v1/library/folders/{bens['id']}", headers=admin)).json()
  61. assert kept["project_id"] is None
  62. assert await _folder_files(async_client, bens["id"], headers=admin) == []
  63. linked = (
  64. await async_client.get(f"/api/v1/library/folders/by-project/{from_json.json()['id']}", headers=admin)
  65. ).json()
  66. assert [f["name"] for f in linked] == ["Models"]
  67. assert linked[0]["id"] != bens["id"]
  68. async def test_an_import_links_an_existing_folder_only_with_library_update_all(async_client: AsyncClient):
  69. admin, a, b = await _users(async_client)
  70. annas = (await async_client.post("/api/v1/library/folders/", headers=a, json={"name": "Mine"})).json()
  71. bens = (await async_client.post("/api/v1/library/folders/", headers=b, json={"name": "Shop"})).json()
  72. await async_client.post("/api/v1/library/folders/", headers=a, json={"name": "Shop"})
  73. own = await async_client.post(
  74. "/api/v1/projects/import", headers=a, json={"name": "A", "linked_folders": [{"name": "Mine"}]}
  75. )
  76. as_admin = await async_client.post(
  77. "/api/v1/projects/import", headers=admin, json={"name": "B", "linked_folders": [{"name": "Shop"}]}
  78. )
  79. assert (own.status_code, as_admin.status_code) == (200, 200)
  80. mine = (await async_client.get(f"/api/v1/library/folders/{annas['id']}", headers=admin)).json()
  81. shop = (await async_client.get(f"/api/v1/library/folders/{bens['id']}", headers=admin)).json()
  82. # Linking a folder takes library:update_all, her own included.
  83. assert mine["project_id"] is None
  84. assert shop["project_id"] == as_admin.json()["id"]
  85. async def test_two_imports_into_same_named_folders_keep_their_own_files(async_client: AsyncClient):
  86. admin, a, b = await _users(async_client)
  87. first = await _import_zip(async_client, _zip("Models", {"part.stl": b"solid a\nendsolid a\n"}), headers=a)
  88. second = await _import_zip(async_client, _zip("Models", {"part.stl": b"solid b\nendsolid b\n"}), headers=b)
  89. contents = []
  90. for imported, who in ((first, a), (second, b)):
  91. folder = (
  92. await async_client.get(f"/api/v1/library/folders/by-project/{imported.json()['id']}", headers=admin)
  93. ).json()[0]
  94. file = (await _folder_files(async_client, folder["id"], headers=who))[0]
  95. contents.append((await async_client.get(f"/api/v1/library/files/{file['id']}/download", headers=who)).content)
  96. assert contents == [b"solid a\nendsolid a\n", b"solid b\nendsolid b\n"]
  97. async def test_an_export_still_carries_files_imported_by_older_versions(async_client: AsyncClient, db_session):
  98. from backend.app.api.routes.library import get_library_dir
  99. from backend.app.models.library import LibraryFile
  100. project = (await async_client.post("/api/v1/projects/", json={"name": "Old"})).json()
  101. folder = (await async_client.post("/api/v1/library/folders/", json={"name": "OldImport"})).json()
  102. await async_client.put(f"/api/v1/library/folders/{folder['id']}", json={"project_id": project["id"]})
  103. disk = get_library_dir() / "OldImport" / "legacy.stl"
  104. disk.parent.mkdir(parents=True, exist_ok=True)
  105. disk.write_bytes(STL)
  106. db_session.add(
  107. LibraryFile(
  108. folder_id=folder["id"],
  109. filename="legacy.stl",
  110. file_path="OldImport/legacy.stl",
  111. file_type="model",
  112. file_size=1,
  113. )
  114. )
  115. await db_session.commit()
  116. try:
  117. exported = await async_client.get(f"/api/v1/projects/{project['id']}/export")
  118. with zipfile.ZipFile(io.BytesIO(exported.content)) as zf:
  119. assert zf.read("files/OldImport/legacy.stl") == STL
  120. finally:
  121. disk.unlink()
  122. disk.parent.rmdir()
  123. async def test_an_export_carries_only_files_the_caller_may_open(async_client: AsyncClient):
  124. from backend.tests.integration.test_printer_scope_1727 import _admin_token, _auth, _group, _user
  125. token = await _admin_token(async_client)
  126. admin = _auth(token)
  127. own = await _group(async_client, token, "own", permissions=["projects:read", "library:read_own", "library:upload"])
  128. none = await _group(async_client, token, "none", permissions=["projects:read"])
  129. reader = _auth((await _user(async_client, token, "reader", [own]))[0])
  130. outsider = _auth((await _user(async_client, token, "outsider", [none]))[0])
  131. project = (await async_client.post("/api/v1/projects/", headers=admin, json={"name": "P"})).json()
  132. folder = (await async_client.post("/api/v1/library/folders/", headers=admin, json={"name": "Private"})).json()
  133. await async_client.put(f"/api/v1/library/folders/{folder['id']}", headers=admin, json={"project_id": project["id"]})
  134. shared = (await async_client.post("/api/v1/library/folders/", headers=admin, json={"name": "Shared"})).json()
  135. await async_client.put(
  136. f"/api/v1/library/folders/{shared['id']}", headers=admin, json={"project_id": project["id"], "shared": True}
  137. )
  138. for who, folder_id, name in (
  139. (admin, folder["id"], "secret.stl"),
  140. (admin, shared["id"], "theirs.stl"),
  141. (reader, shared["id"], "mine.stl"),
  142. ):
  143. uploaded = await async_client.post(
  144. "/api/v1/library/files",
  145. headers=who,
  146. params={"folder_id": folder_id, "generate_stl_thumbnails": False},
  147. files={"file": (name, io.BytesIO(STL), "model/stl")},
  148. )
  149. assert uploaded.status_code == 200, uploaded.text
  150. def contents(response) -> tuple[list[str], list[str]]:
  151. with zipfile.ZipFile(io.BytesIO(response.content)) as zf:
  152. folders = [f["name"] for f in json.loads(zf.read("project.json"))["linked_folders"]]
  153. return folders, [n for n in zf.namelist() if n.startswith("files/")]
  154. exported = {
  155. name: contents(await async_client.get(f"/api/v1/projects/{project['id']}/export", headers=who))
  156. for name, who in (("admin", admin), ("reader", reader), ("outsider", outsider))
  157. }
  158. assert exported["admin"] == (
  159. ["Private", "Shared"],
  160. ["files/Private/secret.stl", "files/Shared/mine.stl", "files/Shared/theirs.stl"],
  161. )
  162. assert exported["reader"] == (["Shared"], ["files/Shared/mine.stl"])
  163. as_json = await async_client.get(
  164. f"/api/v1/projects/{project['id']}/export", headers=reader, params={"format": "json"}
  165. )
  166. assert [(f["name"], [x["filename"] for x in f["files"]]) for f in as_json.json()["linked_folders"]] == [
  167. ("Shared", ["mine.stl"])
  168. ]
  169. assert exported["outsider"] == ([], [])