@cachly-dev/mcp-server 0.10.64 → 0.10.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/handlers/brain.js +1 -1
- package/dist/src/index.js +1 -1
- package/package.json +7 -1
- package/server.json +2 -2
- package/.env.test.example +0 -23
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
- package/.github/workflows/eas-build-android.yml +0 -58
- package/.github/workflows/eas-update.yml +0 -55
- package/.github/workflows/publish.yml +0 -72
- package/BENCH.md +0 -91
- package/CHANGELOG.md +0 -604
- package/CHANGELOG.md.tmp +0 -0
- package/Dockerfile +0 -16
- package/LAUNCH_TEXTS.md +0 -182
- package/PROGRESS.md +0 -137
- package/STRATEGY.md +0 -228
- package/VISION_10X.md +0 -133
- package/apps/travel-chaos-organizer/.env.example +0 -49
- package/apps/travel-chaos-organizer/DEPLOY.md +0 -328
- package/apps/travel-chaos-organizer/Makefile +0 -57
- package/apps/travel-chaos-organizer/README.md +0 -159
- package/apps/travel-chaos-organizer/backend/.env.example +0 -10
- package/apps/travel-chaos-organizer/backend/Dockerfile +0 -25
- package/apps/travel-chaos-organizer/backend/alembic/env.py +0 -56
- package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +0 -24
- package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +0 -24
- package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +0 -24
- package/apps/travel-chaos-organizer/backend/alembic.ini +0 -39
- package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +0 -81
- package/apps/travel-chaos-organizer/backend/app/config.py +0 -66
- package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/db/database.py +0 -20
- package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +0 -71
- package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +0 -31
- package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +0 -4
- package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +0 -5
- package/apps/travel-chaos-organizer/backend/app/limiter.py +0 -4
- package/apps/travel-chaos-organizer/backend/app/logging_config.py +0 -25
- package/apps/travel-chaos-organizer/backend/app/main.py +0 -77
- package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/models/schemas.py +0 -142
- package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/routers/admin.py +0 -237
- package/apps/travel-chaos-organizer/backend/app/routers/cache.py +0 -13
- package/apps/travel-chaos-organizer/backend/app/routers/events.py +0 -29
- package/apps/travel-chaos-organizer/backend/app/routers/export.py +0 -122
- package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +0 -122
- package/apps/travel-chaos-organizer/backend/app/routers/items.py +0 -130
- package/apps/travel-chaos-organizer/backend/app/routers/mail.py +0 -108
- package/apps/travel-chaos-organizer/backend/app/routers/parse.py +0 -303
- package/apps/travel-chaos-organizer/backend/app/routers/payments.py +0 -179
- package/apps/travel-chaos-organizer/backend/app/routers/trips.py +0 -134
- package/apps/travel-chaos-organizer/backend/app/routers/users.py +0 -169
- package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +0 -37
- package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/services/cache.py +0 -131
- package/apps/travel-chaos-organizer/backend/app/services/drip.py +0 -128
- package/apps/travel-chaos-organizer/backend/app/services/email.py +0 -61
- package/apps/travel-chaos-organizer/backend/app/services/errors.py +0 -44
- package/apps/travel-chaos-organizer/backend/app/services/notifier.py +0 -10
- package/apps/travel-chaos-organizer/backend/app/services/ollama.py +0 -101
- package/apps/travel-chaos-organizer/backend/app/services/parser.py +0 -22
- package/apps/travel-chaos-organizer/backend/app/services/quota.py +0 -85
- package/apps/travel-chaos-organizer/backend/app/services/sentry.py +0 -11
- package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +0 -35
- package/apps/travel-chaos-organizer/backend/pytest.ini +0 -4
- package/apps/travel-chaos-organizer/backend/requirements-dev.txt +0 -6
- package/apps/travel-chaos-organizer/backend/requirements.txt +0 -25
- package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/tests/conftest.py +0 -223
- package/apps/travel-chaos-organizer/backend/tests/test_admin.py +0 -99
- package/apps/travel-chaos-organizer/backend/tests/test_cache.py +0 -142
- package/apps/travel-chaos-organizer/backend/tests/test_drip.py +0 -32
- package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +0 -121
- package/apps/travel-chaos-organizer/backend/tests/test_export.py +0 -48
- package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +0 -107
- package/apps/travel-chaos-organizer/backend/tests/test_items.py +0 -138
- package/apps/travel-chaos-organizer/backend/tests/test_mail.py +0 -113
- package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +0 -145
- package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +0 -98
- package/apps/travel-chaos-organizer/backend/tests/test_parser.py +0 -72
- package/apps/travel-chaos-organizer/backend/tests/test_payments.py +0 -163
- package/apps/travel-chaos-organizer/backend/tests/test_quota.py +0 -46
- package/apps/travel-chaos-organizer/backend/tests/test_search.py +0 -67
- package/apps/travel-chaos-organizer/backend/tests/test_trips.py +0 -123
- package/apps/travel-chaos-organizer/backend/tests/test_users.py +0 -52
- package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/caddy/Caddyfile +0 -27
- package/apps/travel-chaos-organizer/docker-compose.node1.yml +0 -31
- package/apps/travel-chaos-organizer/docker-compose.override.yml +0 -14
- package/apps/travel-chaos-organizer/docker-compose.yml +0 -128
- package/apps/travel-chaos-organizer/frontend/.env.example +0 -13
- package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +0 -80
- package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +0 -166
- package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +0 -130
- package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +0 -381
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +0 -346
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +0 -363
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +0 -34
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +0 -35
- package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +0 -91
- package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +0 -71
- package/apps/travel-chaos-organizer/frontend/app/auth.tsx +0 -29
- package/apps/travel-chaos-organizer/frontend/app/index.tsx +0 -16
- package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +0 -119
- package/apps/travel-chaos-organizer/frontend/app.config.ts +0 -54
- package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +0 -66
- package/apps/travel-chaos-organizer/frontend/assets/icon.svg +0 -79
- package/apps/travel-chaos-organizer/frontend/assets/splash.svg +0 -15
- package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +0 -102
- package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +0 -107
- package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +0 -109
- package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +0 -45
- package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +0 -19
- package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +0 -155
- package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +0 -55
- package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +0 -80
- package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +0 -39
- package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +0 -39
- package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +0 -70
- package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +0 -121
- package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +0 -70
- package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +0 -73
- package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +0 -130
- package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +0 -60
- package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +0 -42
- package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +0 -146
- package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +0 -70
- package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +0 -44
- package/apps/travel-chaos-organizer/frontend/eas.json +0 -22
- package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +0 -24
- package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +0 -60
- package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +0 -112
- package/apps/travel-chaos-organizer/frontend/lib/api.ts +0 -223
- package/apps/travel-chaos-organizer/frontend/lib/auth.ts +0 -93
- package/apps/travel-chaos-organizer/frontend/lib/db.ts +0 -86
- package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +0 -54
- package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +0 -13
- package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +0 -112
- package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +0 -145
- package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +0 -43
- package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +0 -28
- package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +0 -44
- package/apps/travel-chaos-organizer/frontend/package-lock.json +0 -13221
- package/apps/travel-chaos-organizer/frontend/package.json +0 -44
- package/apps/travel-chaos-organizer/frontend/tsconfig.json +0 -23
- package/apps/travel-chaos-organizer/keycloak/realm-export.json +0 -59
- package/apps/travel-chaos-organizer/landing/index.html +0 -773
- package/apps/travel-chaos-organizer/scripts/backup.sh +0 -14
- package/apps/travel-chaos-organizer/scripts/configure-env.sh +0 -82
- package/apps/travel-chaos-organizer/scripts/deploy.sh +0 -54
- package/apps/travel-chaos-organizer/scripts/health-check.sh +0 -36
- package/apps/travel-chaos-organizer/scripts/migrate.sh +0 -7
- package/apps/travel-chaos-organizer/scripts/server-setup.sh +0 -85
- package/apps/travel-chaos-organizer/scripts/smoke-test.sh +0 -29
- package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +0 -71
- package/apps/travel-chaos-organizer/telegram-bot/.env.example +0 -3
- package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +0 -6
- package/apps/travel-chaos-organizer/telegram-bot/bot.py +0 -504
- package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +0 -3
- package/dist/src/__tests__/auth.test.d.ts +0 -1
- package/dist/src/__tests__/auth.test.js +0 -107
- package/dist/src/__tests__/auth.test.js.map +0 -1
- package/dist/src/__tests__/brain-flow.test.d.ts +0 -12
- package/dist/src/__tests__/brain-flow.test.js +0 -1241
- package/dist/src/__tests__/brain-flow.test.js.map +0 -1
- package/dist/src/__tests__/confidence.test.d.ts +0 -13
- package/dist/src/__tests__/confidence.test.js +0 -233
- package/dist/src/__tests__/confidence.test.js.map +0 -1
- package/dist/src/__tests__/e2e/api-auth.test.d.ts +0 -7
- package/dist/src/__tests__/e2e/api-auth.test.js +0 -162
- package/dist/src/__tests__/e2e/api-auth.test.js.map +0 -1
- package/dist/src/__tests__/e2e/api-health.test.d.ts +0 -7
- package/dist/src/__tests__/e2e/api-health.test.js +0 -95
- package/dist/src/__tests__/e2e/api-health.test.js.map +0 -1
- package/dist/src/__tests__/e2e/cli.test.d.ts +0 -10
- package/dist/src/__tests__/e2e/cli.test.js +0 -204
- package/dist/src/__tests__/e2e/cli.test.js.map +0 -1
- package/dist/src/__tests__/e2e/config.d.ts +0 -35
- package/dist/src/__tests__/e2e/config.js +0 -53
- package/dist/src/__tests__/e2e/config.js.map +0 -1
- package/dist/src/__tests__/e2e/funnel.test.d.ts +0 -13
- package/dist/src/__tests__/e2e/funnel.test.js +0 -90
- package/dist/src/__tests__/e2e/funnel.test.js.map +0 -1
- package/dist/src/__tests__/e2e/mcp-tools.test.d.ts +0 -7
- package/dist/src/__tests__/e2e/mcp-tools.test.js +0 -129
- package/dist/src/__tests__/e2e/mcp-tools.test.js.map +0 -1
- package/dist/src/__tests__/e2e/provisioning.test.d.ts +0 -15
- package/dist/src/__tests__/e2e/provisioning.test.js +0 -123
- package/dist/src/__tests__/e2e/provisioning.test.js.map +0 -1
- package/dist/src/__tests__/handlers-unit.test.d.ts +0 -12
- package/dist/src/__tests__/handlers-unit.test.js +0 -505
- package/dist/src/__tests__/handlers-unit.test.js.map +0 -1
- package/dist/src/__tests__/namespace.test.d.ts +0 -1
- package/dist/src/__tests__/namespace.test.js +0 -114
- package/dist/src/__tests__/namespace.test.js.map +0 -1
- package/dist/src/__tests__/rerank.test.d.ts +0 -1
- package/dist/src/__tests__/rerank.test.js +0 -115
- package/dist/src/__tests__/rerank.test.js.map +0 -1
- package/dist/src/__tests__/utils.test.d.ts +0 -1
- package/dist/src/__tests__/utils.test.js +0 -83
- package/dist/src/__tests__/utils.test.js.map +0 -1
- package/dist/src/bench/cachly-bench.d.ts +0 -35
- package/dist/src/bench/cachly-bench.js +0 -182
- package/dist/src/bench/cachly-bench.js.map +0 -1
- package/dist/src/bench/fixtures.d.ts +0 -37
- package/dist/src/bench/fixtures.js +0 -225
- package/dist/src/bench/fixtures.js.map +0 -1
- package/dist/src/search.test.d.ts +0 -1
- package/dist/src/search.test.js +0 -713
- package/dist/src/search.test.js.map +0 -1
- package/eslint.config.js +0 -16
- package/glama.json +0 -36
- package/llms.txt +0 -155
- package/packages/telegram-notify/SPLIT_PLAN.md +0 -106
- package/packages/telegram-notify/client.ts +0 -88
- package/packages/telegram-notify/emojis.json +0 -22
- package/packages/telegram-notify/pyproject.toml +0 -17
- package/packages/telegram-notify/telegram_notify/__init__.py +0 -3
- package/packages/telegram-notify/telegram_notify/client.py +0 -87
- package/packages/telegram-notify/telegram_notify/emojis.json +0 -22
- package/scripts/e2e-run.sh +0 -160
- package/scripts/generate-icons.js +0 -28
- package/smithery.yaml +0 -45
- package/src/__tests__/auth.test.ts +0 -147
- package/src/__tests__/brain-flow.test.ts +0 -1472
- package/src/__tests__/confidence.test.ts +0 -285
- package/src/__tests__/e2e/api-auth.test.ts +0 -182
- package/src/__tests__/e2e/api-health.test.ts +0 -103
- package/src/__tests__/e2e/cli.test.ts +0 -228
- package/src/__tests__/e2e/config.ts +0 -53
- package/src/__tests__/e2e/funnel.test.ts +0 -99
- package/src/__tests__/e2e/mcp-tools.test.ts +0 -191
- package/src/__tests__/e2e/provisioning.test.ts +0 -128
- package/src/__tests__/handlers-unit.test.ts +0 -677
- package/src/__tests__/namespace.test.ts +0 -126
- package/src/__tests__/rerank.test.ts +0 -130
- package/src/__tests__/utils.test.ts +0 -81
- package/src/auth.ts +0 -46
- package/src/bench/cachly-bench.ts +0 -217
- package/src/bench/fixtures.ts +0 -256
- package/src/ckg.ts +0 -103
- package/src/confidence.ts +0 -38
- package/src/embeddings.ts +0 -217
- package/src/handlers/advanced.ts +0 -647
- package/src/handlers/brain.ts +0 -2622
- package/src/handlers/cache.ts +0 -633
- package/src/handlers/context.ts +0 -222
- package/src/handlers/fedbrain.ts +0 -1232
- package/src/handlers/instances.ts +0 -180
- package/src/handlers/roadmap.ts +0 -201
- package/src/handlers/syndicate.ts +0 -460
- package/src/handlers/tco.ts +0 -345
- package/src/handlers/team.ts +0 -1249
- package/src/index.ts +0 -2815
- package/src/namespace.ts +0 -128
- package/src/notifier.ts +0 -5
- package/src/rerank.ts +0 -148
- package/src/search.test.ts +0 -848
- package/src/search.ts +0 -1781
- package/src/tools.ts +0 -2056
- package/src/utils.ts +0 -90
- package/test-features.cjs +0 -23
- package/tsconfig.json +0 -16
- package/vitest.config.ts +0 -14
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Shared fixtures for Travel Chaos Organizer backend tests.
|
|
3
|
-
|
|
4
|
-
Patterns from cachly-mcp:
|
|
5
|
-
- In-memory mocks instead of real services (no Docker required for unit tests)
|
|
6
|
-
- Deterministic fake data via factory helpers
|
|
7
|
-
- Auth always injectable / bypassable in unit scope
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
import uuid
|
|
12
|
-
from collections.abc import AsyncGenerator
|
|
13
|
-
from datetime import date
|
|
14
|
-
from typing import Any
|
|
15
|
-
from unittest.mock import AsyncMock, patch
|
|
16
|
-
|
|
17
|
-
import pytest
|
|
18
|
-
import pytest_asyncio
|
|
19
|
-
from fastapi import FastAPI
|
|
20
|
-
from httpx import ASGITransport, AsyncClient
|
|
21
|
-
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
|
22
|
-
|
|
23
|
-
from app.db.database import Base, get_db
|
|
24
|
-
from app.main import app
|
|
25
|
-
from app.models.schemas import ParsedTravelData
|
|
26
|
-
|
|
27
|
-
# ── In-memory SQLite DB (mirrors cachly MockRedis pattern) ────────────────────
|
|
28
|
-
|
|
29
|
-
TEST_DB_URL = "sqlite+aiosqlite:///:memory:"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@pytest_asyncio.fixture(scope="function")
|
|
33
|
-
async def db_engine():
|
|
34
|
-
import sqlalchemy
|
|
35
|
-
engine = create_async_engine(TEST_DB_URL, echo=False)
|
|
36
|
-
async with engine.begin() as conn:
|
|
37
|
-
# Execute each DDL statement separately — SQLite doesn't support
|
|
38
|
-
# multi-statement execute() calls.
|
|
39
|
-
for stmt in SQLITE_SCHEMA.strip().split(";"):
|
|
40
|
-
stmt = stmt.strip()
|
|
41
|
-
if stmt:
|
|
42
|
-
await conn.execute(sqlalchemy.text(stmt))
|
|
43
|
-
yield engine
|
|
44
|
-
await engine.dispose()
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@pytest_asyncio.fixture
|
|
48
|
-
async def db_session(db_engine) -> AsyncGenerator[AsyncSession, None]:
|
|
49
|
-
SessionLocal = async_sessionmaker(db_engine, expire_on_commit=False)
|
|
50
|
-
async with SessionLocal() as session:
|
|
51
|
-
yield session
|
|
52
|
-
await session.rollback()
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# ── Auth bypass (like cachly's checkJwt mock) ─────────────────────────────────
|
|
56
|
-
|
|
57
|
-
FAKE_USER_ID = "user-test-123"
|
|
58
|
-
FAKE_USER_PAYLOAD = {"sub": FAKE_USER_ID, "email": "test@example.com"}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@pytest_asyncio.fixture
|
|
62
|
-
async def client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
|
|
63
|
-
"""HTTP client with auth bypassed and real in-memory DB."""
|
|
64
|
-
from app.auth.keycloak import get_current_user
|
|
65
|
-
from app.db.database import get_db
|
|
66
|
-
|
|
67
|
-
from app.auth.keycloak import user_id_or_bot
|
|
68
|
-
|
|
69
|
-
async def override_auth():
|
|
70
|
-
return FAKE_USER_PAYLOAD
|
|
71
|
-
|
|
72
|
-
async def override_user_id_or_bot():
|
|
73
|
-
return FAKE_USER_ID
|
|
74
|
-
|
|
75
|
-
async def override_db():
|
|
76
|
-
yield db_session
|
|
77
|
-
|
|
78
|
-
app.dependency_overrides[get_current_user] = override_auth
|
|
79
|
-
app.dependency_overrides[user_id_or_bot] = override_user_id_or_bot
|
|
80
|
-
app.dependency_overrides[get_db] = override_db
|
|
81
|
-
|
|
82
|
-
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
|
|
83
|
-
yield c
|
|
84
|
-
|
|
85
|
-
app.dependency_overrides.clear()
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
# ── Ollama mock (no real LLM needed) ──────────────────────────────────────────
|
|
89
|
-
|
|
90
|
-
PARSED_FLIGHT = ParsedTravelData(
|
|
91
|
-
type="flight",
|
|
92
|
-
title="LH 400 Frankfurt → New York",
|
|
93
|
-
booking_ref="ABC123",
|
|
94
|
-
provider="Lufthansa",
|
|
95
|
-
event_at="2024-08-15T10:30:00",
|
|
96
|
-
origin="FRA",
|
|
97
|
-
destination="JFK",
|
|
98
|
-
passengers=["Max Mustermann"],
|
|
99
|
-
price="€ 850",
|
|
100
|
-
confidence=0.92,
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
@pytest.fixture
|
|
105
|
-
def mock_ollama_flight():
|
|
106
|
-
with patch("app.services.ollama.parse_text", new_callable=AsyncMock) as m:
|
|
107
|
-
m.return_value = (PARSED_FLIGHT.model_dump(), False)
|
|
108
|
-
yield m
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
@pytest.fixture
|
|
112
|
-
def mock_ollama_low_confidence():
|
|
113
|
-
with patch("app.services.ollama.parse_text", new_callable=AsyncMock) as m:
|
|
114
|
-
m.return_value = (
|
|
115
|
-
{"type": "other", "title": "Unrecognized document", "confidence": 0.1},
|
|
116
|
-
False,
|
|
117
|
-
)
|
|
118
|
-
yield m
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# ── Factory helpers ────────────────────────────────────────────────────────────
|
|
122
|
-
|
|
123
|
-
def make_trip(name: str = "Sommerurlaub 2024", **kwargs) -> dict[str, Any]:
|
|
124
|
-
return {"name": name, "description": "Test trip", "start_date": "2024-08-01",
|
|
125
|
-
"end_date": "2024-08-15", **kwargs}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def make_item(**kwargs) -> dict[str, Any]:
|
|
129
|
-
return {"type": "flight", "title": "LH 400 FRA→JFK", "event_at": "2024-08-15T10:30:00",
|
|
130
|
-
"booking_ref": "ABC123", "provider": "Lufthansa", **kwargs}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
# ── SQLite-compatible schema (no PostgreSQL ENUMs / pgcrypto) ─────────────────
|
|
134
|
-
|
|
135
|
-
def _uuid_default() -> str:
|
|
136
|
-
"""SQLite UUID expression: generates xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx format."""
|
|
137
|
-
return (
|
|
138
|
-
"lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-' || "
|
|
139
|
-
"lower(hex(randomblob(2))) || '-' || lower(hex(randomblob(2))) || '-' || "
|
|
140
|
-
"lower(hex(randomblob(6)))"
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
_UD = _uuid_default()
|
|
144
|
-
|
|
145
|
-
SQLITE_SCHEMA = f"""
|
|
146
|
-
CREATE TABLE IF NOT EXISTS trips (
|
|
147
|
-
id TEXT PRIMARY KEY DEFAULT ({_UD}),
|
|
148
|
-
user_id TEXT NOT NULL,
|
|
149
|
-
name TEXT NOT NULL,
|
|
150
|
-
description TEXT,
|
|
151
|
-
start_date TEXT,
|
|
152
|
-
end_date TEXT,
|
|
153
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
154
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
CREATE TABLE IF NOT EXISTS trip_items (
|
|
158
|
-
id TEXT PRIMARY KEY DEFAULT ({_UD}),
|
|
159
|
-
trip_id TEXT NOT NULL,
|
|
160
|
-
user_id TEXT NOT NULL,
|
|
161
|
-
type TEXT NOT NULL DEFAULT 'other',
|
|
162
|
-
title TEXT NOT NULL,
|
|
163
|
-
raw_text TEXT,
|
|
164
|
-
parsed_data TEXT,
|
|
165
|
-
event_at TEXT,
|
|
166
|
-
event_end_at TEXT,
|
|
167
|
-
booking_ref TEXT,
|
|
168
|
-
provider TEXT,
|
|
169
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
170
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
CREATE TABLE IF NOT EXISTS attachments (
|
|
174
|
-
id TEXT PRIMARY KEY DEFAULT ({_UD}),
|
|
175
|
-
trip_item_id TEXT,
|
|
176
|
-
inbox_id TEXT,
|
|
177
|
-
user_id TEXT NOT NULL,
|
|
178
|
-
file_path TEXT NOT NULL,
|
|
179
|
-
file_name TEXT NOT NULL,
|
|
180
|
-
mime_type TEXT NOT NULL,
|
|
181
|
-
size_bytes INTEGER,
|
|
182
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
CREATE TABLE IF NOT EXISTS chaos_inbox (
|
|
186
|
-
id TEXT PRIMARY KEY DEFAULT ({_UD}),
|
|
187
|
-
user_id TEXT NOT NULL,
|
|
188
|
-
raw_content TEXT,
|
|
189
|
-
source TEXT,
|
|
190
|
-
status TEXT NOT NULL DEFAULT 'pending',
|
|
191
|
-
parsed_data TEXT,
|
|
192
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
193
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
CREATE TABLE IF NOT EXISTS users (
|
|
197
|
-
id TEXT PRIMARY KEY,
|
|
198
|
-
email TEXT,
|
|
199
|
-
plan TEXT NOT NULL DEFAULT 'free',
|
|
200
|
-
plan_expires_at TEXT,
|
|
201
|
-
stripe_customer_id TEXT,
|
|
202
|
-
telegram_chat_id TEXT UNIQUE,
|
|
203
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
204
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
208
|
-
id TEXT PRIMARY KEY DEFAULT ({_UD}),
|
|
209
|
-
user_id TEXT,
|
|
210
|
-
event_name TEXT NOT NULL,
|
|
211
|
-
properties TEXT,
|
|
212
|
-
platform TEXT,
|
|
213
|
-
app_version TEXT,
|
|
214
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
CREATE TABLE IF NOT EXISTS waitlist (
|
|
218
|
-
id TEXT PRIMARY KEY DEFAULT ({_UD}),
|
|
219
|
-
email TEXT NOT NULL UNIQUE,
|
|
220
|
-
source TEXT DEFAULT 'landing',
|
|
221
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
222
|
-
);
|
|
223
|
-
"""
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
"""Tests for admin endpoints."""
|
|
2
|
-
import base64
|
|
3
|
-
|
|
4
|
-
import pytest
|
|
5
|
-
from httpx import AsyncClient
|
|
6
|
-
|
|
7
|
-
pytestmark = pytest.mark.asyncio
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def admin_headers(user: str = "admin", password: str = "changeme") -> dict:
|
|
11
|
-
token = base64.b64encode(f"{user}:{password}".encode()).decode()
|
|
12
|
-
return {"Authorization": f"Basic {token}"}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
async def test_admin_health_requires_auth(client: AsyncClient):
|
|
16
|
-
r = await client.get("/admin/health")
|
|
17
|
-
assert r.status_code == 401
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
async def test_admin_health_ok(client: AsyncClient):
|
|
21
|
-
r = await client.get("/admin/health", headers=admin_headers())
|
|
22
|
-
assert r.status_code == 200
|
|
23
|
-
data = r.json()
|
|
24
|
-
assert "events_total" in data
|
|
25
|
-
assert "waitlist_total" in data
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
async def test_admin_dashboard_requires_auth(client: AsyncClient):
|
|
29
|
-
r = await client.get("/admin")
|
|
30
|
-
assert r.status_code == 401
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
async def test_admin_dashboard_returns_html(client: AsyncClient):
|
|
34
|
-
r = await client.get("/admin", headers=admin_headers())
|
|
35
|
-
assert r.status_code == 200
|
|
36
|
-
assert "text/html" in r.headers["content-type"]
|
|
37
|
-
assert "TCO Admin" in r.text
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
async def test_admin_wrong_password(client: AsyncClient):
|
|
41
|
-
r = await client.get("/admin/health", headers=admin_headers(password="wrongpassword"))
|
|
42
|
-
assert r.status_code == 401
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
async def test_admin_events_summary(client: AsyncClient):
|
|
46
|
-
r = await client.get("/admin/events/summary", headers=admin_headers())
|
|
47
|
-
assert r.status_code == 200
|
|
48
|
-
assert "events" in r.json()
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
async def test_admin_waitlist(client: AsyncClient):
|
|
52
|
-
r = await client.get("/admin/waitlist", headers=admin_headers())
|
|
53
|
-
assert r.status_code == 200
|
|
54
|
-
assert "signups" in r.json()
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
async def test_admin_set_user_plan(client: AsyncClient, db_session):
|
|
58
|
-
from sqlalchemy import text
|
|
59
|
-
|
|
60
|
-
# First create the user by calling /me
|
|
61
|
-
await client.get("/api/v1/users/me")
|
|
62
|
-
|
|
63
|
-
r = await client.patch(
|
|
64
|
-
"/admin/users/user-test-123/plan?plan=pro", headers=admin_headers()
|
|
65
|
-
)
|
|
66
|
-
assert r.status_code == 200
|
|
67
|
-
assert r.json()["plan"] == "pro"
|
|
68
|
-
|
|
69
|
-
# Verify in DB
|
|
70
|
-
row = await db_session.execute(
|
|
71
|
-
text("SELECT plan FROM users WHERE id = 'user-test-123'")
|
|
72
|
-
)
|
|
73
|
-
assert row.fetchone()[0] == "pro"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
async def test_admin_set_invalid_plan(client: AsyncClient):
|
|
77
|
-
"""Setting an invalid plan returns 400."""
|
|
78
|
-
# Create user first
|
|
79
|
-
await client.get("/api/v1/users/me")
|
|
80
|
-
|
|
81
|
-
r = await client.patch(
|
|
82
|
-
"/admin/users/user-test-123/plan?plan=enterprise", headers=admin_headers()
|
|
83
|
-
)
|
|
84
|
-
assert r.status_code == 400
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
async def test_admin_users_list(client: AsyncClient):
|
|
88
|
-
r = await client.get("/admin/users", headers=admin_headers())
|
|
89
|
-
assert r.status_code == 200
|
|
90
|
-
assert "users" in r.json()
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
async def test_admin_health_counts(client: AsyncClient):
|
|
94
|
-
"""Health endpoint returns all expected count fields."""
|
|
95
|
-
r = await client.get("/admin/health", headers=admin_headers())
|
|
96
|
-
assert r.status_code == 200
|
|
97
|
-
data = r.json()
|
|
98
|
-
assert "users_total" in data
|
|
99
|
-
assert "pro_users" in data
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tests for Cachly Redis cache layer (services/cache.py).
|
|
3
|
-
Uses unittest.mock — no real Redis required.
|
|
4
|
-
"""
|
|
5
|
-
import json
|
|
6
|
-
import pytest
|
|
7
|
-
from unittest.mock import AsyncMock, MagicMock, patch
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# ── No-Redis path ─────────────────────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
def test_cachly_configured_false_when_no_env(monkeypatch):
|
|
13
|
-
monkeypatch.delenv("CACHLY_REDIS_URL", raising=False)
|
|
14
|
-
from app.services import cache as c
|
|
15
|
-
assert c.cachly_configured() is False
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_cachly_configured_true_when_env_set(monkeypatch):
|
|
19
|
-
monkeypatch.setenv("CACHLY_REDIS_URL", "redis://:pw@host:6380/0")
|
|
20
|
-
from app.services import cache as c
|
|
21
|
-
assert c.cachly_configured() is True
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@pytest.mark.asyncio
|
|
25
|
-
async def test_cache_get_returns_none_when_no_redis(monkeypatch):
|
|
26
|
-
monkeypatch.delenv("CACHLY_REDIS_URL", raising=False)
|
|
27
|
-
import importlib
|
|
28
|
-
from app.services import cache as c
|
|
29
|
-
# Reset module-level state
|
|
30
|
-
c._redis_client = None
|
|
31
|
-
c._redis_available = False
|
|
32
|
-
|
|
33
|
-
result = await c.cache_get("some content")
|
|
34
|
-
assert result is None
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@pytest.mark.asyncio
|
|
38
|
-
async def test_cache_set_no_ops_when_no_redis(monkeypatch):
|
|
39
|
-
monkeypatch.delenv("CACHLY_REDIS_URL", raising=False)
|
|
40
|
-
from app.services import cache as c
|
|
41
|
-
c._redis_client = None
|
|
42
|
-
c._redis_available = False
|
|
43
|
-
|
|
44
|
-
# Should not raise
|
|
45
|
-
await c.cache_set("some content", {"type": "flight"})
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# ── With mocked Redis ─────────────────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
@pytest.mark.asyncio
|
|
51
|
-
async def test_cache_get_returns_dict_on_hit():
|
|
52
|
-
from app.services import cache as c
|
|
53
|
-
|
|
54
|
-
mock_redis = AsyncMock()
|
|
55
|
-
mock_redis.get = AsyncMock(return_value=json.dumps({"type": "flight", "title": "LH 400"}))
|
|
56
|
-
|
|
57
|
-
c._redis_client = mock_redis
|
|
58
|
-
c._redis_available = True
|
|
59
|
-
|
|
60
|
-
result = await c.cache_get("flight booking text")
|
|
61
|
-
assert result == {"type": "flight", "title": "LH 400"}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@pytest.mark.asyncio
|
|
65
|
-
async def test_cache_get_returns_none_on_miss():
|
|
66
|
-
from app.services import cache as c
|
|
67
|
-
|
|
68
|
-
mock_redis = AsyncMock()
|
|
69
|
-
mock_redis.get = AsyncMock(return_value=None)
|
|
70
|
-
|
|
71
|
-
c._redis_client = mock_redis
|
|
72
|
-
c._redis_available = True
|
|
73
|
-
|
|
74
|
-
result = await c.cache_get("unknown content")
|
|
75
|
-
assert result is None
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
@pytest.mark.asyncio
|
|
79
|
-
async def test_cache_set_stores_json():
|
|
80
|
-
from app.services import cache as c
|
|
81
|
-
|
|
82
|
-
mock_redis = AsyncMock()
|
|
83
|
-
mock_redis.set = AsyncMock()
|
|
84
|
-
|
|
85
|
-
c._redis_client = mock_redis
|
|
86
|
-
c._redis_available = True
|
|
87
|
-
|
|
88
|
-
parsed = {"type": "hotel", "title": "Hilton Berlin"}
|
|
89
|
-
await c.cache_set("hotel email text", parsed)
|
|
90
|
-
|
|
91
|
-
mock_redis.set.assert_called_once()
|
|
92
|
-
call_args = mock_redis.set.call_args
|
|
93
|
-
assert call_args[1]["ex"] == c.CACHE_TTL
|
|
94
|
-
stored = json.loads(call_args[0][1])
|
|
95
|
-
assert stored["type"] == "hotel"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
@pytest.mark.asyncio
|
|
99
|
-
async def test_cache_get_survives_redis_error():
|
|
100
|
-
from app.services import cache as c
|
|
101
|
-
|
|
102
|
-
mock_redis = AsyncMock()
|
|
103
|
-
mock_redis.get = AsyncMock(side_effect=Exception("connection refused"))
|
|
104
|
-
|
|
105
|
-
c._redis_client = mock_redis
|
|
106
|
-
c._redis_available = True
|
|
107
|
-
|
|
108
|
-
result = await c.cache_get("any content")
|
|
109
|
-
assert result is None # never raises
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
@pytest.mark.asyncio
|
|
113
|
-
async def test_cache_set_survives_redis_error():
|
|
114
|
-
from app.services import cache as c
|
|
115
|
-
|
|
116
|
-
mock_redis = AsyncMock()
|
|
117
|
-
mock_redis.set = AsyncMock(side_effect=Exception("timeout"))
|
|
118
|
-
|
|
119
|
-
c._redis_client = mock_redis
|
|
120
|
-
c._redis_available = True
|
|
121
|
-
|
|
122
|
-
await c.cache_set("any content", {"type": "other"}) # never raises
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def test_cache_key_is_deterministic():
|
|
126
|
-
from app.services import cache as c
|
|
127
|
-
|
|
128
|
-
k1 = c._cache_key("same text")
|
|
129
|
-
k2 = c._cache_key("same text")
|
|
130
|
-
k3 = c._cache_key("different text")
|
|
131
|
-
|
|
132
|
-
assert k1 == k2
|
|
133
|
-
assert k1 != k3
|
|
134
|
-
assert k1.startswith("tco:parse:")
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def test_cache_key_works_for_bytes():
|
|
138
|
-
from app.services import cache as c
|
|
139
|
-
|
|
140
|
-
kb = c._cache_key(b"\x89PNG binary")
|
|
141
|
-
assert kb.startswith("tco:parse:")
|
|
142
|
-
assert len(kb) == len("tco:parse:") + 16
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
"""Tests for email drip sequence logic."""
|
|
2
|
-
import pytest
|
|
3
|
-
from unittest.mock import patch, AsyncMock
|
|
4
|
-
from sqlalchemy import text
|
|
5
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
6
|
-
from datetime import datetime, timedelta, timezone
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@pytest.mark.asyncio
|
|
10
|
-
async def test_drip_dry_run_returns_plan(client, db_session):
|
|
11
|
-
"""Dry run returns what would be sent without sending."""
|
|
12
|
-
r = await client.post("/admin/drip/run?dry_run=true",
|
|
13
|
-
headers={"Authorization": "Basic YWRtaW46Y2hhbmdlbWU="}) # admin:changeme
|
|
14
|
-
# Either 200 (admin auth works in test) or 401 (expected in test env)
|
|
15
|
-
assert r.status_code in (200, 401)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@pytest.mark.asyncio
|
|
19
|
-
async def test_drip_run_admin_required(client):
|
|
20
|
-
"""Drip run without auth returns 401."""
|
|
21
|
-
r = await client.post("/admin/drip/run")
|
|
22
|
-
assert r.status_code == 401
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@pytest.mark.asyncio
|
|
26
|
-
async def test_drip_no_users_no_emails(db_session: AsyncSession):
|
|
27
|
-
"""run_drip with no users sends nothing."""
|
|
28
|
-
from app.services.drip import run_drip
|
|
29
|
-
with patch("app.services.email._send", new_callable=AsyncMock) as mock_send:
|
|
30
|
-
result = await run_drip(db_session, dry_run=True)
|
|
31
|
-
mock_send.assert_not_called()
|
|
32
|
-
assert isinstance(result, dict)
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
E2E chain test: parse text → inbox → assign to trip.
|
|
3
|
-
Covers the full happy path a user would take.
|
|
4
|
-
"""
|
|
5
|
-
import json
|
|
6
|
-
import uuid
|
|
7
|
-
import pytest
|
|
8
|
-
from unittest.mock import patch, AsyncMock
|
|
9
|
-
from httpx import AsyncClient
|
|
10
|
-
from sqlalchemy import text
|
|
11
|
-
from tests.conftest import FAKE_USER_ID
|
|
12
|
-
|
|
13
|
-
# A fixed UUID for the "other user's trip" in cross-user tests
|
|
14
|
-
OTHER_TRIP_ID = "c3d4e5f6-a7b8-9012-cdef-123456789012"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@pytest.mark.asyncio
|
|
18
|
-
async def test_full_parse_to_assign_chain(client: AsyncClient, db_session, mock_ollama_flight):
|
|
19
|
-
"""Parse text → lands in inbox → assign to trip → trip has item."""
|
|
20
|
-
# 1. Create a trip
|
|
21
|
-
r = await client.post("/api/v1/trips", json={
|
|
22
|
-
"name": "E2E Test Trip", "description": "Chain test",
|
|
23
|
-
"start_date": "2025-08-01", "end_date": "2025-08-10"
|
|
24
|
-
})
|
|
25
|
-
assert r.status_code == 201
|
|
26
|
-
trip_id = r.json()["id"]
|
|
27
|
-
|
|
28
|
-
# 2. Parse text → inbox (no trip_id)
|
|
29
|
-
with patch("app.services.telemetry.track", new_callable=AsyncMock):
|
|
30
|
-
r = await client.post("/api/v1/parse/text", data={"raw_text": "Lufthansa Buchungsbestaetigung LH400 FRA-JFK"})
|
|
31
|
-
assert r.status_code == 200
|
|
32
|
-
assert r.json()["parsed"]["type"] == "flight"
|
|
33
|
-
|
|
34
|
-
# 3. Fetch inbox
|
|
35
|
-
r = await client.get("/api/v1/inbox?status_filter=pending")
|
|
36
|
-
assert r.status_code == 200
|
|
37
|
-
inbox = r.json()
|
|
38
|
-
assert len(inbox) >= 1
|
|
39
|
-
inbox_id = inbox[0]["id"]
|
|
40
|
-
|
|
41
|
-
# 4. Assign inbox item to trip
|
|
42
|
-
r = await client.post(f"/api/v1/inbox/{inbox_id}/assign",
|
|
43
|
-
json={"trip_id": trip_id, "type": "flight"})
|
|
44
|
-
assert r.status_code == 200
|
|
45
|
-
trip_item_id = r.json()["trip_item_id"]
|
|
46
|
-
assert trip_item_id
|
|
47
|
-
|
|
48
|
-
# 5. Verify item appears in trip timeline
|
|
49
|
-
r = await client.get(f"/api/v1/trips/{trip_id}/items")
|
|
50
|
-
assert r.status_code == 200
|
|
51
|
-
items = r.json()
|
|
52
|
-
assert any(i["id"] == trip_item_id for i in items)
|
|
53
|
-
|
|
54
|
-
# 6. Inbox item is now "assigned"
|
|
55
|
-
r = await client.get("/api/v1/inbox?status_filter=assigned")
|
|
56
|
-
assert r.status_code == 200
|
|
57
|
-
assigned = [i for i in r.json() if i["id"] == inbox_id]
|
|
58
|
-
assert len(assigned) == 1
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@pytest.mark.asyncio
|
|
62
|
-
async def test_cross_user_inbox_assign_blocked(client: AsyncClient, db_session, mock_ollama_flight):
|
|
63
|
-
"""Cannot assign inbox item to another user's trip."""
|
|
64
|
-
# Create a trip owned by a DIFFERENT user with a valid UUID
|
|
65
|
-
await db_session.execute(
|
|
66
|
-
text("INSERT INTO trips (id, user_id, name) VALUES (:id, :uid, :name)"),
|
|
67
|
-
{"id": OTHER_TRIP_ID, "uid": "other-user", "name": "Other Trip"}
|
|
68
|
-
)
|
|
69
|
-
await db_session.commit()
|
|
70
|
-
|
|
71
|
-
# Parse something into inbox
|
|
72
|
-
with patch("app.services.telemetry.track", new_callable=AsyncMock):
|
|
73
|
-
r = await client.post("/api/v1/parse/text", data={"raw_text": "Booking confirmation XYZ123"})
|
|
74
|
-
assert r.status_code == 200
|
|
75
|
-
|
|
76
|
-
r = await client.get("/api/v1/inbox?status_filter=pending")
|
|
77
|
-
inbox_id = r.json()[0]["id"]
|
|
78
|
-
|
|
79
|
-
# Try to assign to other user's trip — must be 403
|
|
80
|
-
r = await client.post(f"/api/v1/inbox/{inbox_id}/assign",
|
|
81
|
-
json={"trip_id": OTHER_TRIP_ID, "type": "other"})
|
|
82
|
-
assert r.status_code == 403
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
@pytest.mark.asyncio
|
|
86
|
-
async def test_trip_search_returns_correct_results(client: AsyncClient, db_session):
|
|
87
|
-
"""Search finds trips by name and description."""
|
|
88
|
-
# Create two trips
|
|
89
|
-
await client.post("/api/v1/trips", json={"name": "Barcelona Sommer", "description": "Strand"})
|
|
90
|
-
await client.post("/api/v1/trips", json={"name": "Winter Wien", "description": "Kultur"})
|
|
91
|
-
|
|
92
|
-
r = await client.get("/api/v1/trips/search?q=Barcelona")
|
|
93
|
-
assert r.status_code == 200
|
|
94
|
-
results = r.json()
|
|
95
|
-
assert any("Barcelona" in t["name"] for t in results)
|
|
96
|
-
assert all("Wien" not in t["name"] for t in results)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@pytest.mark.asyncio
|
|
100
|
-
async def test_delete_trip_cascades_cleanup(client: AsyncClient, db_session):
|
|
101
|
-
"""Deleting a trip removes it from the list."""
|
|
102
|
-
r = await client.post("/api/v1/trips", json={"name": "Delete Me Trip"})
|
|
103
|
-
trip_id = r.json()["id"]
|
|
104
|
-
|
|
105
|
-
r = await client.delete(f"/api/v1/trips/{trip_id}")
|
|
106
|
-
assert r.status_code == 204
|
|
107
|
-
|
|
108
|
-
r = await client.get("/api/v1/trips")
|
|
109
|
-
assert all(t["id"] != trip_id for t in r.json())
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
@pytest.mark.asyncio
|
|
113
|
-
async def test_waitlist_signup_deduplication(client: AsyncClient):
|
|
114
|
-
"""Same email can't sign up twice."""
|
|
115
|
-
email = "e2e-dedup@test.com"
|
|
116
|
-
r1 = await client.post("/api/v1/waitlist", json={"email": email})
|
|
117
|
-
assert r1.status_code in (200, 201)
|
|
118
|
-
|
|
119
|
-
r2 = await client.post("/api/v1/waitlist", json={"email": email})
|
|
120
|
-
# Should succeed (409 or 200 with "already registered") — not 500
|
|
121
|
-
assert r2.status_code < 500
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
"""Tests for PDF export endpoint."""
|
|
2
|
-
import uuid
|
|
3
|
-
import pytest
|
|
4
|
-
from unittest.mock import patch, AsyncMock
|
|
5
|
-
from httpx import AsyncClient
|
|
6
|
-
from sqlalchemy import text
|
|
7
|
-
from tests.conftest import FAKE_USER_ID
|
|
8
|
-
|
|
9
|
-
EXPORT_TRIP_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@pytest.mark.asyncio
|
|
13
|
-
async def test_pdf_export_trip_not_found(client: AsyncClient):
|
|
14
|
-
"""Returns 404 for non-existent trip."""
|
|
15
|
-
r = await client.get(f"/api/v1/trips/{uuid.uuid4()}/export/pdf")
|
|
16
|
-
assert r.status_code == 404
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@pytest.mark.asyncio
|
|
20
|
-
async def test_pdf_export_returns_pdf(client: AsyncClient, db_session):
|
|
21
|
-
"""Creates a trip and exports it as PDF — verifies content-type."""
|
|
22
|
-
# Create a trip directly in DB using a valid UUID
|
|
23
|
-
await db_session.execute(
|
|
24
|
-
text("INSERT INTO trips (id, user_id, name, start_date, end_date) VALUES (:id, :uid, :name, :sd, :ed)"),
|
|
25
|
-
{"id": EXPORT_TRIP_ID, "uid": FAKE_USER_ID, "name": "Export Test Trip",
|
|
26
|
-
"sd": "2025-06-01", "ed": "2025-06-07"}
|
|
27
|
-
)
|
|
28
|
-
await db_session.commit()
|
|
29
|
-
fake_pdf = b"%PDF-1.4 fake content for test"
|
|
30
|
-
with patch("app.routers.export._build_pdf", return_value=fake_pdf), \
|
|
31
|
-
patch("app.services.telemetry.track", new_callable=AsyncMock):
|
|
32
|
-
r = await client.get(f"/api/v1/trips/{EXPORT_TRIP_ID}/export/pdf")
|
|
33
|
-
assert r.status_code == 200
|
|
34
|
-
assert r.headers["content-type"] == "application/pdf"
|
|
35
|
-
assert len(r.content) > 0
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@pytest.mark.asyncio
|
|
39
|
-
async def test_pdf_export_other_users_trip_returns_404(client: AsyncClient, db_session):
|
|
40
|
-
"""Cannot export another user's trip."""
|
|
41
|
-
other_trip_id = "b2c3d4e5-f6a7-8901-bcde-f12345678901"
|
|
42
|
-
await db_session.execute(
|
|
43
|
-
text("INSERT INTO trips (id, user_id, name) VALUES (:id, :uid, :name)"),
|
|
44
|
-
{"id": other_trip_id, "uid": "other-user-999", "name": "Secret Trip"}
|
|
45
|
-
)
|
|
46
|
-
await db_session.commit()
|
|
47
|
-
r = await client.get(f"/api/v1/trips/{other_trip_id}/export/pdf")
|
|
48
|
-
assert r.status_code == 404
|