test_project_bom_archive_link.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. """A BOM line links only to an archive the caller can see, and shows no other.
  2. The BOM routes stored any archive id and answered with that archive's print
  3. name, looked up by id alone. A member of a group limited to some printers
  4. could read the name of any archive (404 when asked for directly) by linking a
  5. BOM line to it, and every reader of the project saw the names of archives
  6. from printers they have no access to. An unknown id was stored as well (a
  7. server error on PostgreSQL).
  8. """
  9. from __future__ import annotations
  10. import pytest
  11. from httpx import AsyncClient
  12. from backend.tests.integration.test_printer_scope_1727 import TEAM_PERMISSIONS, _admin_token, _auth, _group, _user
  13. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  14. async def _member(client: AsyncClient, admin: str, printer_ids: list[int]) -> dict[str, str]:
  15. perms = await _group(client, admin, "perms", permissions=[*TEAM_PERMISSIONS, "projects:read", "projects:update"])
  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 _project(client: AsyncClient, headers: dict[str, str] | None = None) -> int:
  19. response = await client.post("/api/v1/projects/", headers=headers or {}, json={"name": "P"})
  20. assert response.status_code == 200, response.text
  21. return response.json()["id"]
  22. async def test_a_member_cannot_link_an_archive_of_another_printer(
  23. async_client: AsyncClient, printer_factory, archive_factory
  24. ):
  25. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  26. admin = await _admin_token(async_client)
  27. member = await _member(async_client, admin, [a.id])
  28. project = await _project(async_client, _auth(admin))
  29. hidden = await archive_factory(b.id, print_name="team-b")
  30. created = await async_client.post(
  31. f"/api/v1/projects/{project}/bom", headers=member, json={"name": "x", "archive_id": hidden.id}
  32. )
  33. line = await async_client.post(f"/api/v1/projects/{project}/bom", headers=member, json={"name": "y"})
  34. updated = await async_client.patch(
  35. f"/api/v1/projects/{project}/bom/{line.json()['id']}", headers=member, json={"archive_id": hidden.id}
  36. )
  37. assert created.status_code == 400, created.text
  38. assert created.json()["detail"] == "Archive not found"
  39. assert updated.status_code == 400, updated.text
  40. assert updated.json()["detail"] == "Archive not found"
  41. listed = (await async_client.get(f"/api/v1/projects/{project}/bom", headers=_auth(admin))).json()
  42. assert [(i["name"], i["archive_id"]) for i in listed] == [("y", None)]
  43. async def test_a_hidden_archive_linked_by_an_admin_reads_as_missing(
  44. async_client: AsyncClient, printer_factory, archive_factory
  45. ):
  46. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  47. admin = await _admin_token(async_client)
  48. member = await _member(async_client, admin, [a.id])
  49. project = await _project(async_client, _auth(admin))
  50. hidden, visible = await archive_factory(b.id, print_name="team-b"), await archive_factory(a.id, print_name="ours")
  51. for name, archive in (("hidden", hidden), ("visible", visible)):
  52. response = await async_client.post(
  53. f"/api/v1/projects/{project}/bom", headers=_auth(admin), json={"name": name, "archive_id": archive.id}
  54. )
  55. assert response.json()["archive_name"] == archive.print_name
  56. as_member = (await async_client.get(f"/api/v1/projects/{project}/bom", headers=member)).json()
  57. as_admin = (await async_client.get(f"/api/v1/projects/{project}/bom", headers=_auth(admin))).json()
  58. assert [(i["name"], i["archive_id"], i["archive_name"]) for i in as_member] == [
  59. ("hidden", None, None),
  60. ("visible", visible.id, "ours"),
  61. ]
  62. assert [i["archive_name"] for i in as_admin] == ["team-b", "ours"]
  63. async def test_editing_a_line_shows_no_hidden_archive(async_client: AsyncClient, printer_factory, archive_factory):
  64. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  65. admin = await _admin_token(async_client)
  66. member = await _member(async_client, admin, [a.id])
  67. project = await _project(async_client, _auth(admin))
  68. hidden = await archive_factory(b.id, print_name="team-b")
  69. line = await async_client.post(
  70. f"/api/v1/projects/{project}/bom", headers=_auth(admin), json={"name": "x", "archive_id": hidden.id}
  71. )
  72. response = await async_client.patch(
  73. f"/api/v1/projects/{project}/bom/{line.json()['id']}", headers=member, json={"quantity_acquired": 1}
  74. )
  75. assert response.status_code == 200, response.text
  76. assert (response.json()["archive_id"], response.json()["archive_name"]) == (None, None)
  77. # The link itself stays: the member only does not see it.
  78. as_admin = (await async_client.get(f"/api/v1/projects/{project}/bom", headers=_auth(admin))).json()
  79. assert as_admin[0]["archive_id"] == hidden.id
  80. @pytest.mark.parametrize("archive_id", [9999])
  81. async def test_an_unknown_archive_is_refused(async_client: AsyncClient, archive_id):
  82. project = await _project(async_client)
  83. response = await async_client.post(f"/api/v1/projects/{project}/bom", json={"name": "x", "archive_id": archive_id})
  84. assert response.status_code == 400, response.text
  85. assert response.json()["detail"] == "Archive not found"
  86. async def test_archive_0_means_no_archive(async_client: AsyncClient):
  87. project = await _project(async_client)
  88. response = await async_client.post(f"/api/v1/projects/{project}/bom", json={"name": "x", "archive_id": 0})
  89. assert response.status_code == 200, response.text
  90. assert response.json()["archive_id"] is None
  91. async def test_a_visible_archive_can_be_linked_and_cleared(async_client: AsyncClient, printer_factory, archive_factory):
  92. a = await printer_factory(name="A")
  93. project = await _project(async_client)
  94. archive = await archive_factory(a.id, print_name="ours")
  95. created = await async_client.post(f"/api/v1/projects/{project}/bom", json={"name": "x", "archive_id": archive.id})
  96. cleared = await async_client.patch(f"/api/v1/projects/{project}/bom/{created.json()['id']}", json={"archive_id": 0})
  97. assert (created.json()["archive_id"], created.json()["archive_name"]) == (archive.id, "ours")
  98. assert (cleared.json()["archive_id"], cleared.json()["archive_name"]) == (None, None)
  99. async def test_resending_a_hidden_link_answers_as_a_missing_archive(
  100. async_client: AsyncClient, printer_factory, archive_factory
  101. ):
  102. # Otherwise 200 for the hidden current id against 400 for any other id would
  103. # tell the member which archive the line links to.
  104. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  105. admin = await _admin_token(async_client)
  106. member = await _member(async_client, admin, [a.id])
  107. project = await _project(async_client, _auth(admin))
  108. hidden = await archive_factory(b.id)
  109. line = await async_client.post(
  110. f"/api/v1/projects/{project}/bom", headers=_auth(admin), json={"name": "x", "archive_id": hidden.id}
  111. )
  112. url = f"/api/v1/projects/{project}/bom/{line.json()['id']}"
  113. same = await async_client.patch(url, headers=member, json={"archive_id": hidden.id})
  114. other = await async_client.patch(url, headers=member, json={"archive_id": 9999})
  115. assert (
  116. (same.status_code, same.json()) == (other.status_code, other.json()) == (400, {"detail": "Archive not found"})
  117. )