@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.
Files changed (270) hide show
  1. package/dist/src/handlers/brain.js +1 -1
  2. package/dist/src/index.js +1 -1
  3. package/package.json +7 -1
  4. package/server.json +2 -2
  5. package/.env.test.example +0 -23
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
  7. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  8. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  9. package/.github/workflows/eas-build-android.yml +0 -58
  10. package/.github/workflows/eas-update.yml +0 -55
  11. package/.github/workflows/publish.yml +0 -72
  12. package/BENCH.md +0 -91
  13. package/CHANGELOG.md +0 -604
  14. package/CHANGELOG.md.tmp +0 -0
  15. package/Dockerfile +0 -16
  16. package/LAUNCH_TEXTS.md +0 -182
  17. package/PROGRESS.md +0 -137
  18. package/STRATEGY.md +0 -228
  19. package/VISION_10X.md +0 -133
  20. package/apps/travel-chaos-organizer/.env.example +0 -49
  21. package/apps/travel-chaos-organizer/DEPLOY.md +0 -328
  22. package/apps/travel-chaos-organizer/Makefile +0 -57
  23. package/apps/travel-chaos-organizer/README.md +0 -159
  24. package/apps/travel-chaos-organizer/backend/.env.example +0 -10
  25. package/apps/travel-chaos-organizer/backend/Dockerfile +0 -25
  26. package/apps/travel-chaos-organizer/backend/alembic/env.py +0 -56
  27. package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +0 -24
  28. package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
  29. package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +0 -24
  30. package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +0 -24
  31. package/apps/travel-chaos-organizer/backend/alembic.ini +0 -39
  32. package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
  33. package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
  34. package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +0 -81
  35. package/apps/travel-chaos-organizer/backend/app/config.py +0 -66
  36. package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
  37. package/apps/travel-chaos-organizer/backend/app/db/database.py +0 -20
  38. package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +0 -71
  39. package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +0 -31
  40. package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +0 -4
  41. package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +0 -5
  42. package/apps/travel-chaos-organizer/backend/app/limiter.py +0 -4
  43. package/apps/travel-chaos-organizer/backend/app/logging_config.py +0 -25
  44. package/apps/travel-chaos-organizer/backend/app/main.py +0 -77
  45. package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
  46. package/apps/travel-chaos-organizer/backend/app/models/schemas.py +0 -142
  47. package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
  48. package/apps/travel-chaos-organizer/backend/app/routers/admin.py +0 -237
  49. package/apps/travel-chaos-organizer/backend/app/routers/cache.py +0 -13
  50. package/apps/travel-chaos-organizer/backend/app/routers/events.py +0 -29
  51. package/apps/travel-chaos-organizer/backend/app/routers/export.py +0 -122
  52. package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +0 -122
  53. package/apps/travel-chaos-organizer/backend/app/routers/items.py +0 -130
  54. package/apps/travel-chaos-organizer/backend/app/routers/mail.py +0 -108
  55. package/apps/travel-chaos-organizer/backend/app/routers/parse.py +0 -303
  56. package/apps/travel-chaos-organizer/backend/app/routers/payments.py +0 -179
  57. package/apps/travel-chaos-organizer/backend/app/routers/trips.py +0 -134
  58. package/apps/travel-chaos-organizer/backend/app/routers/users.py +0 -169
  59. package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +0 -37
  60. package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
  61. package/apps/travel-chaos-organizer/backend/app/services/cache.py +0 -131
  62. package/apps/travel-chaos-organizer/backend/app/services/drip.py +0 -128
  63. package/apps/travel-chaos-organizer/backend/app/services/email.py +0 -61
  64. package/apps/travel-chaos-organizer/backend/app/services/errors.py +0 -44
  65. package/apps/travel-chaos-organizer/backend/app/services/notifier.py +0 -10
  66. package/apps/travel-chaos-organizer/backend/app/services/ollama.py +0 -101
  67. package/apps/travel-chaos-organizer/backend/app/services/parser.py +0 -22
  68. package/apps/travel-chaos-organizer/backend/app/services/quota.py +0 -85
  69. package/apps/travel-chaos-organizer/backend/app/services/sentry.py +0 -11
  70. package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +0 -35
  71. package/apps/travel-chaos-organizer/backend/pytest.ini +0 -4
  72. package/apps/travel-chaos-organizer/backend/requirements-dev.txt +0 -6
  73. package/apps/travel-chaos-organizer/backend/requirements.txt +0 -25
  74. package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
  75. package/apps/travel-chaos-organizer/backend/tests/conftest.py +0 -223
  76. package/apps/travel-chaos-organizer/backend/tests/test_admin.py +0 -99
  77. package/apps/travel-chaos-organizer/backend/tests/test_cache.py +0 -142
  78. package/apps/travel-chaos-organizer/backend/tests/test_drip.py +0 -32
  79. package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +0 -121
  80. package/apps/travel-chaos-organizer/backend/tests/test_export.py +0 -48
  81. package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +0 -107
  82. package/apps/travel-chaos-organizer/backend/tests/test_items.py +0 -138
  83. package/apps/travel-chaos-organizer/backend/tests/test_mail.py +0 -113
  84. package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +0 -145
  85. package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +0 -98
  86. package/apps/travel-chaos-organizer/backend/tests/test_parser.py +0 -72
  87. package/apps/travel-chaos-organizer/backend/tests/test_payments.py +0 -163
  88. package/apps/travel-chaos-organizer/backend/tests/test_quota.py +0 -46
  89. package/apps/travel-chaos-organizer/backend/tests/test_search.py +0 -67
  90. package/apps/travel-chaos-organizer/backend/tests/test_trips.py +0 -123
  91. package/apps/travel-chaos-organizer/backend/tests/test_users.py +0 -52
  92. package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
  93. package/apps/travel-chaos-organizer/caddy/Caddyfile +0 -27
  94. package/apps/travel-chaos-organizer/docker-compose.node1.yml +0 -31
  95. package/apps/travel-chaos-organizer/docker-compose.override.yml +0 -14
  96. package/apps/travel-chaos-organizer/docker-compose.yml +0 -128
  97. package/apps/travel-chaos-organizer/frontend/.env.example +0 -13
  98. package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +0 -80
  99. package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +0 -166
  100. package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +0 -130
  101. package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +0 -381
  102. package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +0 -346
  103. package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +0 -363
  104. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +0 -34
  105. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +0 -35
  106. package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +0 -91
  107. package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +0 -71
  108. package/apps/travel-chaos-organizer/frontend/app/auth.tsx +0 -29
  109. package/apps/travel-chaos-organizer/frontend/app/index.tsx +0 -16
  110. package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +0 -119
  111. package/apps/travel-chaos-organizer/frontend/app.config.ts +0 -54
  112. package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +0 -66
  113. package/apps/travel-chaos-organizer/frontend/assets/icon.svg +0 -79
  114. package/apps/travel-chaos-organizer/frontend/assets/splash.svg +0 -15
  115. package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +0 -102
  116. package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +0 -107
  117. package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +0 -109
  118. package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +0 -45
  119. package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +0 -19
  120. package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +0 -155
  121. package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +0 -55
  122. package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +0 -80
  123. package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +0 -39
  124. package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +0 -39
  125. package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +0 -70
  126. package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +0 -121
  127. package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +0 -70
  128. package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +0 -73
  129. package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +0 -130
  130. package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +0 -60
  131. package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +0 -42
  132. package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +0 -146
  133. package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +0 -70
  134. package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +0 -44
  135. package/apps/travel-chaos-organizer/frontend/eas.json +0 -22
  136. package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +0 -24
  137. package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +0 -60
  138. package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +0 -112
  139. package/apps/travel-chaos-organizer/frontend/lib/api.ts +0 -223
  140. package/apps/travel-chaos-organizer/frontend/lib/auth.ts +0 -93
  141. package/apps/travel-chaos-organizer/frontend/lib/db.ts +0 -86
  142. package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +0 -54
  143. package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +0 -13
  144. package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +0 -112
  145. package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +0 -145
  146. package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +0 -43
  147. package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +0 -28
  148. package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +0 -44
  149. package/apps/travel-chaos-organizer/frontend/package-lock.json +0 -13221
  150. package/apps/travel-chaos-organizer/frontend/package.json +0 -44
  151. package/apps/travel-chaos-organizer/frontend/tsconfig.json +0 -23
  152. package/apps/travel-chaos-organizer/keycloak/realm-export.json +0 -59
  153. package/apps/travel-chaos-organizer/landing/index.html +0 -773
  154. package/apps/travel-chaos-organizer/scripts/backup.sh +0 -14
  155. package/apps/travel-chaos-organizer/scripts/configure-env.sh +0 -82
  156. package/apps/travel-chaos-organizer/scripts/deploy.sh +0 -54
  157. package/apps/travel-chaos-organizer/scripts/health-check.sh +0 -36
  158. package/apps/travel-chaos-organizer/scripts/migrate.sh +0 -7
  159. package/apps/travel-chaos-organizer/scripts/server-setup.sh +0 -85
  160. package/apps/travel-chaos-organizer/scripts/smoke-test.sh +0 -29
  161. package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +0 -71
  162. package/apps/travel-chaos-organizer/telegram-bot/.env.example +0 -3
  163. package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +0 -6
  164. package/apps/travel-chaos-organizer/telegram-bot/bot.py +0 -504
  165. package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +0 -3
  166. package/dist/src/__tests__/auth.test.d.ts +0 -1
  167. package/dist/src/__tests__/auth.test.js +0 -107
  168. package/dist/src/__tests__/auth.test.js.map +0 -1
  169. package/dist/src/__tests__/brain-flow.test.d.ts +0 -12
  170. package/dist/src/__tests__/brain-flow.test.js +0 -1241
  171. package/dist/src/__tests__/brain-flow.test.js.map +0 -1
  172. package/dist/src/__tests__/confidence.test.d.ts +0 -13
  173. package/dist/src/__tests__/confidence.test.js +0 -233
  174. package/dist/src/__tests__/confidence.test.js.map +0 -1
  175. package/dist/src/__tests__/e2e/api-auth.test.d.ts +0 -7
  176. package/dist/src/__tests__/e2e/api-auth.test.js +0 -162
  177. package/dist/src/__tests__/e2e/api-auth.test.js.map +0 -1
  178. package/dist/src/__tests__/e2e/api-health.test.d.ts +0 -7
  179. package/dist/src/__tests__/e2e/api-health.test.js +0 -95
  180. package/dist/src/__tests__/e2e/api-health.test.js.map +0 -1
  181. package/dist/src/__tests__/e2e/cli.test.d.ts +0 -10
  182. package/dist/src/__tests__/e2e/cli.test.js +0 -204
  183. package/dist/src/__tests__/e2e/cli.test.js.map +0 -1
  184. package/dist/src/__tests__/e2e/config.d.ts +0 -35
  185. package/dist/src/__tests__/e2e/config.js +0 -53
  186. package/dist/src/__tests__/e2e/config.js.map +0 -1
  187. package/dist/src/__tests__/e2e/funnel.test.d.ts +0 -13
  188. package/dist/src/__tests__/e2e/funnel.test.js +0 -90
  189. package/dist/src/__tests__/e2e/funnel.test.js.map +0 -1
  190. package/dist/src/__tests__/e2e/mcp-tools.test.d.ts +0 -7
  191. package/dist/src/__tests__/e2e/mcp-tools.test.js +0 -129
  192. package/dist/src/__tests__/e2e/mcp-tools.test.js.map +0 -1
  193. package/dist/src/__tests__/e2e/provisioning.test.d.ts +0 -15
  194. package/dist/src/__tests__/e2e/provisioning.test.js +0 -123
  195. package/dist/src/__tests__/e2e/provisioning.test.js.map +0 -1
  196. package/dist/src/__tests__/handlers-unit.test.d.ts +0 -12
  197. package/dist/src/__tests__/handlers-unit.test.js +0 -505
  198. package/dist/src/__tests__/handlers-unit.test.js.map +0 -1
  199. package/dist/src/__tests__/namespace.test.d.ts +0 -1
  200. package/dist/src/__tests__/namespace.test.js +0 -114
  201. package/dist/src/__tests__/namespace.test.js.map +0 -1
  202. package/dist/src/__tests__/rerank.test.d.ts +0 -1
  203. package/dist/src/__tests__/rerank.test.js +0 -115
  204. package/dist/src/__tests__/rerank.test.js.map +0 -1
  205. package/dist/src/__tests__/utils.test.d.ts +0 -1
  206. package/dist/src/__tests__/utils.test.js +0 -83
  207. package/dist/src/__tests__/utils.test.js.map +0 -1
  208. package/dist/src/bench/cachly-bench.d.ts +0 -35
  209. package/dist/src/bench/cachly-bench.js +0 -182
  210. package/dist/src/bench/cachly-bench.js.map +0 -1
  211. package/dist/src/bench/fixtures.d.ts +0 -37
  212. package/dist/src/bench/fixtures.js +0 -225
  213. package/dist/src/bench/fixtures.js.map +0 -1
  214. package/dist/src/search.test.d.ts +0 -1
  215. package/dist/src/search.test.js +0 -713
  216. package/dist/src/search.test.js.map +0 -1
  217. package/eslint.config.js +0 -16
  218. package/glama.json +0 -36
  219. package/llms.txt +0 -155
  220. package/packages/telegram-notify/SPLIT_PLAN.md +0 -106
  221. package/packages/telegram-notify/client.ts +0 -88
  222. package/packages/telegram-notify/emojis.json +0 -22
  223. package/packages/telegram-notify/pyproject.toml +0 -17
  224. package/packages/telegram-notify/telegram_notify/__init__.py +0 -3
  225. package/packages/telegram-notify/telegram_notify/client.py +0 -87
  226. package/packages/telegram-notify/telegram_notify/emojis.json +0 -22
  227. package/scripts/e2e-run.sh +0 -160
  228. package/scripts/generate-icons.js +0 -28
  229. package/smithery.yaml +0 -45
  230. package/src/__tests__/auth.test.ts +0 -147
  231. package/src/__tests__/brain-flow.test.ts +0 -1472
  232. package/src/__tests__/confidence.test.ts +0 -285
  233. package/src/__tests__/e2e/api-auth.test.ts +0 -182
  234. package/src/__tests__/e2e/api-health.test.ts +0 -103
  235. package/src/__tests__/e2e/cli.test.ts +0 -228
  236. package/src/__tests__/e2e/config.ts +0 -53
  237. package/src/__tests__/e2e/funnel.test.ts +0 -99
  238. package/src/__tests__/e2e/mcp-tools.test.ts +0 -191
  239. package/src/__tests__/e2e/provisioning.test.ts +0 -128
  240. package/src/__tests__/handlers-unit.test.ts +0 -677
  241. package/src/__tests__/namespace.test.ts +0 -126
  242. package/src/__tests__/rerank.test.ts +0 -130
  243. package/src/__tests__/utils.test.ts +0 -81
  244. package/src/auth.ts +0 -46
  245. package/src/bench/cachly-bench.ts +0 -217
  246. package/src/bench/fixtures.ts +0 -256
  247. package/src/ckg.ts +0 -103
  248. package/src/confidence.ts +0 -38
  249. package/src/embeddings.ts +0 -217
  250. package/src/handlers/advanced.ts +0 -647
  251. package/src/handlers/brain.ts +0 -2622
  252. package/src/handlers/cache.ts +0 -633
  253. package/src/handlers/context.ts +0 -222
  254. package/src/handlers/fedbrain.ts +0 -1232
  255. package/src/handlers/instances.ts +0 -180
  256. package/src/handlers/roadmap.ts +0 -201
  257. package/src/handlers/syndicate.ts +0 -460
  258. package/src/handlers/tco.ts +0 -345
  259. package/src/handlers/team.ts +0 -1249
  260. package/src/index.ts +0 -2815
  261. package/src/namespace.ts +0 -128
  262. package/src/notifier.ts +0 -5
  263. package/src/rerank.ts +0 -148
  264. package/src/search.test.ts +0 -848
  265. package/src/search.ts +0 -1781
  266. package/src/tools.ts +0 -2056
  267. package/src/utils.ts +0 -90
  268. package/test-features.cjs +0 -23
  269. package/tsconfig.json +0 -16
  270. 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