"""Section 9 — Compliance & Legal Minimums. Covers: Privacy policy : exists, linked from footer, covers agent-first model. Terms of Service : exists, implicit acceptance via key registration. Minimum data : no unnecessary PII fields in MusehubIdentity. GDPR / CCPA : GET /me/export and DELETE /me endpoints exist and work. DMCA : takedown process documented. OSS license audit : license-audit.md exists and covers all direct deps. DB migration : 0023 adds training_opt_out + tos_accepted_at/tos_version. Auth service : tos_accepted_at set at registration time. Training opt-out : MusehubRepo.training_opt_out field exists, defaults False. """ from __future__ import annotations import json from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest _ROOT = Path(__file__).resolve().parents[1] _MUSEHUB_PKG = _ROOT / "musehub" _DOCS_LEGAL = _ROOT / "docs" / "legal" _BASE_HTML = _MUSEHUB_PKG / "templates" / "musehub" / "base.html" _AUTH_SVC = _MUSEHUB_PKG / "services" / "musehub_auth.py" _USERS_ROUTES = _MUSEHUB_PKG / "api" / "routes" / "musehub" / "users.py" _DB_MODELS = _MUSEHUB_PKG / "db" / "musehub_models.py" _MIGRATION = _ROOT / "alembic" / "versions" / "0023_compliance_fields.py" _CHECKLIST = _ROOT / "docs" / "pre-launch-checklist.md" # ═══════════════════════════════════════════════════════════════════════════════ # Privacy Policy # ═══════════════════════════════════════════════════════════════════════════════ class TestPrivacyPolicy: _pp = _DOCS_LEGAL / "privacy-policy.md" def test_privacy_policy_exists(self): assert self._pp.exists(), "docs/legal/privacy-policy.md must exist" def test_privacy_policy_covers_pubkey_identity(self): text = self._pp.read_text() assert "public key" in text.lower() or "pubkey" in text.lower() def test_privacy_policy_covers_agents(self): text = self._pp.read_text() assert "agent" in text.lower() def test_privacy_policy_covers_training_data(self): text = self._pp.read_text() # Must mention training data policy assert "training" in text.lower() def test_privacy_policy_covers_training_opt_out(self): text = self._pp.read_text() assert "training_opt_out" in text or "opt-out" in text.lower() or "opt out" in text.lower() def test_privacy_policy_mentions_export_endpoint(self): text = self._pp.read_text() assert "/me/export" in text or "export" in text.lower() def test_privacy_policy_mentions_delete_endpoint(self): text = self._pp.read_text() assert "DELETE" in text or "deletion" in text.lower() def test_privacy_policy_covers_private_repos_exclusion(self): text = self._pp.read_text() # Private repos must never be used for training assert "private" in text.lower() def test_privacy_policy_has_effective_date(self): text = self._pp.read_text() assert "Effective date" in text or "effective date" in text.lower() # ═══════════════════════════════════════════════════════════════════════════════ # Terms of Service # ═══════════════════════════════════════════════════════════════════════════════ class TestTermsOfService: _tos = _DOCS_LEGAL / "terms-of-service.md" def test_tos_exists(self): assert self._tos.exists(), "docs/legal/terms-of-service.md must exist" def test_tos_implicit_acceptance_via_key_registration(self): text = self._tos.read_text() # Must explain that key registration = acceptance assert "key registration" in text.lower() or "registering a key" in text.lower() def test_tos_records_tos_accepted_at(self): text = self._tos.read_text() assert "tos_accepted_at" in text def test_tos_records_tos_version(self): text = self._tos.read_text() assert "tos_version" in text def test_tos_agent_operator_responsibility(self): text = self._tos.read_text() assert "operator" in text.lower() def test_tos_training_data_policy_section(self): text = self._tos.read_text() assert "training" in text.lower() def test_tos_private_repos_never_used_for_training(self): text = self._tos.read_text() # The word "private" must appear in context with training assert "private" in text.lower() def test_tos_training_opt_out_mentioned(self): text = self._tos.read_text() assert "training_opt_out" in text def test_tos_osi_license_condition(self): text = self._tos.read_text() # Must condition training use on OSI license assert "osi" in text.lower() or "open-source license" in text.lower() or "open source license" in text.lower() def test_tos_has_effective_date(self): text = self._tos.read_text() assert "Effective date" in text or "effective date" in text.lower() # ═══════════════════════════════════════════════════════════════════════════════ # DMCA # ═══════════════════════════════════════════════════════════════════════════════ class TestDmca: _dmca = _DOCS_LEGAL / "dmca.md" def test_dmca_exists(self): assert self._dmca.exists(), "docs/legal/dmca.md must exist" def test_dmca_has_contact_email(self): text = self._dmca.read_text() assert "dmca@" in text or "@musehub" in text def test_dmca_has_response_timeline(self): text = self._dmca.read_text() # Must commit to a response time assert "business day" in text.lower() or "days" in text.lower() def test_dmca_covers_counter_notice(self): text = self._dmca.read_text() assert "counter" in text.lower() def test_dmca_mentions_repeat_infringers(self): text = self._dmca.read_text() assert "repeat" in text.lower() def test_dmca_covers_agent_operators(self): text = self._dmca.read_text() assert "agent" in text.lower() or "operator" in text.lower() # ═══════════════════════════════════════════════════════════════════════════════ # OSS License Audit # ═══════════════════════════════════════════════════════════════════════════════ class TestLicenseAudit: _audit = _DOCS_LEGAL / "license-audit.md" def test_license_audit_exists(self): assert self._audit.exists(), "docs/legal/license-audit.md must exist" def test_license_audit_covers_fastapi(self): text = self._audit.read_text() assert "fastapi" in text.lower() def test_license_audit_covers_sqlalchemy(self): text = self._audit.read_text() assert "sqlalchemy" in text.lower() def test_license_audit_covers_cryptography(self): text = self._audit.read_text() assert "cryptography" in text.lower() def test_license_audit_covers_psycopg2(self): text = self._audit.read_text() assert "psycopg2" in text.lower() def test_license_audit_has_review_schedule(self): text = self._audit.read_text() assert "review" in text.lower() def test_all_direct_deps_are_osi_or_noted(self): """Every dep row must declare 'Yes' (OSI) or have an explanation.""" text = self._audit.read_text() # We just check that 'OSI approved' header is present and 'Yes' appears assert "OSI approved" in text or "osi" in text.lower() # ═══════════════════════════════════════════════════════════════════════════════ # Footer — legal links in base.html # ═══════════════════════════════════════════════════════════════════════════════ class TestLegalFooter: def test_footer_exists_in_base_html(self): text = _BASE_HTML.read_text() assert "site-footer" in text or "= 6, "All 6 section 9 items should be checked"