test: dedizierter mcptest-User statt Stefans echter Daten
USER_ALIASES test->mcptest. Eigene Backends pro Dienst: - Radicale-User mcptest + /mcptest/calendar-test + contacts-test - oCIS-User mcptest (Graph-API), Joplin lokales Profil :41186 - statische Test-Maildir tests/testdata/maildir (mcp-test-mail/-empty) test_all.py auf mcptest-Backends umgestellt (Account-/Kalender-/Kontaktnamen). config.json (gitignored) mit mcptest in allen Maps. Doku: tests/MCPTEST.md. 54 Tests gruen. Stefans calendar-test/contacts-test + Joplin-Test-Reste entfernt.
This commit is contained in:
@@ -6,3 +6,4 @@ config.json
|
|||||||
.active_tokens.json
|
.active_tokens.json
|
||||||
*.before-*
|
*.before-*
|
||||||
.token_audit.log
|
.token_audit.log
|
||||||
|
*.bak-*
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ def _load_tokens():
|
|||||||
return _tokens_cache
|
return _tokens_cache
|
||||||
|
|
||||||
|
|
||||||
USER_ALIASES = {"test": "stefan"}
|
USER_ALIASES = {"test": "mcptest"}
|
||||||
|
|
||||||
def get_current_user() -> str | None:
|
def get_current_user() -> str | None:
|
||||||
user = _current_user.get()
|
user = _current_user.get()
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# mcptest — isoliertes Test-/Dev-Backend
|
||||||
|
|
||||||
|
Die MCP-Test-Suite (`test_all.py`, taeglich via `mcp-tests.timer`) laeuft **NICHT mehr
|
||||||
|
auf Stefans echten Daten**, sondern auf einem dedizierten `mcptest`-User mit eigenen
|
||||||
|
Backends. Dieselbe Umgebung dient als isolierte **Dev-Sandbox** fuer die MCP-Server.
|
||||||
|
|
||||||
|
## Routing
|
||||||
|
|
||||||
|
`common.py`: `USER_ALIASES = {"test": "mcptest"}`. Der Test-OAuth-Client `client_id=test`
|
||||||
|
(Secret in `config.json` -> `test.token`) wird also auf den User `mcptest` gemappt.
|
||||||
|
Alle Server lesen pro User aus `config.json`.
|
||||||
|
|
||||||
|
## Backends pro Dienst
|
||||||
|
|
||||||
|
- **Calendar/Contacts (Radicale):** User `mcptest` (htpasswd `/opt/radicale/config/htpasswd`,
|
||||||
|
bcrypt). Collections `/mcptest/calendar-test/` (VEVENT+VTODO) und `/mcptest/contacts-test/`
|
||||||
|
(VADDRESSBOOK, Test-Kontakt "Max Mustermann"). `calendar_paths/addressbook_paths[mcptest]=["/mcptest/"]`.
|
||||||
|
- **Files (oCIS):** kompletter User `mcptest` (angelegt via Graph-API `POST /graph/v1.0/users`
|
||||||
|
als admin). `ocis_users[mcptest]`. Tests legen `/.mcp-tests` selbst an + raeumen auf.
|
||||||
|
- **Notes (Joplin):** lokales joplin-cli-Profil `/mnt/ssd/joplin-mcp/profiles/mcptest`
|
||||||
|
(`sync.target=0`, kein Server-Sync), Data API auf **:41186** via `joplin-cli-mcptest.service`.
|
||||||
|
Notizbuecher `Inbox` + `MCP Test` mit Beispielnotizen. `joplin_data_api[mcptest]`.
|
||||||
|
- **Mail:** statische Test-**Maildir** `/opt/mcp-servers/tests/testdata/maildir/` mit Konten
|
||||||
|
`mcp-test-mail` (INBOX: "Willkommen" + "Rechnung"+PDF-Anhang, Sent) und `mcp-test-empty`.
|
||||||
|
`mail_roots[mcptest]` zeigt dahin. (Der Mail-MCP liest Maildirs, kein Live-IMAP.)
|
||||||
|
|
||||||
|
## Credentials
|
||||||
|
|
||||||
|
- Klartext-Backup: `/root/.mcptest-creds` (chmod 600).
|
||||||
|
- Aktiv genutzt: `config.json` (gitignored) — radicale/ocis Passwoerter, joplin Token.
|
||||||
|
- oCIS-Admin (fuer User-Anlage): `/mnt/ssd/ocis/auth.txt`.
|
||||||
|
|
||||||
|
## Tests laufen lassen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/opt/mcp-servers/venv/bin/python -m pytest /opt/mcp-servers/tests/test_all.py -q
|
||||||
|
# oder der taegliche Runner:
|
||||||
|
sudo /opt/mcp-servers/tests/run_tests.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Als Dev-Sandbox nutzen
|
||||||
|
|
||||||
|
- Direkt gegen die Backends: Radicale `http://127.0.0.1:5232/mcptest/` (User mcptest),
|
||||||
|
Joplin Data API `http://127.0.0.1:41186` (Token aus config.json), Maildir s.o.
|
||||||
|
- Ueber die MCP-Server: mit dem `test`-OAuth-Client verbinden -> trifft automatisch mcptest.
|
||||||
|
- Testdaten zuruecksetzen: Collections/Notebooks neu seeden (siehe Provisioning unten).
|
||||||
|
|
||||||
|
## Provisioning (Recreate)
|
||||||
|
|
||||||
|
1. Radicale: `htpasswd -bB /opt/radicale/config/htpasswd mcptest <pw>`; dann MKCALENDAR
|
||||||
|
`/mcptest/calendar-test/` + extended-MKCOL `/mcptest/contacts-test/`.
|
||||||
|
2. oCIS: `POST /graph/v1.0/users` als admin (onPremisesSamAccountName=mcptest, passwordProfile).
|
||||||
|
3. Joplin: Profil anlegen (`joplin --profile DIR config api.token ...; sync.target 0`),
|
||||||
|
`api.port=41186` in settings.json, `joplin-cli-mcptest.service` (Kopie von -stefan),
|
||||||
|
Notebooks/Notizen via Data API seeden.
|
||||||
|
4. Mail: `tests/testdata/maildir/` (im Repo) — Maildir-Konten mit cur/-Nachrichten.
|
||||||
|
5. `config.json`: mcptest in radicale_users, ocis_users, joplin_data_api, mail_roots,
|
||||||
|
calendar_paths, addressbook_paths.
|
||||||
|
6. `common.py`: `USER_ALIASES = {"test": "mcptest"}`.
|
||||||
|
|
||||||
|
Verwandt: `/opt/mcp-servers/CLAUDE.md`.
|
||||||
+12
-12
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""MCP Server Integration Tests.
|
"""MCP Server Integration Tests.
|
||||||
|
|
||||||
Uses a 'test' OAuth client (maps to stefan's data).
|
Uses a 'test' OAuth client (maps to dedicated mcptest backends).
|
||||||
Creates, reads, updates, and deletes test data in dedicated test collections:
|
Creates, reads, updates, and deletes test data in dedicated test collections:
|
||||||
- Radicale: calendar-test, contacts-test
|
- Radicale: calendar-test, contacts-test
|
||||||
- oCIS: /mcp-tests/
|
- oCIS: /mcp-tests/
|
||||||
@@ -133,10 +133,10 @@ class TestMail:
|
|||||||
|
|
||||||
def test_list_accounts(self):
|
def test_list_accounts(self):
|
||||||
text = tool_call(self.PORT, get_token(self.PORT), "list_accounts")
|
text = tool_call(self.PORT, get_token(self.PORT), "list_accounts")
|
||||||
assert "d370128_0-slohmaier" in text
|
assert "mcp-test-mail" in text
|
||||||
|
|
||||||
def test_list_folders(self):
|
def test_list_folders(self):
|
||||||
text = tool_call(self.PORT, get_token(self.PORT), "list_folders", {"account": "d370128_0-slohmaier"})
|
text = tool_call(self.PORT, get_token(self.PORT), "list_folders", {"account": "mcp-test-mail"})
|
||||||
assert "INBOX" in text
|
assert "INBOX" in text
|
||||||
|
|
||||||
def test_search_finds_something(self):
|
def test_search_finds_something(self):
|
||||||
@@ -145,13 +145,13 @@ class TestMail:
|
|||||||
|
|
||||||
def test_search_empty_result(self):
|
def test_search_empty_result(self):
|
||||||
text = tool_call(self.PORT, get_token(self.PORT), "search_mail",
|
text = tool_call(self.PORT, get_token(self.PORT), "search_mail",
|
||||||
{"query": "xyzzy_nonexistent_99", "limit": 1, "account": "d370128_0-gitea"})
|
{"query": "xyzzy_nonexistent_99", "limit": 1, "account": "mcp-test-empty"})
|
||||||
assert "No results" in text
|
assert "No results" in text
|
||||||
|
|
||||||
def test_read_mail(self):
|
def test_read_mail(self):
|
||||||
# First search to get a key
|
# First search to get a key
|
||||||
text = tool_call(self.PORT, get_token(self.PORT), "search_mail",
|
text = tool_call(self.PORT, get_token(self.PORT), "search_mail",
|
||||||
{"query": "stefan", "limit": 1, "account": "d370128_0-slohmaier"})
|
{"query": "stefan", "limit": 1, "account": "mcp-test-mail"})
|
||||||
if "No results" in text:
|
if "No results" in text:
|
||||||
pytest.skip("No mails found")
|
pytest.skip("No mails found")
|
||||||
# Parse account, folder, key from result
|
# Parse account, folder, key from result
|
||||||
@@ -171,7 +171,7 @@ class TestMail:
|
|||||||
token = get_token(self.PORT)
|
token = get_token(self.PORT)
|
||||||
# Find a mail with an attachment by scanning Rechnung search results
|
# Find a mail with an attachment by scanning Rechnung search results
|
||||||
text = tool_call(self.PORT, token, "search_mail",
|
text = tool_call(self.PORT, token, "search_mail",
|
||||||
{"query": "Rechnung", "limit": 10, "account": "d370128_0-slohmaier"})
|
{"query": "Rechnung", "limit": 10, "account": "mcp-test-mail"})
|
||||||
if "No results" in text:
|
if "No results" in text:
|
||||||
pytest.skip("No mails found")
|
pytest.skip("No mails found")
|
||||||
# Read each result, look for one with attachments
|
# Read each result, look for one with attachments
|
||||||
@@ -211,7 +211,7 @@ class TestCalendar:
|
|||||||
|
|
||||||
def test_list_calendars(self):
|
def test_list_calendars(self):
|
||||||
text = tool_call(self.PORT, get_token(self.PORT), "list_calendars")
|
text = tool_call(self.PORT, get_token(self.PORT), "list_calendars")
|
||||||
assert "calendar-stefan" in text or "Stefan" in text
|
assert "calendar-test" in text or "MCP Test" in text
|
||||||
|
|
||||||
def test_list_task_lists(self):
|
def test_list_task_lists(self):
|
||||||
text = tool_call(self.PORT, get_token(self.PORT), "list_task_lists")
|
text = tool_call(self.PORT, get_token(self.PORT), "list_task_lists")
|
||||||
@@ -251,7 +251,7 @@ class TestCalendar:
|
|||||||
|
|
||||||
def test_get_events_date_range(self):
|
def test_get_events_date_range(self):
|
||||||
text = tool_call(self.PORT, get_token(self.PORT), "get_events", {
|
text = tool_call(self.PORT, get_token(self.PORT), "get_events", {
|
||||||
"calendar": "stefan", "date_from": "2026-06-01", "date_to": "2026-06-30",
|
"calendar": "calendar-test", "date_from": "2026-06-01", "date_to": "2026-06-30",
|
||||||
})
|
})
|
||||||
assert "Wann:" in text or "Keine Termine" in text
|
assert "Wann:" in text or "Keine Termine" in text
|
||||||
|
|
||||||
@@ -264,8 +264,8 @@ class TestContacts:
|
|||||||
PORT = SERVERS["contacts"]
|
PORT = SERVERS["contacts"]
|
||||||
|
|
||||||
def test_search_real_contacts(self):
|
def test_search_real_contacts(self):
|
||||||
text = tool_call(self.PORT, get_token(self.PORT), "search_contacts", {"query": "Lohmaier", "limit": 3})
|
text = tool_call(self.PORT, get_token(self.PORT), "search_contacts", {"query": "Mustermann", "limit": 3})
|
||||||
assert "Lohmaier" in text
|
assert "Mustermann" in text
|
||||||
|
|
||||||
def test_search_empty(self):
|
def test_search_empty(self):
|
||||||
text = tool_call(self.PORT, get_token(self.PORT), "search_contacts", {"query": "xyzzy_nobody_99"})
|
text = tool_call(self.PORT, get_token(self.PORT), "search_contacts", {"query": "xyzzy_nobody_99"})
|
||||||
@@ -288,14 +288,14 @@ class TestContacts:
|
|||||||
|
|
||||||
def test_get_contact_details(self):
|
def test_get_contact_details(self):
|
||||||
token = get_token(self.PORT)
|
token = get_token(self.PORT)
|
||||||
search = tool_call(self.PORT, token, "search_contacts", {"query": "Lohmaier", "limit": 1})
|
search = tool_call(self.PORT, token, "search_contacts", {"query": "Mustermann", "limit": 1})
|
||||||
if "Keine" in search:
|
if "Keine" in search:
|
||||||
pytest.skip("No contacts")
|
pytest.skip("No contacts")
|
||||||
uid = [l for l in search.split("\n") if "UID:" in l][0].split("UID: ")[1].strip()
|
uid = [l for l in search.split("\n") if "UID:" in l][0].split("UID: ")[1].strip()
|
||||||
# get_contact returns list content, first element is text
|
# get_contact returns list content, first element is text
|
||||||
result = mcp_call(self.PORT, token, "tools/call", {"name": "get_contact", "arguments": {"uid": uid}})
|
result = mcp_call(self.PORT, token, "tools/call", {"name": "get_contact", "arguments": {"uid": uid}})
|
||||||
text = result["result"]["content"][0]["text"]
|
text = result["result"]["content"][0]["text"]
|
||||||
assert "Lohmaier" in text
|
assert "Mustermann" in text
|
||||||
assert "UID:" in text
|
assert "UID:" in text
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
Content-Type: text/plain; charset="utf-8"
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
From: noreply@example.com
|
||||||
|
To: mcptest@local
|
||||||
|
Subject: Newsletter
|
||||||
|
Date: Mon, 16 Jun 2026 09:00:00 +0200
|
||||||
|
|
||||||
|
TmljaHRzIGJlc29uZGVyZXMgaGllci4=
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
Content-Type: text/plain; charset="utf-8"
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
From: test@example.com
|
||||||
|
To: mcptest@local
|
||||||
|
Subject: Willkommen
|
||||||
|
Date: Mon, 16 Jun 2026 09:00:00 +0200
|
||||||
|
|
||||||
|
SGFsbG8sIGRpZXMgaXN0IGVpbmUgVGVzdG5hY2hyaWNodCBmdWVyIHN0ZWZhbiB6dW0gU3VjaGVu
|
||||||
|
Lg==
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
Content-Type: multipart/mixed; boundary="===============0575628907158327116=="
|
||||||
|
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
|
||||||
|
|
||||||
|
--===============0575628907158327116==
|
||||||
|
Content-Type: text/plain; charset="utf-8"
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
SW0gQW5oYW5nIGZpbmRlbiBTaWUgSWhyZSBSZWNobnVuZy4=
|
||||||
|
|
||||||
|
--===============0575628907158327116==
|
||||||
|
Content-Type: application/pdf
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
Content-Disposition: attachment; filename="rechnung.pdf"
|
||||||
|
|
||||||
|
JVBERi0xLjQKMSAwIG9iajw8Pj5lbmRvYmoKdHJhaWxlcjw8Pj4KJSVFT0YK
|
||||||
|
|
||||||
|
--===============0575628907158327116==--
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
Content-Type: text/plain; charset="utf-8"
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
From: mcptest@local
|
||||||
|
To: kunde@example.com
|
||||||
|
Subject: Re: Angebot
|
||||||
|
Date: Mon, 16 Jun 2026 09:00:00 +0200
|
||||||
|
|
||||||
|
RGFua2UgZnVlciBJaHJlIEFuZnJhZ2Uu
|
||||||
Reference in New Issue
Block a user