test_project_priority_status_repair.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. """Projects stored with an unknown priority or status get a valid one at start.
  2. Older builds stored a file-imported project without a priority as ``0`` and
  3. took any priority on create and any status on import. The project editor sends
  4. both back on every save and refused them, so such a project could not be
  5. edited at all, not even its name.
  6. """
  7. from __future__ import annotations
  8. import pytest
  9. from httpx import AsyncClient
  10. from sqlalchemy import select, text
  11. from sqlalchemy.ext.asyncio import AsyncSession
  12. from backend.app.core.database import _migrate_project_priority_status
  13. from backend.app.models.project import Project
  14. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  15. ROWS = [
  16. # (priority, status) as stored -> as repaired
  17. (("0", "active"), ("normal", "active")),
  18. (("whenever", "done"), ("normal", "active")),
  19. (("urgent", "archived"), ("urgent", "archived")),
  20. (("low", "completed"), ("low", "completed")),
  21. ]
  22. async def _seed(session: AsyncSession) -> list[int]:
  23. ids = []
  24. for (priority, status), _ in ROWS:
  25. project = Project(name=f"{priority}/{status}")
  26. session.add(project)
  27. await session.flush()
  28. # Raw UPDATE: the API no longer stores such values.
  29. await session.execute(
  30. text("UPDATE projects SET priority = :p, status = :s WHERE id = :id"),
  31. {"p": priority, "s": status, "id": project.id},
  32. )
  33. ids.append(project.id)
  34. await session.commit()
  35. return ids
  36. async def _repair(session: AsyncSession) -> None:
  37. await _migrate_project_priority_status(await session.connection())
  38. await session.commit()
  39. async def _stored(session: AsyncSession, ids: list[int]) -> list[tuple[str, str]]:
  40. session.expire_all()
  41. rows = (
  42. await session.execute(select(Project.id, Project.priority, Project.status).where(Project.id.in_(ids)))
  43. ).all()
  44. by_id = {row[0]: (row[1], row[2]) for row in rows}
  45. return [by_id[i] for i in ids]
  46. async def test_unknown_values_are_repaired_and_valid_ones_kept(db_session: AsyncSession):
  47. ids = await _seed(db_session)
  48. await _repair(db_session)
  49. assert await _stored(db_session, ids) == [expected for _, expected in ROWS]
  50. async def test_a_second_run_changes_nothing(db_session: AsyncSession):
  51. ids = await _seed(db_session)
  52. await _repair(db_session)
  53. await _repair(db_session)
  54. assert await _stored(db_session, ids) == [expected for _, expected in ROWS]
  55. async def test_a_repaired_project_can_be_edited_again(async_client: AsyncClient, db_session: AsyncSession):
  56. project_id = (await _seed(db_session))[0]
  57. project = (await async_client.get(f"/api/v1/projects/{project_id}")).json()
  58. # What the editor sends back on save: the stored priority and status.
  59. edit = {"name": "Renamed", "priority": project["priority"], "status": project["status"]}
  60. assert (await async_client.patch(f"/api/v1/projects/{project_id}", json=edit)).status_code == 400
  61. await _repair(db_session)
  62. project = (await async_client.get(f"/api/v1/projects/{project_id}")).json()
  63. edit = {"name": "Renamed", "priority": project["priority"], "status": project["status"]}
  64. response = await async_client.patch(f"/api/v1/projects/{project_id}", json=edit)
  65. assert response.status_code == 200, response.text
  66. assert response.json()["name"] == "Renamed"