harden: per-item robustness + deadlines/caps across connectors; layered tests
Mail: _safe_decode + _iter_messages (per-mail try/except) -> eine kaputte Mail (charset x-unknown) bricht die Suche nicht mehr ab, findet so auch uralte Mails. Vollscan + Datum-Sort + MAX_SCAN/DEADLINE_S (Selbstheilung, kein 24h-Haenger mehr). Files: search_files mit SEARCH_DEADLINE_S + SEARCH_MAX_DIRS begrenzt (war unbegrenzte rekursive PROPFIND -> real 35s/449 Calls). Calendar/Contacts: PARSE_DEADLINE_S fuer client-seitiges vobject-Parsen; MAX_RESULTS-Output-Cap in get_events/get_tasks (war 10939 Zeilen bei 6000 Events); search Top-N + Hinweise. Tests: in Schichten getrennt -> Smoke (nightly), Edge + Stress (on-demand). Neu: test_smoke/test_edge/test_stress + conftest-Marker + run_full_tests.sh, nightly laeuft nur Smoke. Kontakt-Foto-Roundtrip-Test, Mail-Edge-Mails (x-unknown, kaputter Header, 2009er Alt-Mail). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
936ebc2f56
commit
c9ccb4392b
@@ -105,3 +105,63 @@ Gemeinsames Modul `pdfutil.py` wird von Files-MCP (`read_file`) UND Mail-MCP
|
||||
(`read_attachment`) genutzt -> Scan-PDF-Mailanhaenge werden genauso gerendert.
|
||||
Produktiv-Feature (alle User). Test: `TestFileTypes` scanned.pdf -> `image`.
|
||||
Runtime-Deps: `pymupdf`, `pytesseract` + System `tesseract-ocr`/`-deu` (s. `requirements-extra.txt`).
|
||||
|
||||
## Test-Schichten (2026-06-24)
|
||||
|
||||
Drei Schichten, NUR Smoke laeuft naechtlich:
|
||||
|
||||
- **Smoke** (`test_smoke.py`, Marker `smoke`): NIGHTLY via `mcp-tests.timer` (05:00) ->
|
||||
`run_tests.sh` -> `pytest test_smoke.py`. Pro Connector: Server up + OAuth + tools/list +
|
||||
EIN read-only Call. Schnell (~0.5s), schreibt keine Testdaten. Faengt Totalausfaelle ab.
|
||||
- **Edge** (`test_edge.py`, Marker `edge`): ON-DEMAND. Boese/ungewoehnliche Eingaben pro
|
||||
Connector: Unicode/Emoji-Roundtrips, Sonderzeichen, riesige Limits/Bodies, kaputte
|
||||
Datumsangaben, **Path-Traversal-Block** (Files). Muss graceful sein, nichts leaken/crashen.
|
||||
- **Stress** (`test_stress.py`, Marker `stress`): ON-DEMAND. 40-60 gleichzeitige Requests
|
||||
pro Connector + Mischlast ueber alle 5; danach Responsiveness-Check. Faengt den Klassiker
|
||||
"single-threaded Server haengt unter Last" (genau der Mail-Haenger 2026-06-23).
|
||||
- **Funktional** (`test_all.py`): CRUD + OAuth + Datei-Typen/-Edge. ON-DEMAND.
|
||||
|
||||
Helper (`SERVERS`, `get_token`, `tool_call`, `mcp_call`) liegen in `test_all.py`; smoke/edge/
|
||||
stress importieren sie. Marker in `conftest.py`.
|
||||
|
||||
**Runner:**
|
||||
- Nightly (Smoke): automatisch via Timer, ODER `run_tests.sh`.
|
||||
- On-demand (Funktional+Edge+Stress): `run_full_tests.sh` (Log `/var/log/mcp-tests-full.log`),
|
||||
oder eine Schicht: `run_full_tests.sh -m stress` (bzw. `-m edge`).
|
||||
- Log `/var/log/mcp-tests.log` muss `stefan:stefan` gehoeren (Service laeuft als User=stefan).
|
||||
|
||||
## Mail-Server-Haertung (2026-06-23/24)
|
||||
|
||||
`mcp-mail` hing 24h an einem Such-`CallToolRequest` (single-threaded -> Connector tot).
|
||||
Ursache: eine kaputte Mail (charset `x-unknown` -> `LookupError`) plus fehlende per-Mail-
|
||||
Fehlerbehandlung. Fix in BEIDEN Servern (`/opt/mcp-servers/mail/server.py` remote,
|
||||
`/opt/mcp-mail/server.py` lokal fuer Claude Code):
|
||||
- `_safe_decode` (unbekannte Charsets), `_iter_messages` (per-Mail try/except statt
|
||||
`md.items()`), per-Treffer try/except -> eine kaputte Mail killt die Suche NICHT mehr
|
||||
(findet so auch uralte Mails nach der kaputten).
|
||||
- Vollscan + Datum-Sortierung (neueste zuerst) statt frueher Abbruch bei `limit`.
|
||||
- `MAX_SCAN=2000` + `DEADLINE_S=120` Wall-Clock -> Server heilt sich selbst, nie wieder
|
||||
Endlos-Haenger. Volltext-Scan ueber die echten 3.3GB dauert ~60-70s fuer seltene Begriffe;
|
||||
per `account`/`folder` eingrenzen ist deutlich schneller.
|
||||
|
||||
## Connector-Skalierung (Stand 2026-06-24, gegen ECHTE Daten gemessen)
|
||||
|
||||
Frage: verkraften die Connector "viel durchwuehlen" wie der Mail-Vollscan?
|
||||
- **Notes:** unkritisch. Joplin server-seitige FTS (`/search`), paginiert mit Cap (max 50
|
||||
Seiten), Timeouts. Alle Ops <0.1s auch bei vielen Notizen.
|
||||
- **Files:** `list_files`/`read_file` ok (1 PROPFIND bzw. 25MB-Cap, Timeouts). ABER
|
||||
`search_files` war riskant: client-seitige Rekursion = **1 PROPFIND pro Ordner**, kein
|
||||
Deadline. Real gemessen: Miss-Suche 35s/449 PROPFINDs, wachsend mit dem Baum -> haette
|
||||
den single-threaded Server blockiert. **Fix:** `SEARCH_DEADLINE_S=20` + `SEARCH_MAX_DIRS=400`
|
||||
+ Truncation-Hinweis ("Begriff praezisieren / 'path' enger"). Worst-Case jetzt ~20s gekappt.
|
||||
Langfristig besser: oCIS server-seitige Suche (Graph/REPORT) statt Client-Rekursion.
|
||||
- **Calendar (Radicale):** `get_events` ist server-seitig zeitgefiltert (gut). ABER bei
|
||||
~6000 Test-Events gemessen: `search_events` 6-8s (vobject parst ALLE Events im ~15-Mon-
|
||||
Fenster client-seitig), `get_events` lieferte **10939 Zeilen** (KEIN Result-Limit -> Token-
|
||||
Explosion). Real schon 5s bei 29 Kalendern. **Fix:** `_parse` mit `PARSE_DEADLINE_S=12` +
|
||||
`get_events`/`get_tasks` Output-Cap `MAX_RESULTS=300` + Hinweise; `search_events` Top-30 +
|
||||
Deadline. Danach get_events 1501 statt 10939 Zeilen.
|
||||
- **Contacts (Radicale):** `search_contacts` parst ALLE vCards client-seitig (real 0.2s, wenige
|
||||
Kontakte; skaliert linear). **Fix:** `PARSE_DEADLINE_S=12` in `_get_contacts` + Hinweis.
|
||||
- Rest-Kosten = CalDAV/CardDAV-REPORT-Transfer selbst (kein Server-Result-Limit im Protokoll);
|
||||
in der Praxis durch Datumsbereich/Begriff eingrenzbar. Langfristig: server-seitige Suche.
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Pytest-Konfiguration: Marker fuer die MCP-Test-Schichten.
|
||||
|
||||
- smoke : schnelle nightly Tests (Server up + OAuth + 1 Read pro Connector)
|
||||
- edge : Edge-Case-Tests (on-demand, NICHT naechtlich)
|
||||
- stress : Last-/Concurrency-Tests (on-demand, NICHT naechtlich)
|
||||
"""
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line("markers", "smoke: schnelle nightly Smoke-Tests")
|
||||
config.addinivalue_line("markers", "edge: Edge-Case-Tests (on-demand)")
|
||||
config.addinivalue_line("markers", "stress: Last-/Concurrency-Tests (on-demand)")
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# MCP Voll-Tests — ON-DEMAND (NICHT naechtlich; manuell ausfuehren).
|
||||
# Funktional (test_all.py) + Edge-Cases (test_edge.py) + Stress/Concurrency (test_stress.py).
|
||||
# Schreibt Testdaten in die mcptest-Sandbox (calendar-test, contacts-test, /.mcp-tests*, Inbox).
|
||||
#
|
||||
# Nutzung:
|
||||
# /opt/mcp-servers/tests/run_full_tests.sh # alles
|
||||
# /opt/mcp-servers/tests/run_full_tests.sh -m stress # nur eine Schicht (pytest-Marker)
|
||||
set -uo pipefail
|
||||
|
||||
LOG="/var/log/mcp-tests-full.log"
|
||||
VENV="/opt/mcp-servers/venv/bin"
|
||||
cd /opt/mcp-servers/tests || exit 1
|
||||
|
||||
echo "[$(date)] MCP Voll-Tests gestartet" | tee -a "$LOG"
|
||||
|
||||
if [ "$#" -gt 0 ]; then
|
||||
# z.B. -m smoke | -m edge | -m stress (Marker-basiert ueber alle Dateien)
|
||||
OUTPUT=$($VENV/python -m pytest test_all.py test_edge.py test_stress.py "$@" -v --tb=short 2>&1)
|
||||
else
|
||||
OUTPUT=$($VENV/python -m pytest test_all.py test_edge.py test_stress.py -v --tb=short 2>&1)
|
||||
fi
|
||||
EXIT=$?
|
||||
|
||||
echo "$OUTPUT" | tee -a "$LOG"
|
||||
echo "$OUTPUT" | tail -1
|
||||
echo "[$(date)] MCP Voll-Tests fertig (exit $EXIT)" | tee -a "$LOG"
|
||||
exit $EXIT
|
||||
+8
-6
@@ -1,25 +1,27 @@
|
||||
#!/bin/bash
|
||||
# MCP Server Integration Tests — laeuft taeglich via systemd timer
|
||||
# MCP Smoke-Tests — laeuft taeglich via systemd timer (mcp-tests.timer 05:00).
|
||||
# NUR Smoke (Server up + Auth + 1 Read/Connector). Stress/Edge/Funktional laufen
|
||||
# NICHT naechtlich -> on-demand via run_full_tests.sh.
|
||||
set -uo pipefail
|
||||
|
||||
LOG="/var/log/mcp-tests.log"
|
||||
NTFY_TOPIC="admin"
|
||||
VENV="/opt/mcp-servers/venv/bin"
|
||||
|
||||
echo "[$(date)] MCP Tests gestartet" | tee -a "$LOG"
|
||||
echo "[$(date)] MCP Smoke-Tests gestartet" | tee -a "$LOG"
|
||||
|
||||
OUTPUT=$($VENV/python -m pytest /opt/mcp-servers/tests/test_all.py -v --tb=short 2>&1)
|
||||
OUTPUT=$(cd /opt/mcp-servers/tests && $VENV/python -m pytest test_smoke.py -v --tb=short 2>&1)
|
||||
EXIT=$?
|
||||
|
||||
echo "$OUTPUT" | tee -a "$LOG"
|
||||
|
||||
if [ $EXIT -eq 0 ]; then
|
||||
PASSED=$(echo "$OUTPUT" | grep -oP '\d+ passed' | head -1)
|
||||
echo "[$(date)] MCP Tests OK: $PASSED" | tee -a "$LOG"
|
||||
echo "[$(date)] MCP Smoke-Tests OK: $PASSED" | tee -a "$LOG"
|
||||
else
|
||||
FAILED=$(echo "$OUTPUT" | grep "FAILED" | head -5)
|
||||
echo "[$(date)] MCP Tests FEHLGESCHLAGEN" | tee -a "$LOG"
|
||||
/usr/local/bin/notify-ntfy "$NTFY_TOPIC" "MCP Tests fehlgeschlagen" \
|
||||
echo "[$(date)] MCP Smoke-Tests FEHLGESCHLAGEN" | tee -a "$LOG"
|
||||
/usr/local/bin/notify-ntfy "$NTFY_TOPIC" "MCP Smoke-Tests fehlgeschlagen" \
|
||||
"$(echo "$FAILED" | head -3)\n\nLog: tail -50 $LOG" "urgent" "x,test_tube"
|
||||
fi
|
||||
|
||||
|
||||
@@ -148,6 +148,33 @@ class TestMail:
|
||||
{"query": "xyzzy_nonexistent_99", "limit": 1, "account": "mcp-test-empty"})
|
||||
assert "No results" in text
|
||||
|
||||
def test_search_finds_old_mail(self):
|
||||
# Uralte Mail (2009) muss gefunden werden -> vollstaendiger Scan, kein Crash davor
|
||||
text = tool_call(self.PORT, get_token(self.PORT), "search_mail",
|
||||
{"query": "URALTMAIL2009", "limit": 5, "account": "mcp-test-mail"})
|
||||
assert "No results" not in text
|
||||
assert "2009" in text
|
||||
|
||||
def test_search_survives_bad_charset(self):
|
||||
# Mail mit charset x-unknown darf die Suche NICHT killen und muss findbar sein
|
||||
text = tool_call(self.PORT, get_token(self.PORT), "search_mail",
|
||||
{"query": "XUNKNOWNTOKEN", "limit": 5, "account": "mcp-test-mail"})
|
||||
assert "No results" not in text
|
||||
assert "Subject:" in text
|
||||
|
||||
def test_search_survives_bad_header(self):
|
||||
# Kaputter encoded-word-Header darf decode_hdr nicht crashen
|
||||
text = tool_call(self.PORT, get_token(self.PORT), "search_mail",
|
||||
{"query": "BADHEADERTOKEN", "limit": 5, "account": "mcp-test-mail"})
|
||||
assert "No results" not in text
|
||||
|
||||
def test_search_broad_no_crash(self):
|
||||
# Breite Suche scannt ueber die Edge-Mails hinweg ohne Fehler
|
||||
text = tool_call(self.PORT, get_token(self.PORT), "search_mail",
|
||||
{"query": "mcptest", "limit": 50, "account": "mcp-test-mail"})
|
||||
assert "Error" not in text
|
||||
assert text.count("Subject:") >= 2
|
||||
|
||||
def test_read_mail(self):
|
||||
# First search to get a key
|
||||
text = tool_call(self.PORT, get_token(self.PORT), "search_mail",
|
||||
@@ -298,6 +325,25 @@ class TestContacts:
|
||||
assert "Mustermann" in text
|
||||
assert "UID:" in text
|
||||
|
||||
def test_set_and_read_photo(self):
|
||||
# Foto setzen + zuruecklesen (get_contact liefert dann ImageContent)
|
||||
token = get_token(self.PORT)
|
||||
tag = f"PhotoTest{int(time.time())}"
|
||||
tool_call(self.PORT, token, "create_contact", {"name": f"{tag} Bildmann", "email": f"{tag.lower()}@example.com"})
|
||||
search = tool_call(self.PORT, token, "search_contacts", {"query": tag, "limit": 1})
|
||||
uid = [l for l in search.split("\n") if "UID:" in l][0].split("UID:")[1].strip()
|
||||
# 1x1 PNG
|
||||
png_b64 = ("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwC"
|
||||
"AAAAC0lEQVR42mNk+M8AAAMCAYAAAS+ZkAAAAABJRU5ErkJggg==")
|
||||
res = tool_call(self.PORT, token, "set_contact_photo",
|
||||
{"uid": uid, "image_base64": png_b64, "image_type": "png"})
|
||||
assert "esetzt" in res.lower() or "set" in res.lower()
|
||||
content = mcp_call(self.PORT, token, "tools/call",
|
||||
{"name": "get_contact", "arguments": {"uid": uid}})["result"]["content"]
|
||||
imgs = [c for c in content if c.get("type") == "image"]
|
||||
assert imgs, f"Kein Bild in get_contact: {[c.get('type') for c in content]}"
|
||||
assert imgs[0].get("mimeType", "").startswith("image/")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Files Tests (CRUD on /mcp-tests/)
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
"""MCP Edge-Case-Tests — ON-DEMAND (nicht naechtlich).
|
||||
|
||||
Pro Connector: ungewoehnliche/boese Eingaben (Unicode/Emoji, Sonderzeichen,
|
||||
riesige Limits/Bodies, kaputte Datumsangaben, Path-Traversal). Ziel: der Server
|
||||
faengt das sauber ab (graceful) statt zu crashen oder Daten zu leaken.
|
||||
|
||||
Lauf: /opt/mcp-servers/venv/bin/python -m pytest test_edge.py -q
|
||||
"""
|
||||
|
||||
import time
|
||||
import pytest
|
||||
|
||||
from test_all import SERVERS, get_token, mcp_call, tool_call
|
||||
|
||||
pytestmark = pytest.mark.edge
|
||||
|
||||
EMOJI = "Zürdäß 🚀😀 漢字 مرحبا"
|
||||
|
||||
|
||||
def _alive(port):
|
||||
"""Server beantwortet nach einer boesen Eingabe noch normal."""
|
||||
res = mcp_call(port, get_token(port), "tools/list")
|
||||
assert res and res.get("result", {}).get("tools"), f"Server {port} nach Edge-Call tot"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Mail
|
||||
# ============================================================
|
||||
|
||||
class TestMailEdge:
|
||||
PORT = SERVERS["mail"]
|
||||
|
||||
def test_huge_limit(self):
|
||||
tool_call(self.PORT, get_token(self.PORT), "search_mail",
|
||||
{"query": "mcptest", "limit": 100000, "account": "mcp-test-mail"})
|
||||
_alive(self.PORT)
|
||||
|
||||
def test_regex_special_chars_literal(self):
|
||||
for q in [".*", "[a-z]+", "%27 OR 1=1", "a\\b", "()|"]:
|
||||
tool_call(self.PORT, get_token(self.PORT), "search_mail",
|
||||
{"query": q, "limit": 3, "account": "mcp-test-mail"})
|
||||
_alive(self.PORT)
|
||||
|
||||
def test_very_long_query(self):
|
||||
tool_call(self.PORT, get_token(self.PORT), "search_mail",
|
||||
{"query": "x" * 2000, "limit": 3, "account": "mcp-test-mail"})
|
||||
_alive(self.PORT)
|
||||
|
||||
def test_nonexistent_account_filter(self):
|
||||
text = tool_call(self.PORT, get_token(self.PORT), "search_mail",
|
||||
{"query": "mcptest", "account": "does-not-exist-xyz"})
|
||||
assert "No results" in text or text.strip() == "" or "Subject:" not in text
|
||||
_alive(self.PORT)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Calendar
|
||||
# ============================================================
|
||||
|
||||
class TestCalendarEdge:
|
||||
PORT = SERVERS["calendar"]
|
||||
CAL = "calendar-test"
|
||||
|
||||
def test_invalid_date_graceful(self):
|
||||
# kaputtes Datum darf den Server nicht killen
|
||||
mcp_call(self.PORT, get_token(self.PORT), "tools/call", {
|
||||
"name": "create_event",
|
||||
"arguments": {"calendar": self.CAL, "title": "BadDate", "start": "kein-datum", "end": "32.13.2026"},
|
||||
})
|
||||
_alive(self.PORT)
|
||||
|
||||
def test_unicode_emoji_event(self):
|
||||
token = get_token(self.PORT)
|
||||
tag = f"edge-uni-{int(time.time())}"
|
||||
tool_call(self.PORT, token, "create_event", {
|
||||
"calendar": self.CAL, "title": f"{EMOJI} {tag}",
|
||||
"start": "2027-01-02T10:00", "end": "2027-01-02T11:00",
|
||||
})
|
||||
found = tool_call(self.PORT, token, "search_events", {"query": tag})
|
||||
assert tag in found
|
||||
|
||||
def test_reversed_date_range(self):
|
||||
tool_call(self.PORT, get_token(self.PORT), "get_events",
|
||||
{"calendar": self.CAL, "date_from": "2027-12-31", "date_to": "2027-01-01"})
|
||||
_alive(self.PORT)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Contacts
|
||||
# ============================================================
|
||||
|
||||
class TestContactsEdge:
|
||||
PORT = SERVERS["contacts"]
|
||||
|
||||
def test_unicode_emoji_contact(self):
|
||||
token = get_token(self.PORT)
|
||||
tag = f"EdgeUni{int(time.time())}"
|
||||
tool_call(self.PORT, token, "create_contact",
|
||||
{"name": f"{tag} {EMOJI}", "email": f"{tag.lower()}@example.com"})
|
||||
found = tool_call(self.PORT, token, "search_contacts", {"query": tag})
|
||||
assert tag in found
|
||||
|
||||
def test_special_char_search(self):
|
||||
for q in ["O'Brien", "müller & söhne", "100% sicher", "<script>"]:
|
||||
tool_call(self.PORT, get_token(self.PORT), "search_contacts", {"query": q})
|
||||
_alive(self.PORT)
|
||||
|
||||
def test_very_long_name(self):
|
||||
token = get_token(self.PORT)
|
||||
tag = f"EdgeLong{int(time.time())}"
|
||||
tool_call(self.PORT, token, "create_contact",
|
||||
{"name": f"{tag} " + ("Lang" * 200), "email": f"{tag.lower()}@example.com"})
|
||||
_alive(self.PORT)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Files
|
||||
# ============================================================
|
||||
|
||||
class TestFilesEdge:
|
||||
PORT = SERVERS["files"]
|
||||
DIR = "/.mcp-tests-edge"
|
||||
|
||||
@pytest.fixture(autouse=True, scope="class")
|
||||
def _dir(self):
|
||||
token = get_token(self.PORT)
|
||||
tool_call(self.PORT, token, "create_folder", {"path": self.DIR})
|
||||
yield
|
||||
tool_call(self.PORT, token, "delete_file", {"path": self.DIR})
|
||||
|
||||
def test_path_traversal_blocked(self):
|
||||
# darf NIEMALS /etc/passwd ausliefern
|
||||
for p in ["/../../../../etc/passwd", "/.mcp-tests-edge/../../../etc/passwd"]:
|
||||
res = mcp_call(self.PORT, get_token(self.PORT), "tools/call",
|
||||
{"name": "read_file", "arguments": {"path": p}})
|
||||
text = ""
|
||||
if res and "result" in res:
|
||||
text = res["result"]["content"][0].get("text", "")
|
||||
assert "root:x:0:0" not in text, f"Path-Traversal LEAK bei {p}"
|
||||
_alive(self.PORT)
|
||||
|
||||
def test_unicode_emoji_roundtrip(self):
|
||||
token = get_token(self.PORT)
|
||||
path = f"{self.DIR}/edge-{int(time.time())}-{EMOJI[:4]}.txt"
|
||||
tool_call(self.PORT, token, "write_file", {"path": path, "content": EMOJI})
|
||||
read = mcp_call(self.PORT, token, "tools/call", {"name": "read_file", "arguments": {"path": path}})
|
||||
assert "🚀" in read["result"]["content"][0]["text"]
|
||||
tool_call(self.PORT, token, "delete_file", {"path": path})
|
||||
|
||||
def test_nonexistent_deep_path(self):
|
||||
text = tool_call(self.PORT, get_token(self.PORT), "file_info",
|
||||
{"path": "/.mcp-tests-edge/nope/nope/nope.txt"})
|
||||
assert "404" in text or "gefunden" in text.lower() or "not found" in text.lower()
|
||||
_alive(self.PORT)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Notes
|
||||
# ============================================================
|
||||
|
||||
class TestNotesEdge:
|
||||
PORT = SERVERS["notes"]
|
||||
NB = "Inbox"
|
||||
|
||||
def test_unicode_emoji_markdown_note(self):
|
||||
token = get_token(self.PORT)
|
||||
tag = f"edge-uni-{int(time.time())}"
|
||||
body = f"# {EMOJI} {tag}\n\n- [ ] Aufgabe\n```py\nprint('x')\n```\n| a | b |\n|---|---|\n"
|
||||
result = tool_call(self.PORT, token, "create_note", {"notebook": self.NB, "title": f"Edge {tag}", "body": body})
|
||||
note_id = result.split("id: ")[1].rstrip(")").strip()
|
||||
read = tool_call(self.PORT, token, "read_note", {"note_id": note_id})
|
||||
assert tag in read and "🚀" in read
|
||||
|
||||
def test_large_body(self):
|
||||
token = get_token(self.PORT)
|
||||
tag = f"edge-big-{int(time.time())}"
|
||||
big = (f"Zeile {tag} " + "lorem ipsum " * 8 + "\n") * 800 # ~ hunderte KB
|
||||
result = tool_call(self.PORT, token, "create_note", {"notebook": self.NB, "title": f"Big {tag}", "body": big})
|
||||
note_id = result.split("id: ")[1].rstrip(")").strip()
|
||||
read = tool_call(self.PORT, token, "read_note", {"note_id": note_id})
|
||||
assert tag in read
|
||||
_alive(self.PORT)
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""MCP Smoke-Tests — NIGHTLY.
|
||||
|
||||
Minimal, schnell, read-only. Pro Connector: Server erreichbar, OAuth ok,
|
||||
tools/list ok, ein billiger Read-Call. Faengt Totalausfaelle (Server down,
|
||||
Auth kaputt, Backend weg) ueber Nacht ab, ohne Last zu erzeugen oder Testdaten
|
||||
zu schreiben.
|
||||
|
||||
Lauf: /opt/mcp-servers/venv/bin/python -m pytest test_smoke.py -q
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from test_all import SERVERS, get_token, mcp_call, tool_call
|
||||
|
||||
pytestmark = pytest.mark.smoke
|
||||
|
||||
|
||||
@pytest.mark.parametrize("svc,port", list(SERVERS.items()))
|
||||
def test_server_up_auth_tools(svc, port):
|
||||
"""Server erreichbar + OAuth (client_credentials) + tools/list liefert Tools."""
|
||||
token = get_token(port)
|
||||
assert token, f"{svc}: kein Token"
|
||||
res = mcp_call(port, token, "tools/list")
|
||||
assert res and res.get("result", {}).get("tools"), f"{svc}: tools/list leer"
|
||||
|
||||
|
||||
# --- ein billiger Read-Call pro Connector (read-only, kein Schreiben) ---
|
||||
|
||||
def test_mail_read():
|
||||
assert "mcp-test-mail" in tool_call(SERVERS["mail"], get_token(SERVERS["mail"]), "list_accounts")
|
||||
|
||||
|
||||
def test_calendar_read():
|
||||
text = tool_call(SERVERS["calendar"], get_token(SERVERS["calendar"]), "list_calendars")
|
||||
assert "calendar-test" in text or "MCP Test" in text
|
||||
|
||||
|
||||
def test_contacts_read():
|
||||
# read-only Suche; Treffer egal, Hauptsache der Call kommt sauber durch
|
||||
text = tool_call(SERVERS["contacts"], get_token(SERVERS["contacts"]),
|
||||
"search_contacts", {"query": "Mustermann", "limit": 1})
|
||||
assert isinstance(text, str) and len(text) >= 0
|
||||
|
||||
|
||||
def test_files_read():
|
||||
text = tool_call(SERVERS["files"], get_token(SERVERS["files"]), "list_files", {"path": "/"})
|
||||
assert "DIR" in text or "/" in text
|
||||
|
||||
|
||||
def test_notes_read():
|
||||
text = tool_call(SERVERS["notes"], get_token(SERVERS["notes"]), "list_notebooks")
|
||||
assert "id:" in text
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""MCP Stress-/Concurrency-Tests — ON-DEMAND (nicht naechtlich).
|
||||
|
||||
Feuert viele gleichzeitige Requests pro Connector und prueft: alle kommen sauber
|
||||
durch (kein Deadlock/Haenger) und der Server ist danach sofort wieder responsiv.
|
||||
Genau diese Art Test haette den Mail-Server-Haenger (single-threaded, 24h blockiert)
|
||||
gefangen.
|
||||
|
||||
Lauf: /opt/mcp-servers/venv/bin/python -m pytest test_stress.py -q
|
||||
"""
|
||||
|
||||
import time
|
||||
import random
|
||||
import concurrent.futures as cf
|
||||
|
||||
import pytest
|
||||
|
||||
from test_all import SERVERS, get_token, tool_call
|
||||
|
||||
pytestmark = pytest.mark.stress
|
||||
|
||||
N = 40 # gleichzeitige Requests pro Welle
|
||||
WORKERS = 16 # parallele Clients
|
||||
RESPONSIVE_S = 15 # Server muss nach der Last so schnell wieder antworten
|
||||
|
||||
|
||||
def _hammer(fn, n=N):
|
||||
"""fn() n-mal nebenlaeufig ausfuehren; gibt Liste der Fehlertexte zurueck."""
|
||||
errors = []
|
||||
with cf.ThreadPoolExecutor(max_workers=WORKERS) as ex:
|
||||
futs = [ex.submit(fn) for _ in range(n)]
|
||||
for f in cf.as_completed(futs):
|
||||
try:
|
||||
f.result()
|
||||
except Exception as e: # noqa: BLE001
|
||||
errors.append(repr(e)[:200])
|
||||
return errors
|
||||
|
||||
|
||||
def _assert_responsive(port, probe):
|
||||
t = time.time()
|
||||
probe()
|
||||
dt = time.time() - t
|
||||
assert dt < RESPONSIVE_S, f"Server {port} nach Last traege: {dt:.1f}s"
|
||||
|
||||
|
||||
class TestStress:
|
||||
|
||||
def test_mail_concurrent_search(self):
|
||||
port = SERVERS["mail"]; token = get_token(port)
|
||||
queries = ["mcptest", "Rechnung", "stefan", "Willkommen",
|
||||
"XUNKNOWNTOKEN", "URALTMAIL2009", "BADHEADERTOKEN", "Newsletter"]
|
||||
errors = _hammer(lambda: tool_call(port, token, "search_mail",
|
||||
{"query": random.choice(queries), "limit": 5, "account": "mcp-test-mail"}))
|
||||
assert not errors, f"{len(errors)} Fehler unter Last: {errors[:3]}"
|
||||
_assert_responsive(port, lambda: tool_call(port, token, "list_accounts"))
|
||||
|
||||
def test_calendar_concurrent_reads(self):
|
||||
port = SERVERS["calendar"]; token = get_token(port)
|
||||
def call():
|
||||
if random.random() < 0.5:
|
||||
tool_call(port, token, "list_calendars")
|
||||
else:
|
||||
tool_call(port, token, "get_events",
|
||||
{"calendar": "calendar-test", "date_from": "2026-01-01", "date_to": "2026-12-31"})
|
||||
errors = _hammer(call)
|
||||
assert not errors, f"{len(errors)} Fehler: {errors[:3]}"
|
||||
_assert_responsive(port, lambda: tool_call(port, token, "list_calendars"))
|
||||
|
||||
def test_contacts_concurrent_search(self):
|
||||
port = SERVERS["contacts"]; token = get_token(port)
|
||||
qs = ["Mustermann", "Mcp", "example", "xyzzy_nobody", "GmbH"]
|
||||
errors = _hammer(lambda: tool_call(port, token, "search_contacts",
|
||||
{"query": random.choice(qs), "limit": 5}))
|
||||
assert not errors, f"{len(errors)} Fehler: {errors[:3]}"
|
||||
_assert_responsive(port, lambda: tool_call(port, token, "search_contacts", {"query": "Mustermann", "limit": 1}))
|
||||
|
||||
def test_files_concurrent_reads(self):
|
||||
port = SERVERS["files"]; token = get_token(port)
|
||||
def call():
|
||||
if random.random() < 0.5:
|
||||
tool_call(port, token, "list_files", {"path": "/"})
|
||||
else:
|
||||
tool_call(port, token, "file_info", {"path": "/"})
|
||||
errors = _hammer(call)
|
||||
assert not errors, f"{len(errors)} Fehler: {errors[:3]}"
|
||||
_assert_responsive(port, lambda: tool_call(port, token, "list_files", {"path": "/"}))
|
||||
|
||||
def test_notes_concurrent_reads(self):
|
||||
port = SERVERS["notes"]; token = get_token(port)
|
||||
def call():
|
||||
if random.random() < 0.5:
|
||||
tool_call(port, token, "list_notebooks")
|
||||
else:
|
||||
tool_call(port, token, "list_notes", {"limit": 5})
|
||||
errors = _hammer(call)
|
||||
assert not errors, f"{len(errors)} Fehler: {errors[:3]}"
|
||||
_assert_responsive(port, lambda: tool_call(port, token, "list_notebooks"))
|
||||
|
||||
def test_all_servers_mixed_load(self):
|
||||
"""Alle 5 Connector gleichzeitig unter Last — Gesamtsystem haelt."""
|
||||
probes = {
|
||||
"mail": lambda p, t: tool_call(p, t, "search_mail", {"query": "mcptest", "limit": 3, "account": "mcp-test-mail"}),
|
||||
"calendar": lambda p, t: tool_call(p, t, "list_calendars"),
|
||||
"contacts": lambda p, t: tool_call(p, t, "search_contacts", {"query": "Mustermann", "limit": 2}),
|
||||
"files": lambda p, t: tool_call(p, t, "list_files", {"path": "/"}),
|
||||
"notes": lambda p, t: tool_call(p, t, "list_notebooks"),
|
||||
}
|
||||
tokens = {svc: get_token(SERVERS[svc]) for svc in probes}
|
||||
def call():
|
||||
svc = random.choice(list(probes))
|
||||
probes[svc](SERVERS[svc], tokens[svc])
|
||||
errors = _hammer(call, n=60)
|
||||
assert not errors, f"{len(errors)} Fehler im Mischbetrieb: {errors[:3]}"
|
||||
Vendored
+27
@@ -46,6 +46,33 @@ add("mcp-test-mail","INBOX", mail_with("voice@example.com","Sprachnachricht","Ku
|
||||
add("mcp-test-mail","INBOX", mail_with("projekt@example.com","Projektdateien","Archiv und Notizen.",["archive.zip","notes.txt","data.csv"]))
|
||||
add("mcp-test-mail","INBOX", mail_with("media@example.com","Praesentation + Clip","Folien und ein kurzes Video.",["slides.pptx","clip.mp4"]))
|
||||
|
||||
# --- Edge-Cases: Robustheit + uralte Mails (Fix 2026-06-24) ---
|
||||
def addraw(acct, folder, raw_bytes):
|
||||
path=os.path.join(BASE,acct,folder); os.makedirs(os.path.dirname(path),exist_ok=True)
|
||||
mailbox.Maildir(path,create=True).add(raw_bytes)
|
||||
|
||||
# Uralte Mail (2009) mit eindeutigem Token -> Suche muss alte Mails finden
|
||||
_old=simple("archiv@example.com","mcptest@local","Uralte Rechnung URALTMAIL2009","Sehr alte Mail aus 2009 zum Testen der Alt-Mail-Suche.")
|
||||
_old.replace_header("Date","Tue, 03 Mar 2009 08:15:00 +0100")
|
||||
add("mcp-test-mail","Archive",_old)
|
||||
|
||||
# Mail mit kaputtem Charset x-unknown -> darf die Suche NICHT killen, Token findbar
|
||||
addraw("mcp-test-mail","INBOX",
|
||||
b"From: weird@example.com\r\nTo: mcptest@local\r\n"
|
||||
b"Subject: Kaputtes Encoding XUNKNOWNTOKEN\r\n"
|
||||
b"Date: Wed, 10 Jan 2018 12:00:00 +0100\r\n"
|
||||
b'Content-Type: text/plain; charset="x-unknown"\r\n'
|
||||
b"Content-Transfer-Encoding: 8bit\r\n\r\n"
|
||||
b"Koerpertext mit kaputtem Charset XUNKNOWNTOKEN und Umlaut \xfc.\r\n")
|
||||
|
||||
# Mail mit kaputtem encoded-word-Header -> decode_hdr darf nicht crashen
|
||||
addraw("mcp-test-mail","INBOX",
|
||||
b"From: =?invalid-charset?Q?Br=F6tchen?= <bad@example.com>\r\nTo: mcptest@local\r\n"
|
||||
b"Subject: =?x-unknown?B?a2FwdXR0?= BADHEADERTOKEN\r\n"
|
||||
b"Date: Fri, 05 Jul 2013 09:00:00 +0200\r\n"
|
||||
b"Content-Type: text/plain; charset=utf-8\r\n\r\n"
|
||||
b"Body BADHEADERTOKEN.\r\n")
|
||||
|
||||
# new -> cur
|
||||
for r,d,f in os.walk(BASE):
|
||||
if r.endswith("new"):
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
From: archiv@example.com
|
||||
To: mcptest@local
|
||||
Subject: Uralte Rechnung URALTMAIL2009
|
||||
Date: Tue, 03 Mar 2009 08:15:00 +0100
|
||||
|
||||
U2VociBhbHRlIE1haWwgYXVzIDIwMDkgenVtIFRlc3RlbiBkZXIgQWx0LU1haWwtU3VjaGUu
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
Content-Type: multipart/mixed; boundary="===============3363393740883118962=="
|
||||
Content-Type: multipart/mixed; boundary="===============2801843982600110124=="
|
||||
MIME-Version: 1.0
|
||||
From: billing@example.com
|
||||
To: mcptest@local
|
||||
Subject: Ihre Rechnung Juni 2026
|
||||
Date: Tue, 17 Jun 2026 10:00:00 +0200
|
||||
|
||||
--===============3363393740883118962==
|
||||
--===============2801843982600110124==
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
@@ -13,7 +13,7 @@ Content-Transfer-Encoding: base64
|
||||
SW0gQW5oYW5nIGRpZSBSZWNobnVuZyAoVGV4dC1QREYpIHVuZCBkZXIgU2NhbiAoQmlsZC1QREYp
|
||||
Lg==
|
||||
|
||||
--===============3363393740883118962==
|
||||
--===============2801843982600110124==
|
||||
Content-Type: application/octet-stream
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
@@ -40,7 +40,7 @@ b290IDIgMCBSCi9JbmZvIDcgMCBSCi9JRCBbPDkzRTNFOTU5Q0U4RUI1RUZFRkRFMjQyNjRCQkIy
|
||||
NzIxPjw5M0UzRTk1OUNFOEVCNUVGRUZERTI0MjY0QkJCMjcyMT5dCj4+CnN0YXJ0eHJlZgo3NzkK
|
||||
JSVFT0YK
|
||||
|
||||
--===============3363393740883118962==
|
||||
--===============2801843982600110124==
|
||||
Content-Type: application/octet-stream
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
@@ -217,4 +217,4 @@ MDAwMCBuIAowMDAwMDA4NzI4IDAwMDAwIG4gCjAwMDAwMDg3NDcgMDAwMDAgbiAKMDAwMDAwODc2
|
||||
OSAwMDAwMCBuIAowMDAwMDA4Nzg4IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgMTgKL0luZm8g
|
||||
MTcgMCBSCi9Sb290IDEgMCBSCj4+CnN0YXJ0eHJlZgo5MjM3CiUlRU9GCg==
|
||||
|
||||
--===============3363393740883118962==--
|
||||
--===============2801843982600110124==--
|
||||
+5
-5
@@ -1,18 +1,18 @@
|
||||
Content-Type: multipart/mixed; boundary="===============4945638286946888290=="
|
||||
Content-Type: multipart/mixed; boundary="===============1039225266335026552=="
|
||||
MIME-Version: 1.0
|
||||
From: foto@example.com
|
||||
To: mcptest@local
|
||||
Subject: Fotos vom Wochenende
|
||||
Date: Tue, 17 Jun 2026 10:00:00 +0200
|
||||
|
||||
--===============4945638286946888290==
|
||||
--===============1039225266335026552==
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
QW5iZWkgendlaSBCaWxkZXIu
|
||||
|
||||
--===============4945638286946888290==
|
||||
--===============1039225266335026552==
|
||||
Content-Type: application/octet-stream
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
@@ -194,7 +194,7 @@ AKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigA
|
||||
ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACi
|
||||
iigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/2Q==
|
||||
|
||||
--===============4945638286946888290==
|
||||
--===============1039225266335026552==
|
||||
Content-Type: application/octet-stream
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
@@ -262,4 +262,4 @@ ABAgwAAQ0O1ULfTCQ1NP1VIA8P+eHTAABAgwAAQIMAAEfKjvgKc8suBU3QcAnFXsgAEgQIABIECA
|
||||
ASBAgAEgQIABIECAASBAgAEgQIABIECAASBAgAEgQIABIECAASBAgAEgQIABIECAASBAgAEgQIAB
|
||||
IECAASDgfwAcngvShI2jGAAAAABJRU5ErkJggg==
|
||||
|
||||
--===============4945638286946888290==--
|
||||
--===============1039225266335026552==--
|
||||
+5
-5
@@ -1,18 +1,18 @@
|
||||
Content-Type: multipart/mixed; boundary="===============8126427565043495650=="
|
||||
Content-Type: multipart/mixed; boundary="===============3148390453173742087=="
|
||||
MIME-Version: 1.0
|
||||
From: buero@example.com
|
||||
To: mcptest@local
|
||||
Subject: Quartalsbericht Q2
|
||||
Date: Tue, 17 Jun 2026 10:00:00 +0200
|
||||
|
||||
--===============8126427565043495650==
|
||||
--===============3148390453173742087==
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
QmVyaWNodCAoV29yZCkgdW5kIFphaGxlbiAoRXhjZWwpIGFuYmVpLg==
|
||||
|
||||
--===============8126427565043495650==
|
||||
--===============3148390453173742087==
|
||||
Content-Type: application/octet-stream
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
@@ -663,7 +663,7 @@ AIkVAAASAAAAAAAAAAAAAACAATmBAAB3b3JkL251bWJlcmluZy54bWxQSwECFAMUAAAACABvPtNc
|
||||
osjWZ70FAACEIAAAFwAAAAAAAAAAAAAAgAHUhAAAZG9jUHJvcHMvdGh1bWJuYWlsLmpwZWdQSwUG
|
||||
AAAAABEAEQBhBAAAxooAAAAA
|
||||
|
||||
--===============8126427565043495650==
|
||||
--===============3148390453173742087==
|
||||
Content-Type: application/octet-stream
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
@@ -756,4 +756,4 @@ AxQAAAAIAG8+01wkHpuirQAAAPgBAAAaAAAAAAAAAAAAAACAAZIOAAB4bC9fcmVscy93b3JrYm9v
|
||||
ay54bWwucmVsc1BLAQIUAxQAAAAIAG8+01xlkHmSGQEAAM8DAAATAAAAAAAAAAAAAACAAXcPAABb
|
||||
Q29udGVudF9UeXBlc10ueG1sUEsFBgAAAAAJAAkAPgIAAMEQAAAAAA==
|
||||
|
||||
--===============8126427565043495650==--
|
||||
--===============3148390453173742087==--
|
||||
+4
-4
@@ -1,18 +1,18 @@
|
||||
Content-Type: multipart/mixed; boundary="===============5981132086698869830=="
|
||||
Content-Type: multipart/mixed; boundary="===============2935331032460717163=="
|
||||
MIME-Version: 1.0
|
||||
From: voice@example.com
|
||||
To: mcptest@local
|
||||
Subject: Sprachnachricht
|
||||
Date: Tue, 17 Jun 2026 10:00:00 +0200
|
||||
|
||||
--===============5981132086698869830==
|
||||
--===============2935331032460717163==
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
S3VyemUgQXVkaW9uYWNocmljaHQgaW0gQW5oYW5nLg==
|
||||
|
||||
--===============5981132086698869830==
|
||||
--===============2935331032460717163==
|
||||
Content-Type: application/octet-stream
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
@@ -175,4 +175,4 @@ VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+xDE94AJeGVd+akgAAAANIOAAARV
|
||||
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
||||
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQ==
|
||||
|
||||
--===============5981132086698869830==--
|
||||
--===============2935331032460717163==--
|
||||
+6
-6
@@ -1,18 +1,18 @@
|
||||
Content-Type: multipart/mixed; boundary="===============1387712317359960679=="
|
||||
Content-Type: multipart/mixed; boundary="===============0315165645846539514=="
|
||||
MIME-Version: 1.0
|
||||
From: projekt@example.com
|
||||
To: mcptest@local
|
||||
Subject: Projektdateien
|
||||
Date: Tue, 17 Jun 2026 10:00:00 +0200
|
||||
|
||||
--===============1387712317359960679==
|
||||
--===============0315165645846539514==
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
QXJjaGl2IHVuZCBOb3RpemVuLg==
|
||||
|
||||
--===============1387712317359960679==
|
||||
--===============0315165645846539514==
|
||||
Content-Type: application/octet-stream
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
@@ -24,7 +24,7 @@ ZGF0YS5jc3ZuYW1lLHdlcnQKQWxwaGEsMQpCZXRhLDIKUEsBAhQDFAAAAAAAbz7TXE28oLEtAAAA
|
||||
LQAAAAkAAAAAAAAAAAAAALCBAAAAAG5vdGVzLnR4dFBLAQIUAxQAAAAAAG8+01wa5IaIGQAAABkA
|
||||
AAAIAAAAAAAAAAAAAACwgVQAAABkYXRhLmNzdlBLBQYAAAAAAgACAG0AAACTAAAAAAA=
|
||||
|
||||
--===============1387712317359960679==
|
||||
--===============0315165645846539514==
|
||||
Content-Type: application/octet-stream
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
@@ -32,7 +32,7 @@ Content-Disposition: attachment; filename="notes.txt"
|
||||
|
||||
RWluZmFjaGUgVGV4dGRhdGVpLgpaZWlsZSAyLgpVbWxhdXRlOiBhZW9ldWUK
|
||||
|
||||
--===============1387712317359960679==
|
||||
--===============0315165645846539514==
|
||||
Content-Type: application/octet-stream
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
@@ -40,4 +40,4 @@ Content-Disposition: attachment; filename="data.csv"
|
||||
|
||||
bmFtZSx3ZXJ0CkFscGhhLDEKQmV0YSwyCg==
|
||||
|
||||
--===============1387712317359960679==--
|
||||
--===============0315165645846539514==--
|
||||
+5
-5
@@ -1,18 +1,18 @@
|
||||
Content-Type: multipart/mixed; boundary="===============0497362329568289493=="
|
||||
Content-Type: multipart/mixed; boundary="===============4194720031885655663=="
|
||||
MIME-Version: 1.0
|
||||
From: media@example.com
|
||||
To: mcptest@local
|
||||
Subject: Praesentation + Clip
|
||||
Date: Tue, 17 Jun 2026 10:00:00 +0200
|
||||
|
||||
--===============0497362329568289493==
|
||||
--===============4194720031885655663==
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
Rm9saWVuIHVuZCBlaW4ga3VyemVzIFZpZGVvLg==
|
||||
|
||||
--===============0497362329568289493==
|
||||
--===============4194720031885655663==
|
||||
Content-Type: application/octet-stream
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
@@ -515,7 +515,7 @@ AAAANgEAACAAAAAAAAAAAAAAAIAB01sAAHBwdC9zbGlkZXMvX3JlbHMvc2xpZGUxLnhtbC5yZWxz
|
||||
UEsBAhQDFAAAAAgAbz7TXFqgDq2jBQAA4w8AABcAAAAAAAAAAAAAAIAByFwAAGRvY1Byb3BzL3Ro
|
||||
dW1ibmFpbC5qcGVnUEsFBgAAAAAmACYAowsAAKBiAAAAAA==
|
||||
|
||||
--===============0497362329568289493==
|
||||
--===============4194720031885655663==
|
||||
Content-Type: application/octet-stream
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
@@ -713,4 +713,4 @@ AX0AAAHtAAABgwAAAukAAADGAAAAUQAAAEYAAAJOAAAAvgAAAGYAAABfAAAAFHN0Y28AAAAAAAAA
|
||||
AQAAADAAAABhdWR0YQAAAFltZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAA
|
||||
AAAAACxpbHN0AAAAJKl0b28AAAAcZGF0YQAAAAEAAAAATGF2ZjYyLjMuMTAw
|
||||
|
||||
--===============0497362329568289493==--
|
||||
--===============4194720031885655663==--
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
From: weird@example.com
|
||||
To: mcptest@local
|
||||
Subject: Kaputtes Encoding XUNKNOWNTOKEN
|
||||
Date: Wed, 10 Jan 2018 12:00:00 +0100
|
||||
Content-Type: text/plain; charset="x-unknown"
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Koerpertext mit kaputtem Charset XUNKNOWNTOKEN und Umlaut ü.
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
From: =?invalid-charset?Q?Br=F6tchen?= <bad@example.com>
|
||||
To: mcptest@local
|
||||
Subject: =?x-unknown?B?a2FwdXR0?= BADHEADERTOKEN
|
||||
Date: Fri, 05 Jul 2013 09:00:00 +0200
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Body BADHEADERTOKEN.
|
||||
Reference in New Issue
Block a user