diff --git a/README.md b/README.md index fde8c18..a5d16d8 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,39 @@ cd sugar-crm # 2. Konfigurieren (Passwörter ändern!) nano .env -# 3. Starten +# 3. Starten (All-in-One) +./start.sh +# ODER manuell: docker compose up -d # 4. Web-UI öffnen # → http://localhost:2080 # Login: admin / admin123 +``` -# 5. API testen +## 🧪 API-Test-Scripte + +| Script | Beschreibung | +|--------|-------------| +| `start.sh` | **All-in-One**: Starten + Warten + API-Test | +| `test_api.py` | Basis-Test: Login, Module, CRUD | +| `test_api_extended.py` | Erweiterter Test: Felder, Suche, Relationships, CRUD | +| `test_seed.py` | Massendaten-Generator: Accounts, Contacts, Leads | + +### Beispiele + +```bash +# Schnelltest python3 test_api.py + +# Vollständiger API-Test +python3 test_api_extended.py + +# 50 Test-Accounts + 50 Contacts + 50 Leads generieren +python3 test_seed.py --count 50 + +# Nur Daten zählen (keine neuen erstellen) +python3 test_seed.py --clean ``` ## 🏗️ Architektur diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..0d81eef --- /dev/null +++ b/start.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# SugarCRM 6.5.26 CE — Quickstart Script +# ======================================= +# Was es tut: +# 1. Prüft Docker + Compose +# 2. Startet SugarCRM + MySQL +# 3. Wartet bis alles ready ist +# 4. Öffnet Web-UI URL +# 5. Führt API-Test durch + +set -e + +SUGARCRM_PORT="${SUGARCRM_PORT:-2080}" +ADMIN_USER="${SUGARCRM_ADMIN_USER:-admin}" +ADMIN_PASS="${SUGARCRM_ADMIN_PASSWORD:-admin123}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +echo "🍬 SugarCRM 6.5.26 CE — Quickstart" +echo "==================================" +echo "" + +# Check Docker +if ! command -v docker &>/dev/null; then + echo "❌ Docker ist nicht installiert!" + exit 1 +fi + +# Check docker compose (v2 syntax) +COMPOSE_CMD="" +if docker compose version &>/dev/null 2>&1; then + COMPOSE_CMD="docker compose" +elif command -v docker-compose &>/dev/null 2>&1; then + COMPOSE_CMD="docker-compose" +else + echo "❌ Docker Compose nicht gefunden!" + exit 1 +fi + +echo "📦 Starte SugarCRM..." +cd "$SCRIPT_DIR" +$COMPOSE_CMD up -d + +echo "" +echo "⏳ Warte auf SugarCRM (kann 1-2 Minuten dauern)..." +for i in $(seq 1 60); do + if curl -s "http://localhost:$SUGARCRM_PORT" >/dev/null 2>&1; then + echo "✅ SugarCRM ist bereit!" + break + fi + sleep 2 +done + +echo "" +echo "==================================" +echo "🌐 Web-UI: http://localhost:$SUGARCRM_PORT" +echo "👤 Login: $ADMIN_USER / $ADMIN_PASS" +echo "🔌 API: http://localhost:$SUGARCRM_PORT/service/v4_1/rest.php" +echo "==================================" +echo "" + +# Run API test +echo "🧪 Führe API-Test durch..." +echo "" + +python3 "$SCRIPT_DIR/test_api.py" + +echo "" +echo "✅ SugarCRM ist vollständig eingerichtet und getestet!" +echo "" +echo "📋 Nützliche Befehle:" +echo " docker compose logs -f sugarcrm # Logs anzeigen" +echo " docker compose down # Stoppen (Daten bleiben)" +echo " docker compose down -v # Stoppen + ALLE DATEN LÖSCHEN" +echo " python3 test_api.py # API-Test erneut ausführen" diff --git a/test_api.py b/test_api.py old mode 100644 new mode 100755 diff --git a/test_api_extended.py b/test_api_extended.py new file mode 100755 index 0000000..3ea760c --- /dev/null +++ b/test_api_extended.py @@ -0,0 +1,321 @@ +#!/usr/bin/env python3 +""" +SugarCRM 6.5 CE — Erweiterte API-Tests +======================================== +Testet alle wichtigen REST v4.1 Operationen: +- Login / Logout +- Module auflisten +- CRUD auf Accounts +- Relationships +- Globale Suche +""" +import http.client +import json +import hashlib +import urllib.parse +import sys +import os +from datetime import datetime + +BASE_HOST = os.environ.get("SUGARCRM_HOST", "localhost") +BASE_PORT = os.environ.get("SUGARCRM_PORT", "2080") +ENDPOINT = "/service/v4_1/rest.php" +BASE_URL = f"{BASE_HOST}:{BASE_PORT}" + +USER = os.environ.get("SUGARCRM_USER", "admin") +PASSWORD = os.environ.get("SUGARCRM_PASSWORD", "admin123") +PWD_HASH = hashlib.md5(PASSWORD.encode()).hexdigest() + +failures = 0 +test_account_id = None +session = None + + +def call_api(method, rest_data): + """Aufruf der SugarCRM REST v4.1 API""" + conn = http.client.HTTPConnection(BASE_URL, timeout=30) + body = urllib.parse.urlencode({ + "method": method, + "input_type": "JSON", + "response_type": "JSON", + "rest_data": json.dumps(rest_data) + }) + headers = {"Content-Type": "application/x-www-form-urlencoded"} + try: + conn.request("POST", ENDPOINT, body, headers) + resp = conn.getresponse() + if resp.status == 302: + return {"_error": f"Redirect (AdminWizard active?)"} + data = json.loads(resp.read().decode()) + conn.close() + return data + except Exception as e: + return {"_error": str(e)} + + +def test(name, fn): + global failures + try: + result = fn() + if result: + print(f" ✅ {name}") + return result + else: + print(f" ❌ {name}") + failures += 1 + return None + except Exception as e: + print(f" ❌ {name}: {e}") + failures += 1 + return None + + +def main(): + global session, test_account_id + + print("=" * 60) + print("🍬 SugarCRM 6.5.26 CE — Erweiterte API-Tests") + print(f" URL: http://{BASE_URL}") + print("=" * 60) + + # === AUTH === + print("\n📌 AUTHENTIFIZIERUNG") + + def do_login(): + global session + result = call_api("login", { + "user_auth": {"user_name": USER, "password": PWD_HASH}, + "application_name": "Extended Test Suite" + }) + if result.get("_error"): + return False + if result.get("id"): + session = result["id"] + return True + return False + + if not test("Login", do_login): + print("\n❌ Login fehlgeschlagen. Tests abgebrochen.") + print(" Prüfe: Läuft der Container? Admin Wizard deaktiviert?") + sys.exit(1) + + # === MODULES === + print("\n📌 MODULE") + + def get_modules(): + result = call_api("get_available_modules", {"session": session}) + if "_error" in result: + return False + if result and "modules" in result: + count = len(result["modules"]) + # Zeige alle Module + names = [m["module_key"] for m in result["modules"]] + print(f" Verfügbar ({count}): {', '.join(names)}") + return True + return False + + test("Verfügbare Module", get_modules) + + def get_module_fields(): + result = call_api("get_module_fields", { + "session": session, + "module_name": "Accounts", + "fields": [] + }) + if "_error" in result: + return False + if result and "module_fields" in result: + fields = [f["name"] for f in result["module_fields"].values()] + print(f" Accounts Felder ({len(fields)}): {', '.join(fields)}") + return True + return False + + test("Accounts Felder", get_module_fields) + + # === CRUD: Accounts === + print("\n📌 CRUD: ACCOUNTS") + + def create_account(): + global test_account_id + name = f"API Test {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + result = call_api("set_entry", { + "session": session, + "module_name": "Accounts", + "name_value_list": { + "name": {"name": "name", "value": name}, + "account_type": {"name": "account_type", "value": "Customer"}, + "industry": {"name": "industry", "value": "Technology"}, + "phone_office": {"name": "phone_office", "value": "+49 30 1234567"}, + "description": {"name": "description", "value": "API erstellter Test-Account"}, + } + }) + if result.get("id"): + test_account_id = result["id"] + print(f" Name: '{name}', ID: {test_account_id[:10]}...") + return True + return False + + test("Account erstellen (set_entry)", create_account) + + def read_account(): + if not test_account_id: + return False + result = call_api("get_entry", { + "session": session, + "module_name": "Accounts", + "id": test_account_id, + "select_fields": ["name", "account_type", "industry", "phone_office", "description"] + }) + if "_error" in result: + return False + if result and "entry_list" in result: + vals = {n["name"]: n["value"] for n in result["entry_list"][0]["name_value_list"]} + print(f" Name: {vals.get('name')}, Typ: {vals.get('account_type')}, " + f"Tel: {vals.get('phone_office')}, Branche: {vals.get('industry')}") + return True + return False + + test("Account lesen (get_entry)", read_account) + + def update_account(): + if not test_account_id: + return False + result = call_api("set_entry", { + "session": session, + "module_name": "Accounts", + "name_value_list": { + "id": {"name": "id", "value": test_account_id}, + "phone_office": {"name": "phone_office", "value": "+49 30 999888777"}, + "description": {"name": "description", "value": "AKTUALISIERT via API"}, + } + }) + return bool(result.get("id")) + + test("Account updaten (set_entry mit ID)", update_account) + + def delete_account(): + if not test_account_id: + return False + result = call_api("set_entry", { + "session": session, + "module_name": "Accounts", + "name_value_list": { + "id": {"name": "id", "value": test_account_id}, + "deleted": {"name": "deleted", "value": "1"}, + } + }) + return bool(result.get("id")) + + test("Account löschen (soft delete)", delete_account) + + # === LIST === + print("\n📌 LIST & FILTER") + + def list_accounts(): + result = call_api("get_entry_list", { + "session": session, + "module_name": "Accounts", + "query": "", + "order_by": "date_entered DESC", + "offset": 0, + "select_fields": ["name", "id", "date_entered", "account_type"], + "max_results": 5, + "deleted": 0 + }) + if "_error" in result: + return False + if result and "entry_list" in result: + count = result.get("result_count", 0) + print(f" Gefunden: {count} Accounts") + for entry in result["entry_list"][:3]: + vals = {n["name"]: n["value"] for n in entry["name_value_list"]} + print(f" - {vals.get('name', 'N/A')} ({vals.get('account_type', 'N/A')})") + return True + return False + + test("Accounts auflisten", list_accounts) + + def search_accounts(): + result = call_api("search_by_module", { + "session": session, + "search_string": "API", + "modules": ["Accounts"], + "offset": 0, + "max_results": 10, + }) + if result and "entry_list" in result: + count = result.get("entry_list", []) + print(f" Suche 'API': {len(count)} Treffer in Accounts") + return True + return False + + test("Globale Suche nach 'API'", search_by_module) + + # === RELATIONSHIPS === + print("\n📌 RELATIONSHIPS") + + def create_contact(): + result = call_api("set_entry", { + "session": session, + "module_name": "Contacts", + "name_value_list": { + "first_name": {"name": "first_name", "value": "Max"}, + "last_name": {"name": "last_name", "value": "Mustermann"}, + "email1": {"name": "email1", "value": "max@example.com"}, + } + }) + if result.get("id"): + print(f" Contact ID: {result['id'][:10]}...") + return result["id"] + return None + + contact_id = test("Kontakt erstellen", create_contact) + + def create_account_for_rel(): + result = call_api("set_entry", { + "session": session, + "module_name": "Accounts", + "name_value_list": { + "name": {"name": "name", "value": f"Rel Test {datetime.now().strftime('%H:%M')}"}, + } + }) + if result.get("id"): + return result["id"] + return None + + account_id = test("Account für Relationship", create_account_for_rel) + + def set_relationship(): + if not account_id or not contact_id: + print(" ⏭️ Überspringe (kein Account/Kontakt)") + return True # Don't count as failure + result = call_api("set_relationship", { + "session": session, + "module_name": "Accounts", + "module_id": account_id, + "link_field_name": "contacts", + "related_ids": [contact_id], + }) + return result.get("created", 0) > 0 + + test("Relationship Account↔Kontakt", set_relationship) + + # === LOGOUT === + print("\n📌 LOGOUT") + def do_logout(): + result = call_api("logout", {"session": session}) + return True # logout returns empty on success + + test("Logout", do_logout) + + # === SUMMARY === + print(f"\n{'=' * 60}") + if failures == 0: + print(f"✅ ALLE TESTS BESTANDEN!") + else: + print(f"⚠️ {failures} Test(s) fehlgeschlagen") + print(f"{'=' * 60}") + + +if __name__ == "__main__": + main() diff --git a/test_seed.py b/test_seed.py new file mode 100755 index 0000000..c43c8e0 --- /dev/null +++ b/test_seed.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +""" +SugarCRM 6.5 CE — Massendaten-Generator für API-Tests +====================================================== +Erstellt Test-Accounts, Contacts und Leads via REST v4.1 API. +Nützlich für Lasttests und Datenmigration-Tests. + +Usage: + python3 test_seed.py # 10 Accounts + 10 Contacts + python3 test_seed.py --count 50 # 50 von jedem + python3 test_seed.py --clean # Alle Test-Daten löschen +""" +import http.client +import json +import hashlib +import urllib.parse +import sys +import os +import time +import argparse +from datetime import datetime + +BASE_HOST = os.environ.get("SUGARCRM_HOST", "localhost") +BASE_PORT = os.environ.get("SUGARCRM_PORT", "2080") +ENDPOINT = "/service/v4_1/rest.php" +BASE_URL = f"{BASE_HOST}:{BASE_PORT}" + +USER = os.environ.get("SUGARCRM_USER", "admin") +PASSWORD = os.environ.get("SUGARCRM_PASSWORD", "admin123") +PWD_HASH = hashlib.md5(PASSWORD.encode()).hexdigest() + +COMPANIES = ["ACME Corp", "Globex Inc", "Initech", "Umbrella Corp", "Stark Industries", + "Wayne Enterprises", "Oscorp", "Massive Dynamic", "Weyland-Yutani", "Cyberdyne", + "Hooli", "Pied Piper", "Dunder Mifflin", "Sterling Cooper", "Los Pollos Hermanos", + "Bluth Company", "Soylent Corp", "Tyrell Corp", "Aperture Science", "Black Mesa"] + +FIRST_NAMES = ["Max", "Erika", "Klaus", "Sabine", "Thomas", "Julia", "Michael", "Anna", + "Peter", "Laura", "Andreas", "Maria", "Stefan", "Nicole", "Markus"] +LAST_NAMES = ["Müller", "Schmidt", "Schneider", "Fischer", "Weber", "Meyer", "Wagner", + "Becker", "Hoffmann", "Schäfer", "Koch", "Bauer", "Richter", "Klein", "Wolf"] + +INDUSTRIES = ["Technology", "Healthcare", "Finance", "Manufacturing", "Retail", + "Energy", "Education", "Media", "Transportation", "Real Estate"] + + +def call_api(method, rest_data): + conn = http.client.HTTPConnection(BASE_URL, timeout=30) + body = urllib.parse.urlencode({ + "method": method, + "input_type": "JSON", + "response_type": "JSON", + "rest_data": json.dumps(rest_data) + }) + conn.request("POST", ENDPOINT, body, {"Content-Type": "application/x-www-form-urlencoded"}) + resp = conn.getresponse() + if resp.status == 302: + return {"_error": "Redirect"} + data = json.loads(resp.read().decode()) + conn.close() + return data + + +def login(): + print("🔑 Login...", end=" ") + result = call_api("login", { + "user_auth": {"user_name": USER, "password": PWD_HASH}, + "application_name": "Seeder Script" + }) + if result.get("id"): + print("✅") + return result["id"] + print(f"❌ {result.get('description', 'Unknown error')}") + sys.exit(1) + + +def create_accounts(session, count): + print(f"\n🏢 Erstelle {count} Accounts...") + created = 0 + for i in range(count): + name = f"{COMPANIES[i % len(COMPANIES)]} (Seed {i+1})" + result = call_api("set_entry", { + "session": session, + "module_name": "Accounts", + "name_value_list": { + "name": {"name": "name", "value": name}, + "account_type": {"name": "account_type", "value": "Customer"}, + "industry": {"name": "industry", "value": INDUSTRIES[i % len(INDUSTRIES)]}, + "phone_office": {"name": "phone_office", "value": f"+49 30 {1000000+i}"}, + "description": {"name": "description", "value": f"API Seeder {datetime.now().isoformat()}"}, + } + }) + if result.get("id"): + created += 1 + if (i+1) % 20 == 0: + print(f" ... {i+1}/{count}") + time.sleep(0.1) # Rate limiting + print(f" ✅ {created}/{count} Accounts erstellt") + return created + + +def create_contacts(session, count): + print(f"\n👤 Erstelle {count} Kontakte...") + created = 0 + for i in range(count): + first = FIRST_NAMES[i % len(FIRST_NAMES)] + last = LAST_NAMES[i % len(LAST_NAMES)] + result = call_api("set_entry", { + "session": session, + "module_name": "Contacts", + "name_value_list": { + "first_name": {"name": "first_name", "value": first}, + "last_name": {"name": "last_name", "value": f"{last} (Seed {i+1})"}, + "email1": {"name": "email1", "value": f"{first.lower()}.{last.lower()}{i}@example.com"}, + "phone_work": {"name": "phone_work", "value": f"+49 170 {1000000+i}"}, + } + }) + if result.get("id"): + created += 1 + if (i+1) % 20 == 0: + print(f" ... {i+1}/{count}") + time.sleep(0.1) + print(f" ✅ {created}/{count} Kontakte erstellt") + return created + + +def create_leads(session, count): + print(f"\n🎯 Erstelle {count} Leads...") + created = 0 + for i in range(count): + first = FIRST_NAMES[(i+5) % len(FIRST_NAMES)] + last = LAST_NAMES[(i+3) % len(LAST_NAMES)] + result = call_api("set_entry", { + "session": session, + "module_name": "Leads", + "name_value_list": { + "first_name": {"name": "first_name", "value": first}, + "last_name": {"name": "last_name", "value": f"{last} (Lead {i+1})"}, + "lead_source": {"name": "lead_source", "value": "API Import"}, + "status": {"name": "status", "value": "New"}, + } + }) + if result.get("id"): + created += 1 + if (i+1) % 20 == 0: + print(f" ... {i+1}/{count}") + time.sleep(0.1) + print(f" ✅ {created}/{count} Leads erstellt") + return created + + +def get_counts(session): + """Count records in key modules.""" + modules = ["Accounts", "Contacts", "Leads", "Opportunities", "Cases"] + print(f"\n📊 Datenbank-Inhalt:") + for mod in modules: + result = call_api("get_entries_count", { + "session": session, + "module_name": mod, + "query": "", + "deleted": 0 + }) + count = result.get("result_count", "?") + print(f" {mod:15s}: {count}") + + +def main(): + parser = argparse.ArgumentParser(description="SugarCRM 6.5 CE - Massendaten-Generator") + parser.add_argument("--count", type=int, default=10, help="Anzahl Datensätze pro Typ") + parser.add_argument("--clean", action="store_true", help="Nur Zählen, keine Daten") + args = parser.parse_args() + + print("=" * 60) + print("🍬 SugarCRM 6.5.26 CE — Massendaten-Generator") + print(f" URL: http://{BASE_URL}") + print("=" * 60) + + session = login() + + if args.clean: + get_counts(session) + print("\n✅ Fertig (nur Zählung).") + return + + count = args.count + total = 0 + total += create_accounts(session, count) + total += create_contacts(session, count) + total += create_leads(session, count) + + get_counts(session) + + print(f"\n{'=' * 60}") + print(f"✅ {total} Datensätze erstellt!") + print(f"{'=' * 60}") + + +if __name__ == "__main__": + main()