From 0a1576aaa5889387ae67f766b6727cff4a41599b Mon Sep 17 00:00:00 2001 From: Stefan Lohmaier Date: Fri, 19 Jun 2026 07:46:35 +0200 Subject: [PATCH] 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. --- .gitignore | 1 + common.py | 2 +- tests/MCPTEST.md | 61 +++++++++++++++++++ tests/test_all.py | 24 ++++---- .../cur/1781847419.M885002P1986110Q4.polo:2,S | 9 +++ .../cur/1781847419.M876631P1986110Q1.polo:2,S | 10 +++ .../cur/1781847419.M881146P1986110Q2.polo:2,S | 23 +++++++ .../cur/1781847419.M883003P1986110Q3.polo:2,S | 9 +++ 8 files changed, 126 insertions(+), 13 deletions(-) create mode 100644 tests/MCPTEST.md create mode 100644 tests/testdata/maildir/mcp-test-empty/INBOX/cur/1781847419.M885002P1986110Q4.polo:2,S create mode 100644 tests/testdata/maildir/mcp-test-mail/INBOX/cur/1781847419.M876631P1986110Q1.polo:2,S create mode 100644 tests/testdata/maildir/mcp-test-mail/INBOX/cur/1781847419.M881146P1986110Q2.polo:2,S create mode 100644 tests/testdata/maildir/mcp-test-mail/Sent/cur/1781847419.M883003P1986110Q3.polo:2,S diff --git a/.gitignore b/.gitignore index bc7bcef..1b8becf 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ config.json .active_tokens.json *.before-* .token_audit.log +*.bak-* diff --git a/common.py b/common.py index ec6adf3..d94da44 100644 --- a/common.py +++ b/common.py @@ -61,7 +61,7 @@ def _load_tokens(): return _tokens_cache -USER_ALIASES = {"test": "stefan"} +USER_ALIASES = {"test": "mcptest"} def get_current_user() -> str | None: user = _current_user.get() diff --git a/tests/MCPTEST.md b/tests/MCPTEST.md new file mode 100644 index 0000000..08905ce --- /dev/null +++ b/tests/MCPTEST.md @@ -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 `; 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`. diff --git a/tests/test_all.py b/tests/test_all.py index f625950..aa03164 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """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: - Radicale: calendar-test, contacts-test - oCIS: /mcp-tests/ @@ -133,10 +133,10 @@ class TestMail: def test_list_accounts(self): 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): - 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 def test_search_finds_something(self): @@ -145,13 +145,13 @@ class TestMail: def test_search_empty_result(self): 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 def test_read_mail(self): # First search to get a key 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: pytest.skip("No mails found") # Parse account, folder, key from result @@ -171,7 +171,7 @@ class TestMail: token = get_token(self.PORT) # Find a mail with an attachment by scanning Rechnung search results 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: pytest.skip("No mails found") # Read each result, look for one with attachments @@ -211,7 +211,7 @@ class TestCalendar: def test_list_calendars(self): 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): 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): 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 @@ -264,8 +264,8 @@ class TestContacts: PORT = SERVERS["contacts"] def test_search_real_contacts(self): - text = tool_call(self.PORT, get_token(self.PORT), "search_contacts", {"query": "Lohmaier", "limit": 3}) - assert "Lohmaier" in text + text = tool_call(self.PORT, get_token(self.PORT), "search_contacts", {"query": "Mustermann", "limit": 3}) + assert "Mustermann" in text def test_search_empty(self): 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): 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: pytest.skip("No contacts") 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 result = mcp_call(self.PORT, token, "tools/call", {"name": "get_contact", "arguments": {"uid": uid}}) text = result["result"]["content"][0]["text"] - assert "Lohmaier" in text + assert "Mustermann" in text assert "UID:" in text diff --git a/tests/testdata/maildir/mcp-test-empty/INBOX/cur/1781847419.M885002P1986110Q4.polo:2,S b/tests/testdata/maildir/mcp-test-empty/INBOX/cur/1781847419.M885002P1986110Q4.polo:2,S new file mode 100644 index 0000000..a3794f9 --- /dev/null +++ b/tests/testdata/maildir/mcp-test-empty/INBOX/cur/1781847419.M885002P1986110Q4.polo:2,S @@ -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= diff --git a/tests/testdata/maildir/mcp-test-mail/INBOX/cur/1781847419.M876631P1986110Q1.polo:2,S b/tests/testdata/maildir/mcp-test-mail/INBOX/cur/1781847419.M876631P1986110Q1.polo:2,S new file mode 100644 index 0000000..b9a7b19 --- /dev/null +++ b/tests/testdata/maildir/mcp-test-mail/INBOX/cur/1781847419.M876631P1986110Q1.polo:2,S @@ -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== diff --git a/tests/testdata/maildir/mcp-test-mail/INBOX/cur/1781847419.M881146P1986110Q2.polo:2,S b/tests/testdata/maildir/mcp-test-mail/INBOX/cur/1781847419.M881146P1986110Q2.polo:2,S new file mode 100644 index 0000000..e316e6c --- /dev/null +++ b/tests/testdata/maildir/mcp-test-mail/INBOX/cur/1781847419.M881146P1986110Q2.polo:2,S @@ -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==-- diff --git a/tests/testdata/maildir/mcp-test-mail/Sent/cur/1781847419.M883003P1986110Q3.polo:2,S b/tests/testdata/maildir/mcp-test-mail/Sent/cur/1781847419.M883003P1986110Q3.polo:2,S new file mode 100644 index 0000000..bece4cd --- /dev/null +++ b/tests/testdata/maildir/mcp-test-mail/Sent/cur/1781847419.M883003P1986110Q3.polo:2,S @@ -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