test_development_api.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. from __future__ import annotations
  2. from dataclasses import replace
  3. import pytest
  4. class FakeDevelopmentService:
  5. def __init__(self, record):
  6. self.record = record
  7. self.actions = []
  8. def create_job(self, payload, actor_uid):
  9. self.actions.append(("create", payload, actor_uid))
  10. return replace(self.record, actor_uid=actor_uid), True
  11. def list_jobs(self, filters=None):
  12. self.actions.append(("list", filters or {}))
  13. return [self.record]
  14. def get_job(self, uid):
  15. self.actions.append(("get", uid))
  16. return self.record
  17. def retry(self, uid):
  18. self.actions.append(("retry", uid))
  19. return replace(self.record, status="queued", last_error=None)
  20. def cancel(self, uid):
  21. self.actions.append(("cancel", uid))
  22. return replace(self.record, status="cancelled")
  23. @pytest.fixture()
  24. def development_client(monkeypatch):
  25. from flask import request
  26. from app import create_app
  27. from app.api.data_development import routes
  28. from app.core.data_research.models import IngestionJobRecord
  29. from app.core.system import permissions
  30. record = IngestionJobRecord(
  31. uid="00000000-0000-0000-0000-000000000010",
  32. source_uid="00000000-0000-0000-0000-000000000001",
  33. artifact_uid="00000000-0000-0000-0000-000000000002",
  34. job_type="file_extract",
  35. parser_version="csv-v1",
  36. idempotency_key="a" * 64,
  37. actor_uid="editor-1",
  38. parameters={"schema": "public"},
  39. )
  40. service = FakeDevelopmentService(record)
  41. def identity():
  42. header = request.headers.get("Authorization", "")
  43. if header == "Bearer viewer":
  44. return {"id": "viewer-1", "roles": ["viewer"]}
  45. if header == "Bearer editor":
  46. return {"id": "editor-1", "roles": ["editor"]}
  47. if header == "Bearer other-editor":
  48. return {"id": "editor-2", "roles": ["editor"]}
  49. if header == "Bearer admin":
  50. return {"id": "admin-1", "roles": ["admin"]}
  51. return None
  52. monkeypatch.setattr(permissions, "authenticate_request", identity)
  53. monkeypatch.setattr(routes, "get_ingestion_service", lambda: service)
  54. app = create_app()
  55. app.config.update(TESTING=True)
  56. return app.test_client(), service
  57. def job_payload():
  58. return {
  59. "source_uid": "00000000-0000-0000-0000-000000000001",
  60. "artifact_uid": "00000000-0000-0000-0000-000000000002",
  61. "job_type": "file_extract",
  62. "parser_version": "csv-v1",
  63. "parameters": {"schema": "public"},
  64. "password": "must-not-return",
  65. }
  66. def test_ingestion_api_requires_authentication_and_run_permission(development_client):
  67. client, _service = development_client
  68. assert client.post("/api/development/v1/ingestion-jobs", json=job_payload()).status_code == 401
  69. assert client.post(
  70. "/api/development/v1/ingestion-jobs",
  71. json=job_payload(),
  72. headers={"Authorization": "Bearer viewer"},
  73. ).status_code == 403
  74. def test_editor_creates_and_lists_secret_free_jobs(development_client):
  75. client, service = development_client
  76. created = client.post(
  77. "/api/development/v1/ingestion-jobs",
  78. json=job_payload(),
  79. headers={"Authorization": "Bearer editor"},
  80. )
  81. listed = client.get(
  82. "/api/development/v1/ingestion-jobs?status=created",
  83. headers={"Authorization": "Bearer editor"},
  84. )
  85. assert created.status_code == 201
  86. assert created.get_json()["data"]["uid"].endswith("0010")
  87. assert "must-not-return" not in created.get_data(as_text=True)
  88. assert listed.status_code == 200
  89. assert listed.get_json()["data"]["total"] == 1
  90. assert service.actions[-1] == ("list", {"status": "created"})
  91. def test_retry_is_admin_only_and_cancel_is_owner_or_admin(development_client):
  92. client, _service = development_client
  93. uid = "00000000-0000-0000-0000-000000000010"
  94. editor_retry = client.post(
  95. f"/api/development/v1/ingestion-jobs/{uid}/retry",
  96. headers={"Authorization": "Bearer editor"},
  97. )
  98. admin_retry = client.post(
  99. f"/api/development/v1/ingestion-jobs/{uid}/retry",
  100. headers={"Authorization": "Bearer admin"},
  101. )
  102. other_cancel = client.post(
  103. f"/api/development/v1/ingestion-jobs/{uid}/cancel",
  104. headers={"Authorization": "Bearer other-editor"},
  105. )
  106. owner_cancel = client.post(
  107. f"/api/development/v1/ingestion-jobs/{uid}/cancel",
  108. headers={"Authorization": "Bearer editor"},
  109. )
  110. assert editor_retry.status_code == 403
  111. assert admin_retry.status_code == 200
  112. assert other_cancel.status_code == 403
  113. assert owner_cancel.status_code == 200