(summary) add multi-tenant support and v2 tasks / API

Add multitenancy support to Summary sub-app. The V1 routes / tasks
behave like before, with the default tenant being "meet".

V2 routes / tasks support being called frm any tenant, and don't have
meet related logic.
V2 tasks are created in separate queues to avoid mix / match,i
This commit is contained in:
Florent Chehab
2026-03-17 14:23:51 +01:00
parent 7e422e5846
commit 5a70604f01
24 changed files with 1310 additions and 99 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ class TestHeartbeat:
"""Tests for the /__heartbeat__ endpoint."""
def test_returns_200(self, client):
"""The heartbeat endpoint responds with 200 OK without a token."""
"""The heartbeat endpoint responds with 200 OK without an api_key."""
response = client.get("/__heartbeat__")
assert response.status_code == 200
@@ -15,7 +15,7 @@ class TestLBHeartbeat:
"""Tests for the /__lbheartbeat__ endpoint."""
def test_returns_200(self, client):
"""The load-balancer heartbeat endpoint responds with 200 OK without a token."""
"""The LB heartbeat endpoint responds with 200 OK without an api_key."""
response = client.get("/__lbheartbeat__")
assert response.status_code == 200
+1 -1
View File
@@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch
class TestTasks:
"""Tests for the /tasks endpoint."""
"""Tests for the /v1/tasks endpoint."""
@patch(
"summary.api.route.tasks.process_audio_transcribe_summarize_v2.apply_async",
+126
View File
@@ -0,0 +1,126 @@
"""Integration tests for the V2 task API endpoints."""
from unittest.mock import MagicMock, patch
class TestTasksV2:
"""Tests for the /v2/async-jobs-jobs endpoints."""
@patch(
"summary.api.route.tasks_v2.process_audio_transcribe_v2_task.apply_async",
return_value=MagicMock(id="transcribe-task-id-abc"),
)
def test_create_transcribe_task_v2_returns_task_id(self, mock_apply_async, client):
"""POST /async-jobs/transcribe creates a task and injects tenant_id."""
response = client.post(
"/api/v2/async-jobs/transcribe",
headers={"Authorization": "Bearer test-api-token"},
json={
"user_sub": "remote-001",
"cloud_storage_url": "https://example.com/audio.mp3",
"language": "en",
"context_language": "fr",
},
)
assert response.status_code == 200
assert response.json() == {
"job_id": "transcribe-task-id-abc",
"message": "Transcribe job created",
}
args = mock_apply_async.call_args.kwargs["args"]
assert args == [
{
"user_sub": "remote-001",
"cloud_storage_url": "https://example.com/audio.mp3",
"language": "en",
"context_language": "fr",
"tenant_id": "test-tenant",
}
]
@patch(
"summary.api.route.tasks_v2.summarize_v2_task.apply_async",
return_value=MagicMock(id="summarize-task-id-abc"),
)
def test_create_summarize_task_v2_returns_task_id(self, mock_apply_async, client):
"""POST /async-jobs/summarize creates a task and injects tenant_id."""
response = client.post(
"/api/v2/async-jobs/summarize",
headers={"Authorization": "Bearer test-api-token"},
json={
"user_sub": "remote-002",
"content": "This is a long meeting transcript to summarize.",
},
)
assert response.status_code == 200
assert response.json() == {
"job_id": "summarize-task-id-abc",
"message": "Summarize job created",
}
args = mock_apply_async.call_args.kwargs["args"]
assert args == [
{
"user_sub": "remote-002",
"content": "This is a long meeting transcript to summarize.",
"tenant_id": "test-tenant",
}
]
@patch("summary.api.route.tasks_v2.AsyncResult")
def test_get_transcribe_task_status_returns_status_for_same_tenant(
self, mock_async_result, client
):
"""GET /async-jobs/transcribe/{id} returns status when tenant matches."""
mock_async_result.return_value = MagicMock(
status="PENDING",
args=[{"tenant_id": "test-tenant"}],
)
response = client.get(
"/api/v2/async-jobs/transcribe/task-id-abc",
headers={"Authorization": "Bearer test-api-token"},
)
assert response.status_code == 200
assert response.json() == {"job_id": "task-id-abc", "status": "PENDING"}
mock_async_result.assert_called_once_with("task-id-abc")
@patch("summary.api.route.tasks_v2.AsyncResult")
def test_get_summarize_task_status_returns_status_for_same_tenant(
self, mock_async_result, client
):
"""GET /async-jobs/summarize/{id} returns status when tenant matches."""
mock_async_result.return_value = MagicMock(
status="SUCCESS",
args=[{"tenant_id": "test-tenant"}],
)
response = client.get(
"/api/v2/async-jobs/summarize/task-id-abc",
headers={"Authorization": "Bearer test-api-token"},
)
assert response.status_code == 200
assert response.json() == {"job_id": "task-id-abc", "status": "SUCCESS"}
mock_async_result.assert_called_once_with("task-id-abc")
@patch("summary.api.route.tasks_v2.AsyncResult")
def test_get_task_status_returns_404_when_args_are_missing(
self, mock_async_result, client
):
"""GET /async-jobs/.../{id} returns 404 when task args are invalid."""
mock_async_result.return_value = MagicMock(status="SUCCESS", args=None)
response = client.get(
"/api/v2/async-jobs/transcribe/task-id-abc",
headers={"Authorization": "Bearer test-api-token"},
)
assert response.status_code == 404
assert response.json() == {"detail": "Not found"}
+10 -2
View File
@@ -4,14 +4,22 @@ import pytest
from fastapi.testclient import TestClient
from pydantic import SecretStr
from summary.core.config import Settings, get_settings
from summary.core.config import AuthorizedTenant, Settings, get_settings
from summary.main import app
def get_settings_override():
"""Return settings for tests."""
return Settings(
app_api_token=SecretStr("test-api-token"),
v1_tenant_id="test-tenant",
authorized_tenants=(
AuthorizedTenant(
webhook_url="https://example.com/webhook",
id="test-tenant",
api_key=SecretStr("test-api-token"),
webhook_api_key=SecretStr("test-webhook-api-key"),
),
),
)