diff --git a/backend/form_test_automator.py b/backend/form_test_automator.py index b7005aa26..13e3738c4 100644 --- a/backend/form_test_automator.py +++ b/backend/form_test_automator.py @@ -30,6 +30,10 @@ try: except ImportError: PLAYWRIGHT_AVAILABLE = False print("⚠️ Playwright nicht verfügbar. Installiere mit: pip install playwright") + # Fallback-Klassen für Type Hints + class Page: pass + class Browser: pass + class BrowserContext: pass # Test-Daten-Generierung try: @@ -38,6 +42,20 @@ try: except ImportError: FAKER_AVAILABLE = False print("⚠️ Faker nicht verfügbar. Installiere mit: pip install faker") + # Fallback-Klasse + class Faker: + def __init__(self, locale='de_DE'): pass + def email(self): return "test@example.com" + def name(self): return "Test User" + def first_name(self): return "Test" + def last_name(self): return "User" + def company(self): return "Test Company" + def city(self): return "Test City" + def street_address(self): return "Test Street 123" + def phone_number(self): return "+49 123 4567890" + def url(self): return "https://example.com" + def date(self): return "2024-06-18" + def text(self, max_nb_chars=200): return "Test text" # HTML-Parsing try: @@ -46,6 +64,9 @@ try: except ImportError: BS4_AVAILABLE = False print("⚠️ BeautifulSoup nicht verfügbar. Installiere mit: pip install beautifulsoup4") + # Fallback-Klasse + class BeautifulSoup: + def __init__(self, *args, **kwargs): pass # Rich Console für schöne Ausgabe try: @@ -59,6 +80,13 @@ try: except ImportError: RICH_AVAILABLE = False console = None + # Fallback-Klassen + class Console: + def print(self, *args, **kwargs): print(*args) + class Table: + def __init__(self, *args, **kwargs): pass + def add_column(self, *args, **kwargs): pass + def add_row(self, *args, **kwargs): pass class TestStatus(Enum): diff --git a/backend/simple_form_tester.py b/backend/simple_form_tester.py new file mode 100644 index 000000000..fa279be23 --- /dev/null +++ b/backend/simple_form_tester.py @@ -0,0 +1,635 @@ +#!/usr/bin/env python3 +""" +Vereinfachter HTML-Formular Tester für MYP System +=============================================== + +Ein schlanker Formular-Tester ohne externe Dependencies, +der grundlegende Formular-Validierung mit Standard-Python-Libraries testet. + +Autor: Till Tomczak +Mercedes-Benz Projektarbeit MYP +""" + +import asyncio +import json +import time +import urllib.request +import urllib.parse +import urllib.error +from urllib.parse import urljoin, urlparse +from html.parser import HTMLParser +from dataclasses import dataclass +from typing import Dict, List, Optional +import sys +import re + + +@dataclass +class FormField: + """Repräsentiert ein Formular-Feld""" + name: str + field_type: str + required: bool = False + pattern: str = "" + placeholder: str = "" + value: str = "" + + +@dataclass +class Form: + """Repräsentiert ein HTML-Formular""" + action: str + method: str + fields: List[FormField] + name: str = "" + + +class FormParser(HTMLParser): + """Parst HTML und extrahiert Formular-Informationen""" + + def __init__(self): + super().__init__() + self.forms = [] + self.current_form = None + self.in_form = False + + def handle_starttag(self, tag, attrs): + attrs_dict = dict(attrs) + + if tag == 'form': + self.in_form = True + self.current_form = Form( + action=attrs_dict.get('action', ''), + method=attrs_dict.get('method', 'GET').upper(), + fields=[], + name=attrs_dict.get('name', attrs_dict.get('id', f'form_{len(self.forms)}')) + ) + + elif self.in_form and tag in ['input', 'textarea', 'select']: + field = FormField( + name=attrs_dict.get('name', attrs_dict.get('id', f'field_{len(self.current_form.fields)}')), + field_type=attrs_dict.get('type', tag), + required='required' in attrs_dict, + pattern=attrs_dict.get('pattern', ''), + placeholder=attrs_dict.get('placeholder', ''), + value=attrs_dict.get('value', '') + ) + self.current_form.fields.append(field) + + def handle_endtag(self, tag): + if tag == 'form' and self.in_form: + self.in_form = False + if self.current_form: + self.forms.append(self.current_form) + self.current_form = None + + +class SimpleFormTester: + """ + Vereinfachter Formular-Tester ohne Browser-Dependencies. + Führt HTTP-basierte Tests durch. + """ + + def __init__(self, base_url: str): + self.base_url = base_url.rstrip('/') + self.session_cookies = {} + + def fetch_page(self, path: str) -> str: + """Lädt eine Seite und gibt den HTML-Inhalt zurück""" + url = urljoin(self.base_url, path) + + try: + request = urllib.request.Request(url) + + # Cookies hinzufügen + if self.session_cookies: + cookie_header = '; '.join([f'{k}={v}' for k, v in self.session_cookies.items()]) + request.add_header('Cookie', cookie_header) + + with urllib.request.urlopen(request) as response: + html = response.read().decode('utf-8') + + # Cookies aus Response extrahieren + cookie_header = response.getheader('Set-Cookie') + if cookie_header: + self._parse_cookies(cookie_header) + + return html + + except urllib.error.URLError as e: + print(f"❌ Fehler beim Laden von {url}: {e}") + return "" + + def _parse_cookies(self, cookie_header: str): + """Parst Set-Cookie Header""" + for cookie in cookie_header.split(','): + if '=' in cookie: + name, value = cookie.split('=', 1) + self.session_cookies[name.strip()] = value.split(';')[0].strip() + + def find_forms(self, html: str) -> List[Form]: + """Findet alle Formulare in HTML""" + parser = FormParser() + parser.feed(html) + return parser.forms + + def generate_test_data(self, field: FormField) -> str: + """Generiert Test-Daten für ein Feld""" + + if field.field_type == 'email': + return "test@mercedes-benz.com" + elif field.field_type == 'password': + return "TestPassword123!" + elif field.field_type == 'tel': + return "+49 711 17-0" + elif field.field_type == 'url': + return "https://www.mercedes-benz.com" + elif field.field_type == 'number': + return "42" + elif field.field_type == 'date': + return "2024-06-18" + elif field.field_type == 'checkbox': + return "on" + elif field.field_type == 'radio': + return field.value or "option1" + elif 'name' in field.name.lower(): + return "Test Benutzer" + elif 'username' in field.name.lower(): + return "admin" + elif 'ip' in field.name.lower(): + return "192.168.1.100" + elif 'port' in field.name.lower(): + return "80" + else: + return f"Test_{field.name}" + + def submit_form(self, form: Form, test_data: Dict[str, str], base_url: str) -> Dict: + """Sendet Formular-Daten und gibt Response zurück""" + + # Action URL bestimmen + if form.action.startswith('http'): + url = form.action + else: + url = urljoin(base_url, form.action) + + # Daten vorbereiten + form_data = {} + for field in form.fields: + if field.name in test_data: + form_data[field.name] = test_data[field.name] + elif field.field_type not in ['submit', 'button']: + form_data[field.name] = self.generate_test_data(field) + + try: + if form.method == 'GET': + # GET-Request mit Query-Parametern + query_string = urllib.parse.urlencode(form_data) + full_url = f"{url}?{query_string}" + request = urllib.request.Request(full_url) + else: + # POST-Request + data = urllib.parse.urlencode(form_data).encode('utf-8') + request = urllib.request.Request(url, data=data) + request.add_header('Content-Type', 'application/x-www-form-urlencoded') + + # Cookies hinzufügen + if self.session_cookies: + cookie_header = '; '.join([f'{k}={v}' for k, v in self.session_cookies.items()]) + request.add_header('Cookie', cookie_header) + + with urllib.request.urlopen(request) as response: + response_html = response.read().decode('utf-8') + status_code = response.getcode() + + return { + 'success': 200 <= status_code < 400, + 'status_code': status_code, + 'html': response_html, + 'form_data': form_data + } + + except urllib.error.HTTPError as e: + return { + 'success': False, + 'status_code': e.code, + 'error': str(e), + 'form_data': form_data + } + except Exception as e: + return { + 'success': False, + 'error': str(e), + 'form_data': form_data + } + + def validate_field(self, field: FormField, value: str) -> Dict: + """Validiert ein Feld mit gegebenem Wert""" + + errors = [] + + # Required-Validierung + if field.required and not value.strip(): + errors.append(f"Feld '{field.name}' ist erforderlich") + + # Pattern-Validierung + if field.pattern and value: + try: + if not re.match(field.pattern, value): + errors.append(f"Feld '{field.name}' entspricht nicht dem Pattern '{field.pattern}'") + except re.error: + errors.append(f"Ungültiges Pattern für Feld '{field.name}': {field.pattern}") + + # Type-spezifische Validierung + if value and field.field_type == 'email': + email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + if not re.match(email_pattern, value): + errors.append(f"Ungültige Email-Adresse: {value}") + + elif value and field.field_type == 'url': + try: + parsed = urlparse(value) + if not parsed.scheme or not parsed.netloc: + errors.append(f"Ungültige URL: {value}") + except: + errors.append(f"Ungültige URL: {value}") + + elif value and field.field_type == 'number': + try: + float(value) + except ValueError: + errors.append(f"'{value}' ist keine gültige Zahl") + + return { + 'valid': len(errors) == 0, + 'errors': errors, + 'field': field.name, + 'value': value + } + + def test_form_validation(self, form: Form) -> List[Dict]: + """Testet Formular-Validierung mit verschiedenen Daten""" + + validation_results = [] + + # 1. Test mit leeren Required-Feldern + for field in form.fields: + if field.required: + result = self.validate_field(field, "") + validation_results.append({ + 'test_type': 'required_field_empty', + 'field': field.name, + 'expected_invalid': True, + 'actual_valid': result['valid'], + 'passed': not result['valid'], # Sollte ungültig sein + 'errors': result['errors'] + }) + + # 2. Test mit ungültigen Email-Adressen + for field in form.fields: + if field.field_type == 'email': + invalid_emails = ['invalid-email', 'test@', '@domain.com', 'test..test@domain.com'] + for invalid_email in invalid_emails: + result = self.validate_field(field, invalid_email) + validation_results.append({ + 'test_type': 'invalid_email', + 'field': field.name, + 'value': invalid_email, + 'expected_invalid': True, + 'actual_valid': result['valid'], + 'passed': not result['valid'], + 'errors': result['errors'] + }) + + # 3. Test mit gültigen Daten + valid_test_data = {} + for field in form.fields: + if field.field_type not in ['submit', 'button']: + valid_test_data[field.name] = self.generate_test_data(field) + + for field in form.fields: + if field.name in valid_test_data: + result = self.validate_field(field, valid_test_data[field.name]) + validation_results.append({ + 'test_type': 'valid_data', + 'field': field.name, + 'value': valid_test_data[field.name], + 'expected_invalid': False, + 'actual_valid': result['valid'], + 'passed': result['valid'], + 'errors': result['errors'] + }) + + return validation_results + + def test_form_submission(self, form: Form, base_url: str) -> Dict: + """Testet Formular-Submission""" + + print(f"🧪 Teste Formular: {form.name} ({form.method} → {form.action})") + + # Test-Daten generieren + test_data = {} + for field in form.fields: + if field.field_type not in ['submit', 'button']: + test_data[field.name] = self.generate_test_data(field) + + print(f" 📝 Test-Daten: {list(test_data.keys())}") + + # Formular senden + start_time = time.time() + result = self.submit_form(form, test_data, base_url) + execution_time = time.time() - start_time + + # Validierungs-Tests + validation_results = self.test_form_validation(form) + + return { + 'form_name': form.name, + 'method': form.method, + 'action': form.action, + 'fields_count': len(form.fields), + 'test_data': test_data, + 'submission_result': result, + 'validation_results': validation_results, + 'execution_time': execution_time, + 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S') + } + + def test_page_forms(self, path: str) -> List[Dict]: + """Testet alle Formulare auf einer Seite""" + + print(f"🔍 Scanne Formulare auf: {self.base_url}{path}") + + # Seite laden + html = self.fetch_page(path) + if not html: + return [] + + # Formulare finden + forms = self.find_forms(html) + print(f"✅ {len(forms)} Formulare gefunden") + + # Jedes Formular testen + results = [] + for form in forms: + result = self.test_form_submission(form, self.base_url + path) + results.append(result) + + return results + + def generate_report(self, test_results: List[Dict], output_file: str = "simple_form_test_report.html"): + """Generiert einen einfachen HTML-Report""" + + total_forms = len(test_results) + successful_submissions = len([r for r in test_results if r['submission_result'].get('success', False)]) + + # Validierungs-Statistiken + total_validations = sum(len(r['validation_results']) for r in test_results) + passed_validations = sum(len([v for v in r['validation_results'] if v['passed']]) for r in test_results) + + html_report = f""" + + + + + MYP Formular Test Report + + + +
+
+

