test_api_key_queue_acts_as_owner.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. """An API key acts as its owner (#3256).
  2. A route's own checks on cost centers, archives, library files and folders run
  3. against the key's owner, with the owner's permissions narrowed to the key's
  4. scope flags. Where it applies, each case goes through the key and through the
  5. owner's session, which must agree. A key made before keys had owners owns
  6. nothing and may use no cost center.
  7. """
  8. from __future__ import annotations
  9. from pathlib import Path
  10. from unittest.mock import AsyncMock, patch
  11. from uuid import uuid4
  12. import pytest
  13. from httpx import AsyncClient
  14. from sqlalchemy import select
  15. from backend.app.core.auth import generate_api_key, get_password_hash
  16. from backend.app.core.config import settings as app_settings
  17. from backend.app.models.api_key import APIKey
  18. from backend.app.models.archive import PrintArchive
  19. from backend.app.models.finance import CostCenter, CostCenterMember
  20. from backend.app.models.group import Group
  21. from backend.app.models.library import LibraryFile, LibraryFolder
  22. from backend.app.models.print_batch import PrintBatch
  23. from backend.app.models.print_queue import PrintQueueItem
  24. from backend.app.models.printer import Printer
  25. from backend.app.models.settings import Settings
  26. from backend.app.models.user import User
  27. PASSWORD = "Ownerpass1!"
  28. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  29. async def _set(db_session, key: str, value: str) -> None:
  30. row = await db_session.scalar(select(Settings).where(Settings.key == key))
  31. if row is None:
  32. db_session.add(Settings(key=key, value=value))
  33. else:
  34. row.value = value
  35. @pytest.fixture
  36. async def world(db_session):
  37. """Auth and billing on; a key owner who may queue and reprint only their
  38. own archives, another user, and one printer."""
  39. await _set(db_session, "auth_enabled", "true")
  40. await _set(db_session, "advanced_auth_enabled", "false")
  41. await _set(db_session, "billing_enabled", "true")
  42. group = Group(
  43. name="own-queuers",
  44. description="t",
  45. permissions=[
  46. "queue:create",
  47. "queue:read_own",
  48. "queue:update_own",
  49. "archives:read_own",
  50. "archives:reprint_own",
  51. "library:read_own",
  52. ],
  53. is_system=False,
  54. )
  55. db_session.add(group)
  56. await db_session.flush()
  57. owner = User(username="keyowner", password_hash=get_password_hash(PASSWORD), is_active=True, groups=[group])
  58. other = User(username="otheruser", password_hash=get_password_hash(PASSWORD), is_active=True)
  59. admin = User(username="adminowner", password_hash=get_password_hash(PASSWORD), role="admin", is_active=True)
  60. printer = Printer(
  61. name="P", ip_address="192.168.9.9", serial_number="00M00A3256000001", access_code="12345678", model="X1C"
  62. )
  63. db_session.add_all([owner, other, admin, printer])
  64. await db_session.commit()
  65. return {"owner": owner, "other": other, "admin": admin, "printer": printer}
  66. async def _archive(db_session, created_by_id: int | None, n: int) -> PrintArchive:
  67. archive = PrintArchive(
  68. filename=f"a{n}.3mf",
  69. print_name=f"a{n}",
  70. file_path=f"/tmp/a3256_{n}.3mf", # nosec B108
  71. file_size=1,
  72. content_hash=f"hash3256_{n}",
  73. status="completed",
  74. cost=1.25,
  75. filament_used_grams=50.0,
  76. created_by_id=created_by_id,
  77. )
  78. db_session.add(archive)
  79. await db_session.commit()
  80. await db_session.refresh(archive)
  81. return archive
  82. async def _center(db_session, *, owner_user_id: int | None = None, member_id: int | None = None) -> CostCenter:
  83. center = CostCenter(
  84. name=f"cc-{owner_user_id}-{member_id}",
  85. is_active=True,
  86. is_private=owner_user_id is not None,
  87. owner_user_id=owner_user_id,
  88. )
  89. db_session.add(center)
  90. await db_session.flush()
  91. if member_id is not None:
  92. db_session.add(CostCenterMember(cost_center_id=center.id, user_id=member_id, can_print=True))
  93. await db_session.commit()
  94. await db_session.refresh(center)
  95. return center
  96. async def _key(db_session, owner_id: int | None, *, can_queue: bool = True) -> dict[str, str]:
  97. full_key, key_hash, key_prefix = generate_api_key()
  98. db_session.add(
  99. APIKey(
  100. name="probe",
  101. key_hash=key_hash,
  102. key_prefix=key_prefix,
  103. user_id=owner_id,
  104. can_queue=can_queue,
  105. can_read_status=True,
  106. )
  107. )
  108. await db_session.commit()
  109. return {"X-API-Key": full_key}
  110. async def _login(client: AsyncClient, username: str) -> dict[str, str]:
  111. response = await client.post("/api/v1/auth/login", json={"username": username, "password": PASSWORD})
  112. assert response.status_code == 200, response.text
  113. return {"Authorization": f"Bearer {response.json()['access_token']}"}
  114. async def _queue(client: AsyncClient, headers, printer: Printer, archive: PrintArchive, cost_center_id: int | None):
  115. return await client.post(
  116. "/api/v1/queue/",
  117. json={"printer_id": printer.id, "archive_id": archive.id, "cost_center_id": cost_center_id},
  118. headers=headers,
  119. )
  120. class TestCostCenters:
  121. async def test_another_users_private_cost_center_is_refused(self, async_client, db_session, world):
  122. archive = await _archive(db_session, world["owner"].id, 1)
  123. center = await _center(db_session, owner_user_id=world["other"].id)
  124. key = await _key(db_session, world["owner"].id)
  125. by_key = await _queue(async_client, key, world["printer"], archive, center.id)
  126. by_session = await _queue(
  127. async_client, await _login(async_client, "keyowner"), world["printer"], archive, center.id
  128. )
  129. assert by_key.status_code == by_session.status_code == 403
  130. assert await db_session.scalar(select(PrintQueueItem)) is None
  131. async def test_a_shared_cost_center_needs_membership(self, async_client, db_session, world):
  132. archive = await _archive(db_session, world["owner"].id, 1)
  133. center = await _center(db_session, member_id=world["other"].id)
  134. key = await _key(db_session, world["owner"].id)
  135. assert (await _queue(async_client, key, world["printer"], archive, center.id)).status_code == 403
  136. async def test_the_owners_own_cost_center_works_and_the_item_is_the_owners(self, async_client, db_session, world):
  137. archive = await _archive(db_session, world["owner"].id, 1)
  138. center = await _center(db_session, owner_user_id=world["owner"].id)
  139. key = await _key(db_session, world["owner"].id)
  140. response = await _queue(async_client, key, world["printer"], archive, center.id)
  141. assert response.status_code == 200, response.text
  142. item = await db_session.scalar(select(PrintQueueItem).where(PrintQueueItem.id == response.json()["id"]))
  143. assert item.cost_center_id == center.id
  144. # Credited to the owner, so the scheduler's check at print start has
  145. # someone to check, and the owner sees it under queue:read_own.
  146. assert item.created_by_id == world["owner"].id
  147. async def test_a_cost_center_the_owner_is_a_member_of_works(self, async_client, db_session, world):
  148. archive = await _archive(db_session, world["owner"].id, 1)
  149. center = await _center(db_session, member_id=world["owner"].id)
  150. key = await _key(db_session, world["owner"].id)
  151. assert (await _queue(async_client, key, world["printer"], archive, center.id)).status_code == 200
  152. async def test_an_admins_key_may_use_any_cost_center_like_the_admin(self, async_client, db_session, world):
  153. archive = await _archive(db_session, world["admin"].id, 1)
  154. center = await _center(db_session, owner_user_id=world["other"].id)
  155. key = await _key(db_session, world["admin"].id)
  156. assert (await _queue(async_client, key, world["printer"], archive, center.id)).status_code == 200
  157. async def test_a_key_without_an_owner_may_use_no_cost_center(self, async_client, db_session, world):
  158. archive = await _archive(db_session, None, 1)
  159. center = await _center(db_session, member_id=world["owner"].id)
  160. key = await _key(db_session, None)
  161. assert (await _queue(async_client, key, world["printer"], archive, center.id)).status_code == 403
  162. async def test_moving_an_item_to_a_forbidden_cost_center_is_refused(self, async_client, db_session, world):
  163. # PATCH through a key needs the owner to hold queue:update_all.
  164. group = await db_session.scalar(select(Group).where(Group.name == "own-queuers"))
  165. group.permissions = [*group.permissions, "queue:update_all"]
  166. await db_session.commit()
  167. archive = await _archive(db_session, world["owner"].id, 1)
  168. allowed = await _center(db_session, owner_user_id=world["owner"].id)
  169. forbidden = await _center(db_session, owner_user_id=world["other"].id)
  170. key = await _key(db_session, world["owner"].id)
  171. created = await _queue(async_client, key, world["printer"], archive, allowed.id)
  172. assert created.status_code == 200, created.text
  173. response = await async_client.patch(
  174. f"/api/v1/queue/{created.json()['id']}", json={"cost_center_id": forbidden.id}, headers=key
  175. )
  176. assert response.status_code == 403
  177. item = await db_session.scalar(select(PrintQueueItem).where(PrintQueueItem.id == created.json()["id"]))
  178. await db_session.refresh(item)
  179. assert item.cost_center_id == allowed.id
  180. class TestListingCostCenters:
  181. async def test_a_key_lists_its_owners_cost_centers(self, async_client, db_session, world):
  182. mine = await _center(db_session, owner_user_id=world["owner"].id)
  183. shared = await _center(db_session, member_id=world["owner"].id)
  184. await _center(db_session, owner_user_id=world["other"].id)
  185. key = await _key(db_session, world["owner"].id)
  186. by_key = await async_client.get("/api/v1/finance/cost-centers/mine", headers=key)
  187. by_session = await async_client.get(
  188. "/api/v1/finance/cost-centers/mine", headers=await _login(async_client, "keyowner")
  189. )
  190. assert by_key.status_code == 200, by_key.text
  191. assert {c["id"] for c in by_key.json()} == {mine.id, shared.id}
  192. assert by_key.json() == by_session.json()
  193. async def test_a_key_that_cannot_queue_gets_none(self, async_client, db_session, world):
  194. await _center(db_session, owner_user_id=world["owner"].id)
  195. key = await _key(db_session, world["owner"].id, can_queue=False)
  196. assert (await async_client.get("/api/v1/finance/cost-centers/mine", headers=key)).status_code == 403
  197. async def test_a_key_without_an_owner_has_none(self, async_client, db_session, world):
  198. await _center(db_session, owner_user_id=world["owner"].id)
  199. key = await _key(db_session, None)
  200. response = await async_client.get("/api/v1/finance/cost-centers/mine", headers=key)
  201. assert response.status_code == 200
  202. assert response.json() == []
  203. class TestSources:
  204. async def test_another_users_archive_is_not_found(self, async_client, db_session, world):
  205. await _set(db_session, "billing_enabled", "false")
  206. archive = await _archive(db_session, world["other"].id, 1)
  207. key = await _key(db_session, world["owner"].id)
  208. by_key = await _queue(async_client, key, world["printer"], archive, None)
  209. by_session = await _queue(async_client, await _login(async_client, "keyowner"), world["printer"], archive, None)
  210. assert by_key.status_code == by_session.status_code == 404
  211. async def test_another_users_library_file_is_not_found(self, async_client, db_session, world):
  212. await _set(db_session, "billing_enabled", "false")
  213. # Unique: the checkout's library folder is shared by every xdist worker.
  214. rel_path = f"archive/library/files/probe_3256_{uuid4().hex}.gcode.3mf"
  215. abs_path = Path(app_settings.base_dir) / rel_path
  216. abs_path.parent.mkdir(parents=True, exist_ok=True)
  217. abs_path.write_bytes(b"probe")
  218. library_file = LibraryFile(
  219. filename="probe_3256.gcode.3mf",
  220. file_path=rel_path,
  221. file_size=5,
  222. file_type="3mf",
  223. created_by_id=world["other"].id,
  224. )
  225. db_session.add(library_file)
  226. await db_session.commit()
  227. key = await _key(db_session, world["owner"].id)
  228. try:
  229. response = await async_client.post(
  230. "/api/v1/queue/",
  231. json={"printer_id": world["printer"].id, "library_file_id": library_file.id},
  232. headers=key,
  233. )
  234. finally:
  235. abs_path.unlink(missing_ok=True)
  236. assert response.status_code == 404
  237. async def test_the_owners_own_archive_still_queues(self, async_client, db_session, world):
  238. await _set(db_session, "billing_enabled", "false")
  239. archive = await _archive(db_session, world["owner"].id, 1)
  240. key = await _key(db_session, world["owner"].id)
  241. assert (await _queue(async_client, key, world["printer"], archive, None)).status_code == 200
  242. class TestBatches:
  243. async def test_another_users_batch_cannot_be_changed(self, async_client, db_session, world):
  244. await _set(db_session, "billing_enabled", "false")
  245. batch = PrintBatch(name="theirs", created_by_id=world["other"].id)
  246. db_session.add(batch)
  247. await db_session.commit()
  248. key = await _key(db_session, world["owner"].id)
  249. update = await async_client.patch(f"/api/v1/queue/batches/{batch.id}", json={"name": "mine"}, headers=key)
  250. ungroup = await async_client.post(f"/api/v1/queue/batches/{batch.id}/ungroup", headers=key)
  251. assert update.status_code == 404
  252. assert ungroup.status_code in (403, 404)
  253. await db_session.refresh(batch)
  254. assert batch.name == "theirs"
  255. class TestAnOwnOnlyOwner:
  256. """An owner who may change only their own queue items.
  257. Their key changes their items and no other user's item (see
  258. test_api_key_own_rows.py), and an owner's deactivation stops it.
  259. """
  260. async def _items(self, db_session, world) -> tuple[PrintQueueItem, PrintQueueItem]:
  261. await _set(db_session, "billing_enabled", "false")
  262. mine = await _archive(db_session, world["owner"].id, 1)
  263. theirs = await _archive(db_session, world["other"].id, 2)
  264. own = PrintQueueItem(
  265. printer_id=world["printer"].id, archive_id=mine.id, status="pending", created_by_id=world["owner"].id
  266. )
  267. other = PrintQueueItem(
  268. printer_id=world["printer"].id, archive_id=theirs.id, status="pending", created_by_id=world["other"].id
  269. )
  270. db_session.add_all([own, other])
  271. await db_session.commit()
  272. return own, other
  273. async def test_another_users_item_is_left_alone(self, async_client, db_session, world):
  274. own, other = await self._items(db_session, world)
  275. key = await _key(db_session, world["owner"].id)
  276. patched = await async_client.patch(f"/api/v1/queue/{other.id}", json={"manual_start": True}, headers=key)
  277. bulk = await async_client.patch(
  278. "/api/v1/queue/bulk", json={"item_ids": [own.id, other.id], "manual_start": True}, headers=key
  279. )
  280. deleted = await async_client.delete(f"/api/v1/queue/{other.id}", headers=key)
  281. assert patched.status_code in (403, 404)
  282. assert bulk.status_code == 200, bulk.text
  283. assert bulk.json()["skipped_count"] == 1
  284. assert deleted.status_code in (403, 404)
  285. await db_session.refresh(other)
  286. assert other.manual_start is False
  287. assert await db_session.get(PrintQueueItem, other.id) is not None
  288. async def test_a_deactivated_owners_key_changes_nothing(self, async_client, db_session, world):
  289. """An admin's key, so the deactivation alone is what refuses it."""
  290. own, _other = await self._items(db_session, world)
  291. key = await _key(db_session, world["admin"].id)
  292. world["admin"].is_active = False
  293. await db_session.commit()
  294. patched = await async_client.patch(f"/api/v1/queue/{own.id}", json={"manual_start": True}, headers=key)
  295. deleted = await async_client.delete(f"/api/v1/queue/{own.id}", headers=key)
  296. assert patched.status_code in (401, 403)
  297. assert deleted.status_code in (401, 403)
  298. await db_session.refresh(own)
  299. assert own.manual_start is False
  300. assert await db_session.get(PrintQueueItem, own.id) is not None
  301. @pytest.fixture
  302. async def library_world(db_session, world):
  303. """The same owner, who may also upload, read stats and pipelines, and a
  304. key with the library and status scopes as well."""
  305. await _set(db_session, "billing_enabled", "false")
  306. group = await db_session.scalar(select(Group).where(Group.name == "own-queuers"))
  307. group.permissions = [*group.permissions, "library:upload", "stats:read", "pipelines:read"]
  308. full_key, key_hash, key_prefix = generate_api_key()
  309. db_session.add(
  310. APIKey(
  311. name="library probe",
  312. key_hash=key_hash,
  313. key_prefix=key_prefix,
  314. user_id=world["owner"].id,
  315. can_queue=True,
  316. can_read_status=True,
  317. can_manage_library=True,
  318. )
  319. )
  320. theirs = LibraryFolder(name="theirs", created_by_id=world["other"].id, shared=False)
  321. db_session.add(theirs)
  322. await db_session.commit()
  323. return {**world, "key": {"X-API-Key": full_key}, "their_folder": theirs}
  324. async def _their_file(db_session, world, filename: str = "theirs.stl") -> LibraryFile:
  325. row = LibraryFile(
  326. filename=filename,
  327. file_path=f"library/files/{filename}",
  328. file_type=filename.rsplit(".", 1)[-1],
  329. file_size=1024,
  330. created_by_id=world["other"].id,
  331. )
  332. db_session.add(row)
  333. await db_session.commit()
  334. await db_session.refresh(row)
  335. return row
  336. _SLICE_BODY = {"printer_preset_id": 1, "process_preset_id": 2, "filament_preset_id": 3}
  337. class TestLibrary:
  338. async def test_no_folder_inside_another_users_private_folder(self, async_client, db_session, library_world):
  339. body = {"name": "sneaky", "parent_id": library_world["their_folder"].id}
  340. by_key = await async_client.post("/api/v1/library/folders", json=body, headers=library_world["key"])
  341. by_session = await async_client.post(
  342. "/api/v1/library/folders", json=body, headers=await _login(async_client, "keyowner")
  343. )
  344. assert by_key.status_code == by_session.status_code == 404
  345. async def test_a_keys_folder_is_its_owners_and_not_shared(self, async_client, db_session, library_world):
  346. response = await async_client.post(
  347. "/api/v1/library/folders", json={"name": "mine"}, headers=library_world["key"]
  348. )
  349. assert response.status_code == 200, response.text
  350. folder = await db_session.get(LibraryFolder, response.json()["id"])
  351. assert folder.created_by_id == library_world["owner"].id
  352. assert folder.shared is False
  353. async def test_no_upload_into_another_users_private_folder(self, async_client, db_session, library_world):
  354. response = await async_client.post(
  355. "/api/v1/library/files",
  356. params={"folder_id": library_world["their_folder"].id},
  357. files={"file": ("cube.stl", b"solid cube\nendsolid cube\n", "application/octet-stream")},
  358. headers=library_world["key"],
  359. )
  360. assert response.status_code == 404
  361. async def test_no_combining_another_users_file(self, async_client, db_session, library_world):
  362. theirs = await _their_file(db_session, library_world)
  363. response = await async_client.post(
  364. "/api/v1/library/files/combine",
  365. json={"items": [{"file_id": theirs.id}], "filename": "mix"},
  366. headers=library_world["key"],
  367. )
  368. assert response.status_code == 404
  369. assert response.json()["detail"] == "File not found"
  370. async def test_no_slicing_another_users_file(self, async_client, db_session, library_world):
  371. theirs = await _their_file(db_session, library_world)
  372. response = await async_client.post(
  373. f"/api/v1/library/files/{theirs.id}/slice", json=_SLICE_BODY, headers=library_world["key"]
  374. )
  375. assert response.status_code == 404
  376. assert response.json()["detail"] == "File not found"
  377. async def test_no_queueing_another_users_file_from_the_library(self, async_client, db_session, library_world):
  378. theirs = await _their_file(db_session, library_world, "theirs.gcode.3mf")
  379. response = await async_client.post(
  380. "/api/v1/library/files/add-to-queue",
  381. json={"file_ids": [theirs.id], "printer_id": library_world["printer"].id},
  382. headers=library_world["key"],
  383. )
  384. # The same answer an unknown id gets
  385. assert response.status_code == 400
  386. assert response.json()["detail"]["errors"][0]["error"] == "File not found"
  387. assert await db_session.scalar(select(PrintQueueItem)) is None
  388. class TestArchivesAndPipelines:
  389. async def test_no_slicing_another_users_archive(self, async_client, db_session, library_world):
  390. archive = await _archive(db_session, library_world["other"].id, 1)
  391. response = await async_client.post(
  392. f"/api/v1/archives/{archive.id}/slice", json=_SLICE_BODY, headers=library_world["key"]
  393. )
  394. assert response.status_code == 404
  395. assert response.json()["detail"] == "Archive not found"
  396. async def test_no_per_user_stats_without_the_owners_permission(self, async_client, db_session, library_world):
  397. by_key = await async_client.get(
  398. "/api/v1/archives/stats",
  399. params={"created_by_id": library_world["other"].id},
  400. headers=library_world["key"],
  401. )
  402. by_session = await async_client.get(
  403. "/api/v1/archives/stats",
  404. params={"created_by_id": library_world["other"].id},
  405. headers=await _login(async_client, "keyowner"),
  406. )
  407. assert by_key.status_code == by_session.status_code == 403
  408. async def test_no_pipeline_on_another_users_file(self, async_client, db_session, library_world):
  409. theirs = await _their_file(db_session, library_world)
  410. created = await async_client.post(
  411. "/api/v1/slicer-pipelines/",
  412. json={
  413. "name": "Batch",
  414. "description": None,
  415. "printer_preset": {"source": "local", "id": "1"},
  416. "process_preset": {"source": "local", "id": "2"},
  417. "filament_presets": [{"source": "local", "id": "3"}],
  418. "bed_type": None,
  419. },
  420. headers=await _login(async_client, "adminowner"),
  421. )
  422. assert created.status_code == 201, created.text
  423. response = await async_client.post(
  424. f"/api/v1/slicer-pipelines/{created.json()['id']}/check-eligibility",
  425. json={"source_library_file_id": theirs.id},
  426. headers=library_world["key"],
  427. )
  428. # Refused by the ownership check, not later for the missing file
  429. assert response.status_code == 404
  430. assert response.json()["detail"] == "File not found"
  431. class TestSlicedThroughAKeyIsTheOwners:
  432. """A key without the cloud scope resolves no cloud presets, but what it
  433. slices is still its owner's, as a pipeline run it starts is."""
  434. async def _slice(self, async_client, url, key):
  435. from dataclasses import dataclass
  436. @dataclass
  437. class _Job:
  438. id: int = 1
  439. status: str = "pending"
  440. with patch(
  441. "backend.app.services.slice_dispatch.slice_dispatch.enqueue", new=AsyncMock(return_value=_Job())
  442. ) as enqueue:
  443. response = await async_client.post(url, json=_SLICE_BODY, headers=key)
  444. assert response.status_code == 202, response.text
  445. return enqueue.await_args.kwargs["owner_id"]
  446. async def test_a_library_file(self, async_client, db_session, library_world, tmp_path, monkeypatch):
  447. monkeypatch.setattr(app_settings, "base_dir", tmp_path)
  448. (tmp_path / "mine.3mf").write_bytes(b"PK")
  449. mine = LibraryFile(
  450. filename="mine.3mf",
  451. file_path="mine.3mf",
  452. file_type="3mf",
  453. file_size=2,
  454. created_by_id=library_world["owner"].id,
  455. )
  456. db_session.add(mine)
  457. await db_session.commit()
  458. owner = await self._slice(async_client, f"/api/v1/library/files/{mine.id}/slice", library_world["key"])
  459. assert owner == library_world["owner"].id
  460. async def test_an_archive(self, async_client, db_session, library_world, tmp_path, monkeypatch):
  461. monkeypatch.setattr(app_settings, "base_dir", tmp_path)
  462. (tmp_path / "mine_archive.3mf").write_bytes(b"PK")
  463. archive = await _archive(db_session, library_world["owner"].id, 77)
  464. archive.file_path = "mine_archive.3mf"
  465. await db_session.commit()
  466. owner = await self._slice(async_client, f"/api/v1/archives/{archive.id}/slice", library_world["key"])
  467. assert owner == library_world["owner"].id