@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,107 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tests for /api/v1/inbox
|
|
3
|
-
|
|
4
|
-
Covers:
|
|
5
|
-
list_inbox – empty, pending filter, other status filters
|
|
6
|
-
assign – moves item to trip, updates status to 'assigned', migrates attachments
|
|
7
|
-
reject – sets status to 'rejected', idempotent on 404
|
|
8
|
-
|
|
9
|
-
Run: pytest tests/test_inbox.py -v
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import pytest
|
|
13
|
-
from httpx import AsyncClient
|
|
14
|
-
from tests.conftest import make_trip
|
|
15
|
-
|
|
16
|
-
pytestmark = pytest.mark.asyncio
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
async def _create_trip(client: AsyncClient) -> str:
|
|
20
|
-
r = await client.post("/api/v1/trips", json=make_trip())
|
|
21
|
-
assert r.status_code == 201
|
|
22
|
-
return r.json()["id"]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
async def _seed_inbox(client: AsyncClient) -> str:
|
|
26
|
-
"""Parse some text so it lands in inbox (no trip_id → inbox)."""
|
|
27
|
-
r = await client.post(
|
|
28
|
-
"/api/v1/parse/text",
|
|
29
|
-
data={"raw_text": "Flight LH 400 Frankfurt to New York on Aug 15"},
|
|
30
|
-
)
|
|
31
|
-
assert r.status_code == 200
|
|
32
|
-
# fetch inbox to get the id
|
|
33
|
-
inbox = await client.get("/api/v1/inbox")
|
|
34
|
-
assert inbox.status_code == 200
|
|
35
|
-
items = inbox.json()
|
|
36
|
-
assert len(items) > 0
|
|
37
|
-
return items[0]["id"]
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# ── list ──────────────────────────────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
async def test_inbox_empty(client):
|
|
43
|
-
r = await client.get("/api/v1/inbox")
|
|
44
|
-
assert r.status_code == 200
|
|
45
|
-
assert r.json() == []
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
async def test_inbox_default_status_is_pending(client, mock_ollama_flight):
|
|
49
|
-
await _seed_inbox(client)
|
|
50
|
-
r = await client.get("/api/v1/inbox")
|
|
51
|
-
assert r.status_code == 200
|
|
52
|
-
items = r.json()
|
|
53
|
-
assert all(i["status"] == "pending" for i in items)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
async def test_inbox_filter_by_status(client, mock_ollama_flight):
|
|
57
|
-
inbox_id = await _seed_inbox(client)
|
|
58
|
-
await client.delete(f"/api/v1/inbox/{inbox_id}")
|
|
59
|
-
|
|
60
|
-
r = await client.get("/api/v1/inbox?status_filter=rejected")
|
|
61
|
-
assert r.status_code == 200
|
|
62
|
-
assert any(i["id"] == inbox_id for i in r.json())
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# ── assign ────────────────────────────────────────────────────────────────────
|
|
66
|
-
|
|
67
|
-
async def test_assign_creates_trip_item(client, mock_ollama_flight):
|
|
68
|
-
trip_id = await _create_trip(client)
|
|
69
|
-
inbox_id = await _seed_inbox(client)
|
|
70
|
-
|
|
71
|
-
r = await client.post(
|
|
72
|
-
f"/api/v1/inbox/{inbox_id}/assign",
|
|
73
|
-
json={"trip_id": trip_id, "type": "flight"},
|
|
74
|
-
)
|
|
75
|
-
assert r.status_code == 200
|
|
76
|
-
assert "trip_item_id" in r.json()
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
async def test_assign_marks_inbox_as_assigned(client, mock_ollama_flight):
|
|
80
|
-
trip_id = await _create_trip(client)
|
|
81
|
-
inbox_id = await _seed_inbox(client)
|
|
82
|
-
await client.post(f"/api/v1/inbox/{inbox_id}/assign",
|
|
83
|
-
json={"trip_id": trip_id, "type": "flight"})
|
|
84
|
-
|
|
85
|
-
r = await client.get("/api/v1/inbox?status_filter=assigned")
|
|
86
|
-
assert any(i["id"] == inbox_id for i in r.json())
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
async def test_assign_not_found(client):
|
|
90
|
-
r = await client.post(
|
|
91
|
-
"/api/v1/inbox/00000000-0000-0000-0000-000000000000/assign",
|
|
92
|
-
json={"trip_id": "00000000-0000-0000-0000-000000000001", "type": "other"},
|
|
93
|
-
)
|
|
94
|
-
assert r.status_code == 404
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
# ── reject ────────────────────────────────────────────────────────────────────
|
|
98
|
-
|
|
99
|
-
async def test_reject_inbox_item(client, mock_ollama_flight):
|
|
100
|
-
inbox_id = await _seed_inbox(client)
|
|
101
|
-
r = await client.delete(f"/api/v1/inbox/{inbox_id}")
|
|
102
|
-
assert r.status_code == 204
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
async def test_reject_not_found(client):
|
|
106
|
-
r = await client.delete("/api/v1/inbox/00000000-0000-0000-0000-000000000000")
|
|
107
|
-
assert r.status_code == 404
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tests for /api/v1/trips/{trip_id}/items
|
|
3
|
-
|
|
4
|
-
Covers:
|
|
5
|
-
list_items – empty, sorted by event_at, trip isolation
|
|
6
|
-
create_item – happy path, optional fields, missing title → 422
|
|
7
|
-
update_item – partial patch, type change, 404 for wrong trip
|
|
8
|
-
delete_item – removes item, 404 guard
|
|
9
|
-
timeline order – items sorted chronologically regardless of insert order
|
|
10
|
-
|
|
11
|
-
Run: pytest tests/test_items.py -v
|
|
12
|
-
"""
|
|
13
|
-
import pytest
|
|
14
|
-
from httpx import AsyncClient
|
|
15
|
-
from tests.conftest import make_trip, make_item
|
|
16
|
-
|
|
17
|
-
pytestmark = pytest.mark.asyncio
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
async def _create_trip(client: AsyncClient) -> str:
|
|
21
|
-
r = await client.post("/api/v1/trips", json=make_trip())
|
|
22
|
-
assert r.status_code == 201
|
|
23
|
-
return r.json()["id"]
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
async def _create_item(client: AsyncClient, trip_id: str, **kwargs) -> dict:
|
|
27
|
-
r = await client.post(f"/api/v1/trips/{trip_id}/items", json=make_item(**kwargs))
|
|
28
|
-
assert r.status_code == 201, r.text
|
|
29
|
-
return r.json()
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# ── list ──────────────────────────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
async def test_list_items_empty(client):
|
|
35
|
-
trip_id = await _create_trip(client)
|
|
36
|
-
r = await client.get(f"/api/v1/trips/{trip_id}/items")
|
|
37
|
-
assert r.status_code == 200
|
|
38
|
-
assert r.json() == []
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
async def test_list_items_wrong_trip_404(client):
|
|
42
|
-
r = await client.get("/api/v1/trips/00000000-0000-0000-0000-000000000000/items")
|
|
43
|
-
assert r.status_code == 404
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
async def test_timeline_sorted_by_event_at(client):
|
|
47
|
-
trip_id = await _create_trip(client)
|
|
48
|
-
await _create_item(client, trip_id, title="Late flight", event_at="2024-08-20T18:00:00")
|
|
49
|
-
await _create_item(client, trip_id, title="Early flight", event_at="2024-08-15T06:00:00")
|
|
50
|
-
await _create_item(client, trip_id, title="Mid hotel", event_at="2024-08-17T14:00:00")
|
|
51
|
-
|
|
52
|
-
r = await client.get(f"/api/v1/trips/{trip_id}/items")
|
|
53
|
-
titles = [i["title"] for i in r.json()]
|
|
54
|
-
assert titles == ["Early flight", "Mid hotel", "Late flight"]
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
# ── create ────────────────────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
async def test_create_item_happy_path(client):
|
|
60
|
-
trip_id = await _create_trip(client)
|
|
61
|
-
r = await client.post(f"/api/v1/trips/{trip_id}/items", json=make_item(title="LH 400 FRA→JFK"))
|
|
62
|
-
assert r.status_code == 201
|
|
63
|
-
body = r.json()
|
|
64
|
-
assert body["title"] == "LH 400 FRA→JFK"
|
|
65
|
-
assert body["type"] == "flight"
|
|
66
|
-
assert body["trip_id"] == trip_id
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
async def test_create_item_missing_title_rejected(client):
|
|
70
|
-
trip_id = await _create_trip(client)
|
|
71
|
-
r = await client.post(f"/api/v1/trips/{trip_id}/items", json={"type": "flight"})
|
|
72
|
-
assert r.status_code == 422
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
async def test_create_item_optional_fields_nullable(client):
|
|
76
|
-
trip_id = await _create_trip(client)
|
|
77
|
-
r = await client.post(f"/api/v1/trips/{trip_id}/items",
|
|
78
|
-
json={"title": "Minimal item", "type": "other"})
|
|
79
|
-
assert r.status_code == 201
|
|
80
|
-
body = r.json()
|
|
81
|
-
assert body["event_at"] is None
|
|
82
|
-
assert body["booking_ref"] is None
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
async def test_create_item_with_parsed_data(client):
|
|
86
|
-
trip_id = await _create_trip(client)
|
|
87
|
-
parsed = {"origin": "FRA", "destination": "JFK", "confidence": 0.95}
|
|
88
|
-
r = await client.post(f"/api/v1/trips/{trip_id}/items",
|
|
89
|
-
json={**make_item(), "parsed_data": parsed})
|
|
90
|
-
assert r.status_code == 201
|
|
91
|
-
assert r.json()["parsed_data"]["origin"] == "FRA"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
# ── update ────────────────────────────────────────────────────────────────────
|
|
95
|
-
|
|
96
|
-
async def test_update_item_type(client):
|
|
97
|
-
trip_id = await _create_trip(client)
|
|
98
|
-
item = await _create_item(client, trip_id)
|
|
99
|
-
r = await client.patch(f"/api/v1/trips/{trip_id}/items/{item['id']}",
|
|
100
|
-
json={"type": "hotel"})
|
|
101
|
-
assert r.status_code == 200
|
|
102
|
-
assert r.json()["type"] == "hotel"
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
async def test_update_item_not_found(client):
|
|
106
|
-
trip_id = await _create_trip(client)
|
|
107
|
-
r = await client.patch(
|
|
108
|
-
f"/api/v1/trips/{trip_id}/items/00000000-0000-0000-0000-000000000000",
|
|
109
|
-
json={"title": "Ghost"},
|
|
110
|
-
)
|
|
111
|
-
assert r.status_code == 404
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
async def test_update_item_empty_body_rejected(client):
|
|
115
|
-
trip_id = await _create_trip(client)
|
|
116
|
-
item = await _create_item(client, trip_id)
|
|
117
|
-
r = await client.patch(f"/api/v1/trips/{trip_id}/items/{item['id']}", json={})
|
|
118
|
-
assert r.status_code == 400
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# ── delete ────────────────────────────────────────────────────────────────────
|
|
122
|
-
|
|
123
|
-
async def test_delete_item(client):
|
|
124
|
-
trip_id = await _create_trip(client)
|
|
125
|
-
item = await _create_item(client, trip_id)
|
|
126
|
-
r = await client.delete(f"/api/v1/trips/{trip_id}/items/{item['id']}")
|
|
127
|
-
assert r.status_code == 204
|
|
128
|
-
|
|
129
|
-
items = await client.get(f"/api/v1/trips/{trip_id}/items")
|
|
130
|
-
assert items.json() == []
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
async def test_delete_item_not_found(client):
|
|
134
|
-
trip_id = await _create_trip(client)
|
|
135
|
-
r = await client.delete(
|
|
136
|
-
f"/api/v1/trips/{trip_id}/items/00000000-0000-0000-0000-000000000000"
|
|
137
|
-
)
|
|
138
|
-
assert r.status_code == 404
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tests for /api/v1/mail/import
|
|
3
|
-
|
|
4
|
-
Covers:
|
|
5
|
-
strip_email_headers – removes From/To/Subject/X-* lines, preserves body
|
|
6
|
-
import_mail – text body → inbox when no trip_id
|
|
7
|
-
import_mail – text body → trip item when trip_id provided
|
|
8
|
-
import_mail – stripped headers don't leak into parsed_data
|
|
9
|
-
|
|
10
|
-
Run: pytest tests/test_mail.py -v
|
|
11
|
-
"""
|
|
12
|
-
import pytest
|
|
13
|
-
from httpx import AsyncClient
|
|
14
|
-
|
|
15
|
-
from app.routers.mail import strip_email_headers
|
|
16
|
-
from tests.conftest import make_trip, mock_ollama_flight
|
|
17
|
-
|
|
18
|
-
RAW_EMAIL = """\
|
|
19
|
-
From: booking@lufthansa.com
|
|
20
|
-
To: max@example.com
|
|
21
|
-
Subject: Ihre Buchungsbestätigung LH 400
|
|
22
|
-
Date: Mon, 12 Aug 2024 09:00:00 +0200
|
|
23
|
-
Content-Type: text/plain; charset=utf-8
|
|
24
|
-
|
|
25
|
-
Sehr geehrter Herr Mustermann,
|
|
26
|
-
|
|
27
|
-
Ihre Buchung wurde bestätigt.
|
|
28
|
-
Flug LH 400 Frankfurt (FRA) → New York (JFK)
|
|
29
|
-
Abflug: 15. August 2024, 10:30 Uhr
|
|
30
|
-
Buchungsreferenz: ABC123
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# ── strip_email_headers (pure, synchronous) ───────────────────────────────────
|
|
35
|
-
|
|
36
|
-
def test_strip_removes_from_header():
|
|
37
|
-
result = strip_email_headers(RAW_EMAIL)
|
|
38
|
-
assert "From:" not in result
|
|
39
|
-
assert "To:" not in result
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def test_strip_removes_subject():
|
|
43
|
-
result = strip_email_headers(RAW_EMAIL)
|
|
44
|
-
assert "Subject:" not in result
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def test_strip_preserves_body():
|
|
48
|
-
result = strip_email_headers(RAW_EMAIL)
|
|
49
|
-
assert "LH 400" in result
|
|
50
|
-
assert "ABC123" in result
|
|
51
|
-
assert "Mustermann" in result
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def test_strip_x_headers():
|
|
55
|
-
raw = "X-Mailer: Outlook\nX-Spam-Status: No\n\nBody content here."
|
|
56
|
-
result = strip_email_headers(raw)
|
|
57
|
-
assert "X-Mailer" not in result
|
|
58
|
-
assert "Body content here" in result
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def test_strip_collapses_blank_lines():
|
|
62
|
-
raw = "From: a@b.com\n\n\n\n\nBody text."
|
|
63
|
-
result = strip_email_headers(raw)
|
|
64
|
-
assert "\n\n\n" not in result
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# ── import_mail endpoint ──────────────────────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
@pytest.mark.asyncio
|
|
70
|
-
async def test_import_mail_goes_to_inbox_without_trip(client: AsyncClient, mock_ollama_flight):
|
|
71
|
-
r = await client.post(
|
|
72
|
-
"/api/v1/mail/import",
|
|
73
|
-
content=RAW_EMAIL,
|
|
74
|
-
headers={"Content-Type": "text/plain"},
|
|
75
|
-
)
|
|
76
|
-
assert r.status_code == 200
|
|
77
|
-
body = r.json()
|
|
78
|
-
assert body["parsed"]["type"] == "flight"
|
|
79
|
-
assert body["parsed"]["booking_ref"] == "ABC123"
|
|
80
|
-
|
|
81
|
-
inbox = await client.get("/api/v1/inbox")
|
|
82
|
-
assert inbox.status_code == 200
|
|
83
|
-
assert len(inbox.json()) == 1
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
@pytest.mark.asyncio
|
|
87
|
-
async def test_import_mail_assigned_to_trip(client: AsyncClient, mock_ollama_flight):
|
|
88
|
-
trip_r = await client.post("/api/v1/trips", json=make_trip())
|
|
89
|
-
trip_id = trip_r.json()["id"]
|
|
90
|
-
|
|
91
|
-
r = await client.post(
|
|
92
|
-
f"/api/v1/mail/import?trip_id={trip_id}",
|
|
93
|
-
content=RAW_EMAIL,
|
|
94
|
-
headers={"Content-Type": "text/plain"},
|
|
95
|
-
)
|
|
96
|
-
assert r.status_code == 200
|
|
97
|
-
|
|
98
|
-
items_r = await client.get(f"/api/v1/trips/{trip_id}/items")
|
|
99
|
-
assert len(items_r.json()) == 1
|
|
100
|
-
|
|
101
|
-
inbox = await client.get("/api/v1/inbox")
|
|
102
|
-
assert inbox.json() == []
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
@pytest.mark.asyncio
|
|
106
|
-
async def test_import_mail_low_confidence_still_saves(client: AsyncClient, mock_ollama_low_confidence):
|
|
107
|
-
r = await client.post(
|
|
108
|
-
"/api/v1/mail/import",
|
|
109
|
-
content="Some random text that isn't a booking",
|
|
110
|
-
headers={"Content-Type": "text/plain"},
|
|
111
|
-
)
|
|
112
|
-
assert r.status_code == 200
|
|
113
|
-
assert r.json()["parsed"]["confidence"] == 0.1
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Unit tests for the Ollama service layer.
|
|
3
|
-
|
|
4
|
-
Covers:
|
|
5
|
-
parse_text – valid JSON response, malformed JSON fallback, empty response
|
|
6
|
-
parse_image – base64 encoding, correct payload shape
|
|
7
|
-
_safe_parse – markdown fence stripping, JSON decode errors → fallback dict
|
|
8
|
-
confidence – low-confidence items still return a dict (never raise)
|
|
9
|
-
|
|
10
|
-
Run: pytest tests/test_ollama.py -v
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
import json
|
|
14
|
-
from unittest.mock import AsyncMock, MagicMock, patch
|
|
15
|
-
|
|
16
|
-
import pytest
|
|
17
|
-
|
|
18
|
-
from app.services.ollama import _safe_parse, parse_image, parse_text
|
|
19
|
-
|
|
20
|
-
# ── _safe_parse (pure, synchronous) ──────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
def test_safe_parse_valid_json():
|
|
23
|
-
raw = '{"type": "flight", "title": "LH 400", "confidence": 0.9}'
|
|
24
|
-
result = _safe_parse(raw)
|
|
25
|
-
assert result["type"] == "flight"
|
|
26
|
-
assert result["confidence"] == 0.9
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def test_safe_parse_strips_markdown_fences():
|
|
30
|
-
raw = "```json\n{\"type\": \"hotel\", \"title\": \"Marriott\"}\n```"
|
|
31
|
-
result = _safe_parse(raw)
|
|
32
|
-
assert result["type"] == "hotel"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def test_safe_parse_strips_plain_fences():
|
|
36
|
-
raw = "```\n{\"type\": \"train\"}\n```"
|
|
37
|
-
assert _safe_parse(raw)["type"] == "train"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def test_safe_parse_malformed_returns_fallback():
|
|
41
|
-
result = _safe_parse("this is not json at all")
|
|
42
|
-
assert result["type"] == "other"
|
|
43
|
-
assert result["confidence"] == 0.0
|
|
44
|
-
assert "Unrecognized" in result["title"]
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def test_safe_parse_empty_string_returns_fallback():
|
|
48
|
-
result = _safe_parse("")
|
|
49
|
-
assert result["type"] == "other"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def test_safe_parse_truncates_long_summary():
|
|
53
|
-
long_text = "x" * 1000
|
|
54
|
-
result = _safe_parse(long_text)
|
|
55
|
-
assert len(result.get("raw_summary", "")) <= 300
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# ── parse_text ────────────────────────────────────────────────────────────────
|
|
59
|
-
|
|
60
|
-
@pytest.mark.asyncio
|
|
61
|
-
async def test_parse_text_happy_path():
|
|
62
|
-
expected = {"type": "flight", "title": "LH 400 FRA→JFK", "confidence": 0.9}
|
|
63
|
-
mock_response = MagicMock()
|
|
64
|
-
mock_response.json.return_value = {"response": json.dumps(expected)}
|
|
65
|
-
|
|
66
|
-
with patch("httpx.AsyncClient") as mock_cls:
|
|
67
|
-
mock_client = AsyncMock()
|
|
68
|
-
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
|
69
|
-
mock_client.__aexit__ = AsyncMock(return_value=False)
|
|
70
|
-
mock_client.post = AsyncMock(return_value=mock_response)
|
|
71
|
-
mock_cls.return_value = mock_client
|
|
72
|
-
|
|
73
|
-
result, was_cached = await parse_text("Flight LH 400 from Frankfurt to New York")
|
|
74
|
-
|
|
75
|
-
assert result["type"] == "flight"
|
|
76
|
-
assert result["confidence"] == 0.9
|
|
77
|
-
assert was_cached is False
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
@pytest.mark.asyncio
|
|
81
|
-
async def test_parse_text_ollama_returns_garbage():
|
|
82
|
-
mock_response = MagicMock()
|
|
83
|
-
mock_response.json.return_value = {"response": "I cannot parse this."}
|
|
84
|
-
|
|
85
|
-
with patch("httpx.AsyncClient") as mock_cls:
|
|
86
|
-
mock_client = AsyncMock()
|
|
87
|
-
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
|
88
|
-
mock_client.__aexit__ = AsyncMock(return_value=False)
|
|
89
|
-
mock_client.post = AsyncMock(return_value=mock_response)
|
|
90
|
-
mock_cls.return_value = mock_client
|
|
91
|
-
|
|
92
|
-
result, was_cached = await parse_text("some text")
|
|
93
|
-
|
|
94
|
-
assert result["type"] == "other"
|
|
95
|
-
assert result["confidence"] == 0.0
|
|
96
|
-
assert was_cached is False
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@pytest.mark.asyncio
|
|
100
|
-
async def test_parse_text_ollama_empty_response():
|
|
101
|
-
mock_response = MagicMock()
|
|
102
|
-
mock_response.json.return_value = {}
|
|
103
|
-
|
|
104
|
-
with patch("httpx.AsyncClient") as mock_cls:
|
|
105
|
-
mock_client = AsyncMock()
|
|
106
|
-
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
|
107
|
-
mock_client.__aexit__ = AsyncMock(return_value=False)
|
|
108
|
-
mock_client.post = AsyncMock(return_value=mock_response)
|
|
109
|
-
mock_cls.return_value = mock_client
|
|
110
|
-
|
|
111
|
-
result, was_cached = await parse_text("anything")
|
|
112
|
-
|
|
113
|
-
assert isinstance(result, dict)
|
|
114
|
-
assert was_cached is False
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
# ── parse_image ───────────────────────────────────────────────────────────────
|
|
118
|
-
|
|
119
|
-
@pytest.mark.asyncio
|
|
120
|
-
async def test_parse_image_sends_base64():
|
|
121
|
-
payload_sent = {}
|
|
122
|
-
expected = {"type": "hotel", "title": "Marriott Berlin", "confidence": 0.85}
|
|
123
|
-
|
|
124
|
-
async def fake_post(url, json=None, **kwargs):
|
|
125
|
-
payload_sent.update(json or {})
|
|
126
|
-
mock_resp = MagicMock()
|
|
127
|
-
mock_resp.json.return_value = {"response": json_str(expected)}
|
|
128
|
-
return mock_resp
|
|
129
|
-
|
|
130
|
-
def json_str(d): return __import__("json").dumps(d)
|
|
131
|
-
|
|
132
|
-
with patch("httpx.AsyncClient") as mock_cls:
|
|
133
|
-
mock_client = AsyncMock()
|
|
134
|
-
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
|
135
|
-
mock_client.__aexit__ = AsyncMock(return_value=False)
|
|
136
|
-
mock_client.post = AsyncMock(side_effect=fake_post)
|
|
137
|
-
mock_cls.return_value = mock_client
|
|
138
|
-
|
|
139
|
-
result, raw, was_cached = await parse_image(b"\xff\xd8\xff", "image/jpeg")
|
|
140
|
-
|
|
141
|
-
assert "images" in payload_sent
|
|
142
|
-
assert len(payload_sent["images"]) == 1
|
|
143
|
-
assert isinstance(payload_sent["images"][0], str)
|
|
144
|
-
assert result["type"] == "hotel"
|
|
145
|
-
assert was_cached is False
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tests for POST /parse/url endpoint.
|
|
3
|
-
httpx is mocked — no real network calls.
|
|
4
|
-
"""
|
|
5
|
-
import json
|
|
6
|
-
import pytest
|
|
7
|
-
from unittest.mock import AsyncMock, patch, MagicMock
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
FAKE_HTML = """
|
|
11
|
-
<html><head><title>Booking Confirmation</title></head>
|
|
12
|
-
<body>
|
|
13
|
-
<h1>Booking Confirmed</h1>
|
|
14
|
-
<p>Flight LH 400 Frankfurt to New York</p>
|
|
15
|
-
<p>Booking Reference: XYZ789</p>
|
|
16
|
-
<p>Date: 2025-08-15</p>
|
|
17
|
-
</body></html>
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@pytest.fixture
|
|
22
|
-
def mock_ollama_parse():
|
|
23
|
-
with patch("app.services.ollama.parse_text", new_callable=AsyncMock) as m:
|
|
24
|
-
m.return_value = (
|
|
25
|
-
{
|
|
26
|
-
"type": "flight",
|
|
27
|
-
"title": "LH 400 Frankfurt → New York",
|
|
28
|
-
"booking_ref": "XYZ789",
|
|
29
|
-
"provider": "Lufthansa",
|
|
30
|
-
"event_at": "2025-08-15T10:30:00",
|
|
31
|
-
"confidence": 0.88,
|
|
32
|
-
},
|
|
33
|
-
False,
|
|
34
|
-
)
|
|
35
|
-
yield m
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@pytest.fixture
|
|
39
|
-
def mock_http_response():
|
|
40
|
-
mock_resp = MagicMock()
|
|
41
|
-
mock_resp.headers = {"content-type": "text/html; charset=utf-8"}
|
|
42
|
-
mock_resp.text = FAKE_HTML
|
|
43
|
-
mock_resp.raise_for_status = MagicMock()
|
|
44
|
-
return mock_resp
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@pytest.mark.asyncio
|
|
48
|
-
async def test_parse_url_to_inbox(client, mock_ollama_parse, mock_http_response):
|
|
49
|
-
with patch("httpx.AsyncClient") as mock_client_cls:
|
|
50
|
-
mock_client_instance = AsyncMock()
|
|
51
|
-
mock_client_instance.get = AsyncMock(return_value=mock_http_response)
|
|
52
|
-
mock_client_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client_instance)
|
|
53
|
-
mock_client_cls.return_value.__aexit__ = AsyncMock(return_value=False)
|
|
54
|
-
|
|
55
|
-
resp = await client.post("/api/v1/parse/url", json={"url": "https://example.com/booking"})
|
|
56
|
-
|
|
57
|
-
assert resp.status_code == 200
|
|
58
|
-
data = resp.json()
|
|
59
|
-
assert data["parsed"]["type"] == "flight"
|
|
60
|
-
assert data["parsed"]["booking_ref"] == "XYZ789"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@pytest.mark.asyncio
|
|
64
|
-
async def test_parse_url_to_trip(client, mock_ollama_parse, mock_http_response):
|
|
65
|
-
# Create a trip first
|
|
66
|
-
trip_resp = await client.post("/api/v1/trips", json={"name": "Test Trip"})
|
|
67
|
-
assert trip_resp.status_code == 201
|
|
68
|
-
trip_id = trip_resp.json()["id"]
|
|
69
|
-
|
|
70
|
-
with patch("httpx.AsyncClient") as mock_client_cls:
|
|
71
|
-
mock_client_instance = AsyncMock()
|
|
72
|
-
mock_client_instance.get = AsyncMock(return_value=mock_http_response)
|
|
73
|
-
mock_client_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client_instance)
|
|
74
|
-
mock_client_cls.return_value.__aexit__ = AsyncMock(return_value=False)
|
|
75
|
-
|
|
76
|
-
resp = await client.post(
|
|
77
|
-
"/api/v1/parse/url",
|
|
78
|
-
json={"url": "https://example.com/booking", "trip_id": trip_id},
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
assert resp.status_code == 200
|
|
82
|
-
data = resp.json()
|
|
83
|
-
assert data["parsed"]["type"] == "flight"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
@pytest.mark.asyncio
|
|
87
|
-
async def test_parse_url_unreachable_returns_422(client):
|
|
88
|
-
import httpx
|
|
89
|
-
with patch("httpx.AsyncClient") as mock_client_cls:
|
|
90
|
-
mock_client_instance = AsyncMock()
|
|
91
|
-
mock_client_instance.get = AsyncMock(side_effect=httpx.ConnectError("refused"))
|
|
92
|
-
mock_client_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client_instance)
|
|
93
|
-
mock_client_cls.return_value.__aexit__ = AsyncMock(return_value=False)
|
|
94
|
-
|
|
95
|
-
resp = await client.post("/api/v1/parse/url", json={"url": "https://unreachable.invalid"})
|
|
96
|
-
|
|
97
|
-
assert resp.status_code == 422
|
|
98
|
-
assert "Could not fetch URL" in resp.json()["detail"]
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Unit tests for app/services/parser.py
|
|
3
|
-
|
|
4
|
-
Covers:
|
|
5
|
-
extract_text_from_pdf – real minimal PDF bytes, multi-page join
|
|
6
|
-
is_image / is_pdf / is_text – mime type routing guards
|
|
7
|
-
|
|
8
|
-
Run: pytest tests/test_parser.py -v
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import pytest
|
|
12
|
-
from app.services.parser import extract_text_from_pdf, is_image, is_pdf, is_text
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
# ── MIME type guards ───────────────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
def test_is_pdf():
|
|
18
|
-
assert is_pdf("application/pdf") is True
|
|
19
|
-
assert is_pdf("application/octet-stream") is False
|
|
20
|
-
assert is_pdf("image/png") is False
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def test_is_image():
|
|
24
|
-
assert is_image("image/jpeg") is True
|
|
25
|
-
assert is_image("image/png") is True
|
|
26
|
-
assert is_image("image/webp") is True
|
|
27
|
-
assert is_image("application/pdf") is False
|
|
28
|
-
assert is_image("text/plain") is False
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def test_is_text():
|
|
32
|
-
assert is_text("text/plain") is True
|
|
33
|
-
assert is_text("text/html") is True
|
|
34
|
-
assert is_text("text/csv") is True
|
|
35
|
-
assert is_text("application/pdf") is False
|
|
36
|
-
assert is_text("image/jpeg") is False
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# ── PDF extraction ─────────────────────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
def _minimal_pdf(text: str) -> bytes:
|
|
42
|
-
"""Creates a minimal valid 1-page PDF containing `text`."""
|
|
43
|
-
import fitz
|
|
44
|
-
doc = fitz.open()
|
|
45
|
-
page = doc.new_page()
|
|
46
|
-
page.insert_text((72, 72), text)
|
|
47
|
-
return doc.tobytes()
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def test_extract_text_from_pdf_single_page():
|
|
51
|
-
content = _minimal_pdf("Booking confirmation LH400")
|
|
52
|
-
result = extract_text_from_pdf(content)
|
|
53
|
-
assert "LH400" in result
|
|
54
|
-
assert "Booking" in result
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def test_extract_text_from_pdf_strips_whitespace():
|
|
58
|
-
content = _minimal_pdf(" some text ")
|
|
59
|
-
result = extract_text_from_pdf(content)
|
|
60
|
-
assert result == result.strip()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def test_extract_text_from_pdf_multi_page():
|
|
64
|
-
import fitz
|
|
65
|
-
doc = fitz.open()
|
|
66
|
-
doc.new_page().insert_text((72, 72), "Page one content")
|
|
67
|
-
doc.new_page().insert_text((72, 72), "Page two content")
|
|
68
|
-
content = doc.tobytes()
|
|
69
|
-
|
|
70
|
-
result = extract_text_from_pdf(content)
|
|
71
|
-
assert "Page one" in result
|
|
72
|
-
assert "Page two" in result
|