🧪 MYP Formular Test Report

+

Generiert am {time.strftime('%d.%m.%Y um %H:%M:%S')}

+
+ +
+
+
{total_forms}
+
Formulare getestet
+
+
+
{successful_submissions}
+
Erfolgreiche Submissions
+
+
+
{total_validations}
+
Validierungs-Tests
+
+
+
{passed_validations}
+
Validierungen bestanden
+
+
+ +

📋 Detaillierte Ergebnisse

+""" + + # Formular-spezifische Ergebnisse + for result in test_results: + submission = result['submission_result'] + success_class = 'success' if submission.get('success', False) else 'failure' + + html_report += f""" +
+
+

📝 {result['form_name']}

+

Method: {result['method']} | Action: {result['action']} | Felder: {result['fields_count']}

+
+
+

Submission-Ergebnis: {'✅ Erfolgreich' if submission.get('success', False) else '❌ Fehlgeschlagen'}

+ + {f'

Status Code: {submission.get("status_code", "N/A")}

' if 'status_code' in submission else ''} + {f'

Fehler: {submission.get("error", "N/A")}

' if 'error' in submission else ''} + +

Test-Daten:

+
{json.dumps(result['test_data'], indent=2, ensure_ascii=False)}
+ +

Validierungs-Ergebnisse:

+""" + + # Validierungs-Ergebnisse + for validation in result['validation_results']: + validation_class = 'validation-passed' if validation['passed'] else 'validation-failed' + status_icon = '✅' if validation['passed'] else '❌' + + html_report += f""" +
+ {status_icon} {validation['test_type']} - Feld: {validation['field']} + {f"
Wert: {validation.get('value', 'N/A')}" if 'value' in validation else ''} + {f"
Fehler: {', '.join(validation['errors'])}" if validation['errors'] else ''} +
+""" + + html_report += f""" +

⏱️ Ausführungszeit: {result['execution_time']:.2f}s

+
+
+""" + + html_report += """ +
+ +""" + + try: + with open(output_file, 'w', encoding='utf-8') as f: + f.write(html_report) + print(f"📊 Report erstellt: {output_file}") + return output_file + except Exception as e: + print(f"❌ Fehler beim Report-Erstellen: {e}") + return "" + + +def main(): + """Haupt-CLI-Interface""" + + if len(sys.argv) < 2: + print(""" +🧪 Vereinfachter MYP Formular Tester + +Verwendung: + python simple_form_tester.py [path] + +Beispiele: + python simple_form_tester.py http://localhost:5000 + python simple_form_tester.py http://localhost:5000 /login + python simple_form_tester.py http://localhost:5000 /admin/add_printer + +Testet alle Formulare auf der angegebenen Seite und generiert einen HTML-Report. + """) + return + + base_url = sys.argv[1] + path = sys.argv[2] if len(sys.argv) > 2 else '/' + + print(f""" +{'='*60} +🧪 MYP FORMULAR TESTER (Vereinfacht) +{'='*60} + +🎯 Basis-URL: {base_url} +📍 Pfad: {path} +🕒 Gestartet: {time.strftime('%d.%m.%Y um %H:%M:%S')} + +{'='*60} +""") + + # Tester initialisieren + tester = SimpleFormTester(base_url) + + # Tests ausführen + try: + results = tester.test_page_forms(path) + + if results: + # Report generieren + report_file = tester.generate_report(results) + + # Zusammenfassung + total_forms = len(results) + successful = len([r for r in results if r['submission_result'].get('success', False)]) + + print(f""" +{'='*60} +🎉 TESTS ABGESCHLOSSEN +{'='*60} + +📊 ZUSAMMENFASSUNG: + • Formulare getestet: {total_forms} + • Erfolgreiche Submissions: {successful} + • Erfolgsrate: {successful/total_forms*100:.1f}% + +📋 Report: {report_file} + +{'='*60} +""") + else: + print("⚠️ Keine Formulare gefunden oder Fehler beim Laden der Seite") + + except Exception as e: + print(f"❌ Fehler beim Testen: {e}") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backend/templates/base.html b/backend/templates/base.html index 026a2a22c..37ab89d36 100644 --- a/backend/templates/base.html +++ b/backend/templates/base.html @@ -336,11 +336,13 @@ System-Info
- +
+ {{ csrf_token() }} + +
@@ -438,16 +440,11 @@

{{ message }}

- - {% endfor %} {% endif %} @@ -727,12 +724,11 @@ const payload = JSON.parse(notification.payload || '{}'); return `
- -
@@ -748,10 +744,7 @@ // Event Listeners werden über onclick direkt gesetzt } - async viewGuestRequest(requestId) { - // Weiterleitung zur Admin-Gastanfragen-Seite - window.location.href = `/admin/guest-requests?highlight=${requestId}`; - } + // Replaced by direct links in templates async markAsRead(notificationId) { try { @@ -862,26 +855,19 @@ e.stopPropagation(); }); - // Logout Handler - function handleLogout() { - if (confirm('Möchten Sie sich wirklich abmelden?')) { - const form = document.createElement('form'); - form.method = 'POST'; - form.action = '{{ url_for("auth.logout") }}'; - - const csrfToken = document.querySelector('meta[name="csrf-token"]'); - if (csrfToken) { - const input = document.createElement('input'); - input.type = 'hidden'; - input.name = 'csrf_token'; - input.value = csrfToken.getAttribute('content'); - form.appendChild(input); - } - - document.body.appendChild(form); - form.submit(); - } - } + // Flash-Message close buttons + document.querySelectorAll('.close-flash-btn').forEach(btn => { + btn.addEventListener('click', function() { + this.parentElement.parentElement.remove(); + }); + }); + + // Auto-hide flash messages after 5 seconds + document.querySelectorAll('[id^="flash-"]').forEach((flash, index) => { + setTimeout(() => { + flash?.remove(); + }, 5000 + (index * 500)); // Staggered removal + }); // Smooth scroll for anchor links document.querySelectorAll('a[href^="#"]').forEach(anchor => { diff --git a/backend/templates/dashboard.html b/backend/templates/dashboard.html index ce319dbc5..57b00172f 100644 --- a/backend/templates/dashboard.html +++ b/backend/templates/dashboard.html @@ -269,13 +269,13 @@
- + @@ -401,7 +401,7 @@
{{ job.progress }}%
-
Details + Details {% endfor %} @@ -499,454 +499,51 @@ {% block extra_js %} {% endblock %} \ No newline at end of file diff --git a/backend/templates/macros/ui_components.html b/backend/templates/macros/ui_components.html new file mode 100644 index 000000000..ac5e130aa --- /dev/null +++ b/backend/templates/macros/ui_components.html @@ -0,0 +1,183 @@ +{# Jinja-Makros für UI-Komponenten zur JavaScript-Ersetzung #} + +{# Status-Indikator mit CSS-Animation #} +{% macro status_indicator(status, text="") %} +{% set status_classes = { + 'online': 'mb-status-online', + 'offline': 'mb-status-offline', + 'busy': 'mb-status-busy', + 'idle': 'mb-status-idle', + 'running': 'mb-status-busy', + 'completed': 'mb-status-online', + 'failed': 'mb-status-offline', + 'paused': 'mb-status-idle', + 'queued': 'mb-status-idle' +} %} +
+
+ {% if text %} + {{ text }} + {% endif %} +
+{% endmacro %} + +{# Fortschrittsbalken mit CSS-Animation #} +{% macro progress_bar(progress, show_text=True) %} +
+
+
+{% if show_text %} +
{{ progress }}%
+{% endif %} +{% endmacro %} + +{# Klickbare Karte #} +{% macro clickable_card(url, class="dashboard-card p-6") %} + + {{ caller() }} + +{% endmacro %} + +{# Tab-Navigation mit serverseitiger Logik #} +{% macro tab_navigation(tabs, active_tab) %} +
+ +
+{% endmacro %} + +{# Drucker-Status-Karte ohne JavaScript #} +{% macro printer_card(printer, show_link=True) %} +{% if show_link %} + +{% endif %} + +{% if show_link %} + +{% endif %} +{% endmacro %} + +{# Job-Zeile in Tabelle ohne JavaScript #} +{% macro job_row(job, show_link=True) %} + + {% if show_link %} + + + {{ job_row_content(job) }} + + + {% else %} + {{ job_row_content(job) }} + {% endif %} + +{% endmacro %} + +{# Job-Zeilen-Inhalt (für Wiederverwendung) #} +{% macro job_row_content(job) %} + + {{ status_indicator(job.status, job.status_text) }} + + +
{{ job.name }}
+
{{ job.file_name }}
+ + +
{{ job.printer }}
+ + +
{{ job.start_time }}
+ + + {{ progress_bar(job.progress) }} + + + Details + +{% endmacro %} + +{# Benachrichtigungs-Toast mit Auto-Close via CSS #} +{% macro notification_toast(message, type="info", auto_close=True) %} +{% set type_classes = { + 'success': 'border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-300', + 'error': 'border-red-200 bg-red-50 text-red-800 dark:border-red-800 dark:bg-red-900/20 dark:text-red-300', + 'warning': 'border-yellow-200 bg-yellow-50 text-yellow-800 dark:border-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300', + 'info': 'border-blue-200 bg-blue-50 text-blue-800 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-300' +} %} +
+
+ +
+

{{ message }}

+
+
+ {{ csrf_token() }} + +
+
+
+{% endmacro %} + +{# Formular-Submit-Button mit Loading-State #} +{% macro submit_button(text, loading_text="Wird verarbeitet...", class="btn-primary") %} + + +{% endmacro %} + +{# Auto-Refresh Meta-Tag für periodische Seitenaktualisierung #} +{% macro auto_refresh(seconds) %} + +{% endmacro %} + +{# CSS-only Dropdown-Menu #} +{% macro css_dropdown(button_text, items, button_class="btn-secondary") %} +
+ + +
+{% endmacro %} \ No newline at end of file