forked from 0xWheatyz/SPARC
Add multi-tenant support with owner_id isolation
- Add owner_id (FK to users) column to llm_messages, jobs, and
tracked_companies tables via schema migration in initialize_schema()
- Filter all read/write operations by authenticated user's owner_id
so users cannot see or modify each other's data
- Add user-scoped /tracked endpoints alongside existing admin ones
- Add admin-scoped /admin/analyses and /admin/jobs endpoints that
return cross-tenant data without owner filtering
- Create migration script (scripts/migrate_add_owner_id.py) that
backfills owner_id=1 for all existing rows
- Replace global UNIQUE on tracked_companies.company_name with
per-owner unique index (company_name, owner_id)
- Fix route ordering: /analyze/batch and /analyze/patent routes now
registered before /analyze/{company_name} to prevent path conflicts
- Update all existing API tests with proper auth headers and owner_id
assertions
- Add comprehensive cross-tenant isolation test suite
(tests/test_multi_tenant.py)
Closes leeworks-agents/SPARC#1677
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,18 @@
|
||||
"""Tests for tracked company admin endpoints and scheduler integration.
|
||||
"""Tests for tracked company endpoints and scheduler integration.
|
||||
|
||||
Covers issue #1656:
|
||||
- GET /admin/tracked (list tracked companies)
|
||||
- POST /admin/tracked (add a tracked company)
|
||||
- DELETE /admin/tracked/{company_name} (remove a tracked company)
|
||||
Covers:
|
||||
- GET /tracked (user-scoped list)
|
||||
- POST /tracked (user-scoped add)
|
||||
- DELETE /tracked/{company_name} (user-scoped remove)
|
||||
- GET /admin/tracked (admin: all companies)
|
||||
- POST /admin/tracked (admin: add)
|
||||
- DELETE /admin/tracked/{company_name} (admin: remove any)
|
||||
- GET /admin/alerts (list alerts)
|
||||
- scheduler.run_scheduled_analysis() integration
|
||||
|
||||
All tests mock the database layer and use JWT auth fixtures.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import MagicMock, patch, call
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
@@ -125,7 +126,7 @@ class TestAddTrackedCompany:
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["company_name"] == "Intel"
|
||||
mock_db.add_tracked_company.assert_called_once_with("Intel")
|
||||
mock_db.add_tracked_company.assert_called_once_with("Intel", owner_id=1)
|
||||
|
||||
def test_add_duplicate_returns_409(self, client, mock_db):
|
||||
"""Adding an already-tracked company returns 409."""
|
||||
@@ -141,7 +142,7 @@ class TestAddTrackedCompany:
|
||||
assert "already tracked" in response.json()["detail"].lower()
|
||||
|
||||
def test_add_tracked_requires_admin(self, client, mock_db):
|
||||
"""Regular user cannot add tracked companies."""
|
||||
"""Regular user cannot add tracked companies via admin endpoint."""
|
||||
mock_db.get_user_by_id.return_value = {
|
||||
"id": 2,
|
||||
"email": "user@test.com",
|
||||
@@ -215,6 +216,66 @@ class TestRemoveTrackedCompany:
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
# ---------- User-scoped tracked companies ----------
|
||||
|
||||
class TestUserScopedTrackedCompanies:
|
||||
"""Tests for /tracked user-scoped endpoints."""
|
||||
|
||||
def test_user_list_tracked(self, client, mock_db):
|
||||
"""Regular user can list their own tracked companies."""
|
||||
mock_db.get_user_by_id.return_value = {
|
||||
"id": 2,
|
||||
"email": "user@test.com",
|
||||
"role": "user",
|
||||
"created_at": datetime(2025, 1, 1, tzinfo=timezone.utc),
|
||||
}
|
||||
mock_db.list_tracked_companies.return_value = [
|
||||
{"company_name": "AMD", "owner_id": 2},
|
||||
]
|
||||
|
||||
response = client.get("/tracked", headers=_user_header())
|
||||
|
||||
assert response.status_code == 200
|
||||
mock_db.list_tracked_companies.assert_called_with(owner_id=2)
|
||||
|
||||
def test_user_add_tracked(self, client, mock_db):
|
||||
"""Regular user can add a company to their own tracked list."""
|
||||
mock_db.get_user_by_id.return_value = {
|
||||
"id": 2,
|
||||
"email": "user@test.com",
|
||||
"role": "user",
|
||||
"created_at": datetime(2025, 1, 1, tzinfo=timezone.utc),
|
||||
}
|
||||
mock_db.add_tracked_company.return_value = {
|
||||
"company_name": "Intel",
|
||||
"owner_id": 2,
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
"/tracked",
|
||||
json={"company_name": "Intel"},
|
||||
headers=_user_header(),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
mock_db.add_tracked_company.assert_called_once_with("Intel", owner_id=2)
|
||||
|
||||
def test_user_remove_tracked(self, client, mock_db):
|
||||
"""Regular user can remove a company from their own tracked list."""
|
||||
mock_db.get_user_by_id.return_value = {
|
||||
"id": 2,
|
||||
"email": "user@test.com",
|
||||
"role": "user",
|
||||
"created_at": datetime(2025, 1, 1, tzinfo=timezone.utc),
|
||||
}
|
||||
mock_db.remove_tracked_company.return_value = True
|
||||
|
||||
response = client.delete("/tracked/Intel", headers=_user_header())
|
||||
|
||||
assert response.status_code == 200
|
||||
mock_db.remove_tracked_company.assert_called_once_with("Intel", owner_id=2)
|
||||
|
||||
|
||||
# ---------- GET /admin/alerts ----------
|
||||
|
||||
class TestListAlerts:
|
||||
|
||||
Reference in New Issue
Block a user