test_bulk_archive_routes_scope.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. """Bulk archive actions keep to the caller's printers.
  2. Recalculating costs and rescanning all archives went over every archive,
  3. so a member of a group limited to some printers rewrote other printers'
  4. archives and got their ids back in the error list.
  5. """
  6. from __future__ import annotations
  7. import pytest
  8. from httpx import AsyncClient
  9. from sqlalchemy import select
  10. from backend.app.models.archive import PrintArchive
  11. from backend.tests.integration.test_printer_scope_1727 import TEAM_PERMISSIONS, _admin_token, _auth, _group, _user
  12. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  13. async def _member(client: AsyncClient, printer_ids: list[int]) -> dict[str, str]:
  14. admin = await _admin_token(client)
  15. perms = await _group(client, admin, "perms", permissions=[*TEAM_PERMISSIONS, "archives:update_all"])
  16. team = await _group(client, admin, "team", printer_ids=printer_ids)
  17. return _auth((await _user(client, admin, "member", [perms, team]))[0])
  18. async def test_rescan_all_reports_only_the_team_printers_archives(
  19. async_client: AsyncClient, printer_factory, archive_factory
  20. ):
  21. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  22. # No file behind either archive, so each one lands in the error list.
  23. on_a = await archive_factory(a.id, file_path="archives/missing-a.3mf")
  24. await archive_factory(b.id, file_path="archives/missing-b.3mf")
  25. member = await _member(async_client, [a.id])
  26. response = await async_client.post("/api/v1/archives/rescan-all", headers=member)
  27. assert response.status_code == 200, response.text
  28. assert [error["id"] for error in response.json()["errors"]] == [on_a.id]
  29. async def test_recalculating_costs_leaves_other_printers_alone(
  30. async_client: AsyncClient, db_session, printer_factory, archive_factory
  31. ):
  32. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  33. on_a = await archive_factory(a.id, cost=99.0, filament_used_grams=100, filament_type="PLA")
  34. on_b = await archive_factory(b.id, cost=99.0, filament_used_grams=100, filament_type="PLA")
  35. ids = (on_a.id, on_b.id)
  36. member = await _member(async_client, [a.id])
  37. response = await async_client.post("/api/v1/archives/recalculate-costs", headers=member)
  38. assert response.status_code == 200, response.text
  39. db_session.expire_all()
  40. costs = dict(
  41. (await db_session.execute(select(PrintArchive.id, PrintArchive.cost).where(PrintArchive.id.in_(ids)))).all()
  42. )
  43. assert costs[ids[0]] == 2.5 # 100 g at the default 25.00 per kg
  44. assert costs[ids[1]] == 99.0
  45. async def test_tags_keep_to_the_team_printers(async_client: AsyncClient, db_session, printer_factory, archive_factory):
  46. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  47. on_a = await archive_factory(a.id, tags="shared, alpha")
  48. on_b = await archive_factory(b.id, tags="shared, beta")
  49. ids = (on_a.id, on_b.id)
  50. member = await _member(async_client, [a.id])
  51. listed = await async_client.get("/api/v1/archives/tags", headers=member)
  52. renamed = await async_client.put("/api/v1/archives/tags/shared", headers=member, json={"new_name": "common"})
  53. deleted = await async_client.delete("/api/v1/archives/tags/common", headers=member)
  54. assert sorted(t["name"] for t in listed.json()) == ["alpha", "shared"]
  55. assert (renamed.json()["affected"], deleted.json()["affected"]) == (1, 1)
  56. db_session.expire_all()
  57. tags = dict(
  58. (await db_session.execute(select(PrintArchive.id, PrintArchive.tags).where(PrintArchive.id.in_(ids)))).all()
  59. )
  60. assert tags[ids[1]] == "shared, beta"
  61. async def test_duplicates_and_similar_show_only_visible_archives(
  62. async_client: AsyncClient, printer_factory, archive_factory
  63. ):
  64. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  65. mine = await archive_factory(a.id, print_name="Benchy", content_hash="h1")
  66. same_team = await archive_factory(a.id, print_name="Benchy", content_hash="h1")
  67. await archive_factory(b.id, print_name="Benchy", content_hash="h1")
  68. member = await _member(async_client, [a.id])
  69. duplicates = await async_client.get(f"/api/v1/archives/{mine.id}/duplicates", headers=member)
  70. detail = await async_client.get(f"/api/v1/archives/{mine.id}", headers=member)
  71. similar = await async_client.get(f"/api/v1/archives/{mine.id}/similar", headers=member)
  72. assert [d["id"] for d in duplicates.json()["duplicates"]] == [same_team.id]
  73. assert [d["id"] for d in detail.json()["duplicates"]] == [same_team.id]
  74. assert {s["archive"]["id"] for s in similar.json()} == {same_team.id}
  75. async def test_backfill_hashes_reports_only_the_team_printers_archives(
  76. async_client: AsyncClient, printer_factory, archive_factory
  77. ):
  78. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  79. on_a = await archive_factory(a.id, file_path="archives/none-a.3mf", content_hash=None)
  80. await archive_factory(b.id, file_path="archives/none-b.3mf", content_hash=None)
  81. member = await _member(async_client, [a.id])
  82. response = await async_client.post("/api/v1/archives/backfill-hashes", headers=member)
  83. assert [e["id"] for e in response.json()["errors"]] == [on_a.id]
  84. async def test_uploading_a_source_by_name_matches_only_the_team_printers(
  85. async_client: AsyncClient, printer_factory, archive_factory
  86. ):
  87. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  88. await archive_factory(b.id, print_name="Secret Part")
  89. member = await _member(async_client, [a.id])
  90. response = await async_client.post(
  91. "/api/v1/archives/upload-source",
  92. headers=member,
  93. params={"print_name": "Secret"},
  94. files={"file": ("Secret Part.3mf", b"PK\x03\x04", "application/octet-stream")},
  95. )
  96. assert response.status_code == 404, response.text
  97. assert "Secret Part" not in response.text
  98. async def test_the_list_counts_duplicates_among_visible_archives_only(
  99. async_client: AsyncClient, printer_factory, archive_factory
  100. ):
  101. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  102. await archive_factory(b.id, print_name="Benchy", content_hash="h2") # older, on B
  103. mine = await archive_factory(a.id, print_name="Benchy", content_hash="h2")
  104. member = await _member(async_client, [a.id])
  105. listed = {row["id"]: row for row in (await async_client.get("/api/v1/archives/", headers=member)).json()}
  106. assert listed[mine.id]["duplicate_count"] in (0, None)
  107. assert listed[mine.id]["original_archive_id"] is None
  108. async def test_the_no_3mf_warning_ignores_other_printers(async_client: AsyncClient, printer_factory, archive_factory):
  109. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  110. await archive_factory(b.id, extra_data={"no_3mf_available": True, "no_3mf_reason": "internal_storage"})
  111. member = await _member(async_client, [a.id])
  112. response = await async_client.get("/api/v1/archives/no-3mf-warning", headers=member)
  113. assert response.json() == {"has_fallback": False, "reason": None}