from __future__ import annotations import asyncio import hashlib from collections.abc import AsyncIterable from uuid import uuid4 from fastapi.testclient import TestClient from ragcore.api.v1.control.dependencies import ( get_control_application, get_credential_service, get_principal, ) from ragcore.api.v1.uploads.dependencies import get_upload_service from ragcore.application.authorization import AuthorizationService from ragcore.application.control.api import ControlPlaneApplication from ragcore.application.documents import DocumentRecord from ragcore.application.identity.credentials import ServiceAccountCredentialService from ragcore.application.jobs import Job, JobService from ragcore.application.storage import BlobReference from ragcore.domain.authorization import Principal, PrincipalType from ragcore.domain.control import ( Application, Grant, KnowledgeSpace, Policy, Role, Scope, ServiceAccount, UserGrant, Workspace, ) from ragcore.infrastructure.connectors.upload import ( UploadIdempotencyStore, UploadPolicy, UploadService, ) from ragcore.infrastructure.database.identity.memory import InMemoryCredentialRepository from ragcore.main import create_app class RecordingBlobStorage: def __init__(self) -> None: self.calls = 0 async def put( self, chunks: AsyncIterable[bytes], *, media_type: str | None = None, original_name: str | None = None, expected_sha256: str | None = None, ) -> BlobReference: self.calls += 1 body = b"".join([chunk async for chunk in chunks]) digest = hashlib.sha256(body).hexdigest() return BlobReference( digest=digest, size=len(body), media_type=media_type, original_name=original_name, storage_key=f"blobs/{digest}", ) class RecordingJobRepository: def __init__(self) -> None: self.calls = 0 self.enqueued: list[Job] = [] async def enqueue(self, job: Job) -> Job: self.calls += 1 self.enqueued.append(job) return job class FakeControlRepository: def __init__(self, workspace: Workspace) -> None: self.workspace = workspace self.applications: list[Application] = [] self.service_accounts: list[ServiceAccount] = [] self.spaces: list[KnowledgeSpace] = [] self.grants: list[Grant | UserGrant] = [] self.policies: list[Policy] = [] async def get_workspace(self, workspace_id): return self.workspace if workspace_id == self.workspace.id else None async def create_application(self, entity): self.applications.append(entity) return entity async def get_application(self, application_id, workspace_id): return next( ( item for item in self.applications if item.id == application_id and item.workspace_id == workspace_id ), None, ) async def list_applications(self, workspace_id): return tuple(item for item in self.applications if item.workspace_id == workspace_id) async def create_service_account(self, entity): self.service_accounts.append(entity) return entity async def get_service_account_by_id(self, service_account_id): return next((item for item in self.service_accounts if item.id == service_account_id), None) async def create_space(self, entity): self.spaces.append(entity) return entity async def list_spaces(self, workspace_id): return tuple(item for item in self.spaces if item.workspace_id == workspace_id) async def update_space(self, entity): self.spaces = [entity if item.id == entity.id else item for item in self.spaces] return entity async def create_grant(self, entity): self.grants.append(entity) return entity async def create_user_grant(self, entity): self.grants.append(entity) return entity async def create_policy(self, entity): self.policies.append(entity) return entity async def list_policies(self, workspace_id): return tuple(item for item in self.policies if item.workspace_id == workspace_id) async def list_grants(self, workspace_id): return tuple( item for item in self.grants if isinstance(item, Grant) and item.workspace_id == workspace_id ) class FakeDocumentRepository: def __init__(self) -> None: self.documents: list[DocumentRecord] = [] async def create_document(self, document: DocumentRecord) -> DocumentRecord: self.documents.append(document) return document async def create_version(self, version): # type: ignore[no-untyped-def] raise AssertionError("should not be reached in these tests") async def get_version(self, version_id): # type: ignore[no-untyped-def] raise AssertionError("should not be reached in these tests") async def publish_validated(self, version_id, artifacts, chunks): # type: ignore[no-untyped-def] raise AssertionError("should not be reached in these tests") async def mark_source_deleted(self, document_id): # type: ignore[no-untyped-def] raise AssertionError("should not be reached in these tests") async def list_searchable_versions(self, document_id): # type: ignore[no-untyped-def] raise AssertionError("should not be reached in these tests") async def list_documents_by_space(self, space_id): # type: ignore[no-untyped-def] return tuple(item for item in self.documents if item.space_id == space_id) async def list_documents_by_space_with_status(self, space_id): # type: ignore[no-untyped-def] from ragcore.application.documents import DocumentStatusRecord, DocumentVersionState return tuple( DocumentStatusRecord( document=item, latest_state=DocumentVersionState.PUBLISHED, parser_version="text-parser-1", chunk_profile_version="prose-v1", updated_at=None, ) for item in self.documents if item.space_id == space_id ) def _fixture(): workspace = Workspace(uuid4(), "Web workspace", "web-workspace") principal = Principal( uuid4(), workspace.id, PrincipalType.HUMAN, frozenset({Role.WORKSPACE_ADMIN}) ) repository = FakeControlRepository(workspace) document_repository = FakeDocumentRepository() repository.documents = document_repository application = workspace.create_application( id=uuid4(), name="Web application", slug="web-application", scopes=frozenset({Scope.SEARCH}), ) asyncio.run(repository.create_application(application)) service = ControlPlaneApplication( repository, AuthorizationService(), document_repository=document_repository ) credentials = InMemoryCredentialRepository() credential_service = ServiceAccountCredentialService( credentials, "x" * 32, repository.get_service_account_by_id, ) blob_storage = RecordingBlobStorage() job_repository = RecordingJobRepository() repository.uploads = job_repository upload_service = UploadService( blob_storage, JobService(job_repository), policy=UploadPolicy(), idempotency=UploadIdempotencyStore(), ) app = create_app() app.dependency_overrides[get_principal] = lambda: principal app.dependency_overrides[get_control_application] = lambda: service app.dependency_overrides[get_credential_service] = lambda: credential_service app.dependency_overrides[get_upload_service] = lambda: upload_service return app, repository, principal, application def test_control_screens_have_empty_states_and_accessible_landmarks() -> None: app, _, _, _ = _fixture() client = TestClient(app) for path, heading in ( ("/admin/control/applications", "Applications"), ("/admin/control/spaces", "Knowledge spaces"), ("/admin/control/policies", "Policies"), ): response = client.get(path) assert response.status_code == 200 assert heading in response.text if path != "/admin/control/applications": assert 'role="status"' in response.text html = client.get("/admin/control/applications").text assert 'href="#main-content"' in html assert '