This commit is contained in:
2026-04-24 13:01:16 +02:00
parent 047ac6e239
commit 65a60356dd
12 changed files with 635 additions and 72 deletions
+68
View File
@@ -0,0 +1,68 @@
import sys
from pathlib import Path
import pytest
from werkzeug.security import generate_password_hash
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from app import create_app, db
from app.models.user import User
@pytest.fixture
def app_instance(tmp_path):
db_path = Path(tmp_path) / "test_app.db"
app = create_app(
{
"TESTING": True,
"SECRET_KEY": "test-secret-key",
"SQLALCHEMY_DATABASE_URI": f"sqlite:///{db_path}",
}
)
with app.app_context():
db.drop_all()
db.create_all()
yield app
with app.app_context():
db.session.remove()
db.drop_all()
@pytest.fixture
def client(app_instance):
return app_instance.test_client()
@pytest.fixture
def user_factory(app_instance):
def _create_user(username="alice", password="Password123!", **extra_fields):
with app_instance.app_context():
user = User(
username=username,
password=generate_password_hash(password),
**extra_fields,
)
db.session.add(user)
db.session.commit()
return user.id
return _create_user
@pytest.fixture
def login_as(client):
def _login(username="alice", password="Password123!"):
return client.post(
"/login",
data={"username": username, "password": password},
follow_redirects=False,
)
return _login
+61
View File
@@ -0,0 +1,61 @@
# Testdurchführung
## Durchführungsrahmen
- Datum: 2026-04-24
- Ausführung auf zwei Ebenen:
- Unit-Tests
- Integrations-/Routentests
- Werkzeug: `pytest 8.3.4`
- Testkommando:
```bash
PYTHONDONTWRITEBYTECODE=1 /tmp/acht-damen-test-venv/bin/pytest -q test
```
## Bewertung der durchgeführten Testfälle
| ID | Testfall | Ebene | Erwartung | Resultat | Bewertung | Nachvollziehbare Bewertung |
| --- | --- | --- | --- | --- | --- | --- |
| U-MAIN-01 | Brettgrösse wird begrenzt | Unit | Grenzwerte werden korrekt behandelt | Bestanden | OK | Kleine, grosse und ungültige Werte wurden korrekt auf den erlaubten Bereich abgebildet |
| U-MAIN-02 | Favoriten-Flag wird erkannt | Unit | Typische truthy Werte werden korrekt erkannt | Bestanden | OK | Die Hilfsfunktion unterschied erfolgreich zwischen gültigen truthy und anderen Werten |
| U-MAIN-03 | Nicht-negative Ganzzahlen werden abgesichert | Unit | Negative und ungültige Werte werden abgefangen | Bestanden | OK | Negative Werte wurden auf 0 begrenzt, ungültige Eingaben fielen auf den Default zurück |
| U-MAIN-04 | Legacy-Brett wird umgewandelt | Unit | Positionsliste wird zur Matrix | Bestanden | OK | Altes Speicherformat konnte verlustfrei in das aktülle Format übernommen werden |
| U-MAIN-05 | Matrixwerte werden normalisiert | Unit | Nur 0/1 bleiben übrig | Bestanden | OK | Truthy- und falsy-Werte wurden korrekt vereinheitlicht |
| U-MAIN-06 | Damen-Solver für 4x4 | Unit | Gültige Standardlösungen werden gefunden | Bestanden | OK | Der Solver lieferte die fachlich erwarteten zwei 4x4-Lösungen |
| U-MAIN-07 | Turm-Trace ist vollständig | Unit | Pro Reihe ein Schritt plus Abschluss | Bestanden | OK | Der Trace enthielt die erwartete Struktur für das Frontend |
| U-LOGIN-01 | Gültiger Username | Unit | Gültiger Name wird akzeptiert | Bestanden | OK | Ein Name im erlaubten Format wurde ohne Fehlermeldung akzeptiert |
| U-LOGIN-02 | Ungültiger Username | Unit | Fehlerhafte Zeichen werden abgelehnt | Bestanden | OK | Ein Name mit Leerzeichen/Sonderzeichen wurde korrekt abgewiesen |
| U-LOGIN-03 | Zu kurzes Passwort | Unit | Mindestlänge wird erzwungen | Bestanden | OK | Die Funktion lieferte die passende Fehlermeldung für zu kurze Passwörter |
| U-LOGIN-04 | Zu langes Passwort | Unit | Maximallänge wird erzwungen | Bestanden | OK | Auch überlange Passwörter wurden korrekt abgefangen |
| U-LOGIN-05 | 2FA-Secret-Normalisierung | Unit | Secret wird korrekt bereinigt und formatiert | Bestanden | OK | Schreibweisen mit Leerzeichen/Kleinschreibung wurden korrekt vereinheitlicht |
| U-LOGIN-06 | TOTP zur gleichen Zeit | Unit | Richtiger Code ist gültig | Bestanden | OK | Erzeugter Code wurde bei identischem Zeitpunkt akzeptiert |
| U-LOGIN-07 | TOTP mit Zeitfenster | Unit | Code bleibt im gültigen Nachbarfenster gültig | Bestanden | OK | Das erlaubte Zeitfenster funktionierte wie vorgesehen |
| U-LOGIN-08 | Fehlerhafte TOTP-Eingabe | Unit | Falsch formatierte Codes werden abgelehnt | Bestanden | OK | Nicht numerische oder zu kurze Eingaben wurden nicht akzeptiert |
| I-APP-01 | Dashboard ohne Login | Integration | Weiterleitung auf Login | Bestanden | OK | Die geschützte Route war ohne Authentifizierung nicht direkt erreichbar |
| I-APP-02 | Registrierung | Integration | Benutzer wird erstellt und umgeleitet | Bestanden | OK | Nach dem POST war der Benutzer in der Testdatenbank vorhanden |
| I-APP-03 | Login bei gesperrter IP | Integration | Sperrmeldung statt Login | Bestanden | OK | Die Anti-Brute-Force-Logik blockierte den Login im gesperrten Zeitraum korrekt |
| I-APP-04 | Dame vs. Turm bei Diagonale | Integration | Dame ungültig, Turm gültig | Bestanden | OK | Die Route `/check` setzte die unterschiedlichen Fachregeln korrekt um |
| I-APP-05 | Speichern und Laden | Integration | Spielstand bleibt konsistent | Bestanden | OK | Gespeicherter Spielstand konnte mit denselben Daten wieder geladen werden |
| I-APP-06 | Favoriten filtern | Integration | Nur markierte Save-Points erscheinen im Filter | Bestanden | OK | Das Umschalten des Favoritenstatus wirkte sich korrekt auf die Filterliste aus |
## Ergebnis
- Anzahl Testebenen: 2
- Anzahl dokumentierter Testfälle: 21
- Bestandene Testfälle: 21
- Nicht bestandene Testfälle: 0
Die Testdurchführung zeigt, dass die zentrale Fachlogik, die
Authentifizierungslogik und die wichtigsten Datenflüsse der Anwendung für die
Abgabe nachvollziehbar und erfolgreich überprüft wurden.
## Hinweis zu Warnungen
Beim Testlauf traten nur Warnungen auf, aber keine Fehler:
- DeprecationWarnings zu `datetime.utcnow()` in bestehendem Anwendungscode
- Pytest-Cache-Warnungen wegen schreibgeschütztem Dateisystem im Projektpfad
Diese Warnungen haben das Ergebnis der Testfälle nicht beeinflusst. Alle
geplanten und ausgeführten Tests wurden bestanden.
+158
View File
@@ -0,0 +1,158 @@
from datetime import timedelta
from app import db
from app.models.gamestate import GameState
from app.models.login_attempt import LoginAttempt
from app.models.user import User
from app.routes.login_routes import utcnow
def test_dashboard_requires_login(client):
response = client.get("/dashboard")
assert response.status_code == 302
assert "/login" in response.headers["Location"]
def test_register_creates_user_and_redirects_to_login(client, app_instance):
response = client.post(
"/register",
data={"username": "newuser", "password": "Password123!"},
follow_redirects=False,
)
assert response.status_code == 302
assert "/login" in response.headers["Location"]
with app_instance.app_context():
saved_user = User.query.filter_by(username="newuser").first()
assert saved_user is not None
def test_login_blocks_requests_from_a_locked_ip(client, app_instance, user_factory):
user_factory(username="lockeduser", password="Password123!")
with app_instance.app_context():
db.session.add(
LoginAttempt(
ip_address="203.0.113.10",
failed_attempts=4,
locked_until=utcnow() + timedelta(minutes=5),
)
)
db.session.commit()
response = client.post(
"/login",
data={"username": "lockeduser", "password": "Password123!"},
headers={"X-Forwarded-For": "203.0.113.10"},
)
assert response.status_code == 200
assert b"Too many failed login attempts" in response.data
def test_check_endpoint_applies_different_rules_for_queens_and_rooks(client):
board = [
[1, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
]
queens_response = client.post(
"/check",
json={"board": board, "row": 1, "col": 1, "mode": "queens"},
)
rooks_response = client.post(
"/check",
json={"board": board, "row": 1, "col": 1, "mode": "rooks"},
)
assert queens_response.status_code == 200
assert rooks_response.status_code == 200
assert queens_response.get_json() == {"valid": False}
assert rooks_response.get_json() == {"valid": True}
def test_authenticated_user_can_save_and_load_a_game_state(client, app_instance, user_factory, login_as):
user_factory(username="alice", password="Password123!")
login_response = login_as("alice", "Password123!")
assert login_response.status_code == 302
payload = {
"board": [
[0, 1, 0, 0],
[0, 0, 0, 1],
[1, 0, 0, 0],
[0, 0, 1, 0],
],
"mode": "queens",
"boardSize": 4,
"saveName": "test-save",
"saveNote": "unit integration save",
"isFavorite": False,
"stepCount": 7,
"elapsedSeconds": 42,
"isSolved": True,
}
save_response = client.post("/save", json=payload)
assert save_response.status_code == 200
saved_data = save_response.get_json()["save_point"]
load_response = client.get(f"/load/{saved_data['id']}")
assert load_response.status_code == 200
loaded_data = load_response.get_json()
assert loaded_data["board"] == payload["board"]
assert loaded_data["mode"] == payload["mode"]
assert loaded_data["step_count"] == payload["stepCount"]
assert loaded_data["elapsed_seconds"] == payload["elapsedSeconds"]
assert loaded_data["is_solved"] is True
with app_instance.app_context():
assert GameState.query.count() == 1
def test_favorite_toggle_changes_filtered_save_point_list(client, user_factory, login_as):
user_factory(username="favuser", password="Password123!")
login_response = login_as("favuser", "Password123!")
assert login_response.status_code == 302
save_response = client.post(
"/save",
json={
"board": [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
],
"mode": "rooks",
"boardSize": 4,
"saveName": "favorite-candidate",
"saveNote": "",
"isFavorite": False,
"stepCount": 2,
"elapsedSeconds": 5,
"isSolved": False,
},
)
save_id = save_response.get_json()["save_point"]["id"]
filtered_before = client.get("/save_points?favorites_only=true")
assert filtered_before.status_code == 200
assert filtered_before.get_json() == []
toggle_response = client.post(f"/save_points/{save_id}/favorite")
assert toggle_response.status_code == 200
assert toggle_response.get_json()["is_favorite"] is True
filtered_after = client.get("/save_points?favorites_only=true")
assert filtered_after.status_code == 200
assert len(filtered_after.get_json()) == 1
assert filtered_after.get_json()[0]["id"] == save_id
+57
View File
@@ -0,0 +1,57 @@
from datetime import datetime, timedelta, timezone
from app.routes.login_routes import (
format_two_factor_secret,
generate_totp_code,
normalize_two_factor_secret,
validate_password,
validate_username,
verify_totp_code,
)
def test_validate_username_accepts_supported_format():
assert validate_username("valid_user-01") is None
def test_validate_username_rejects_unsupported_characters():
assert validate_username("bad user!") is not None
def test_validate_password_rejects_too_short_passwords():
assert validate_password("short") == "Password must be at least 8 characters long."
def test_validate_password_rejects_too_long_passwords():
assert validate_password("a" * 129) == "Password must be at most 128 characters long."
def test_normalize_and_format_two_factor_secret_prepare_manual_input():
secret = "jbsw y3dp ehpk 3pxp"
assert normalize_two_factor_secret(secret) == "JBSWY3DPEHPK3PXP"
assert format_two_factor_secret(secret) == "JBSW Y3DP EHPK 3PXP"
def test_verify_totp_code_accepts_code_generated_for_same_time():
secret = "JBSWY3DPEHPK3PXP"
moment = datetime(2026, 4, 24, 12, 0, 0, tzinfo=timezone.utc)
code = generate_totp_code(secret, moment)
assert verify_totp_code(secret, code, moment) is True
def test_verify_totp_code_accepts_code_in_adjacent_time_window():
secret = "JBSWY3DPEHPK3PXP"
moment = datetime(2026, 4, 24, 12, 0, 0, tzinfo=timezone.utc)
code = generate_totp_code(secret, moment)
assert verify_totp_code(secret, code, moment + timedelta(seconds=30)) is True
def test_verify_totp_code_rejects_malformed_code_input():
secret = "JBSWY3DPEHPK3PXP"
moment = datetime(2026, 4, 24, 12, 0, 0, tzinfo=timezone.utc)
assert verify_totp_code(secret, "12ab56", moment) is False
assert verify_totp_code(secret, "12345", moment) is False
+65
View File
@@ -0,0 +1,65 @@
from app.routes.main_routes import (
build_rooks_trace,
count_placed_pieces,
normalize_board,
parse_board_size,
parse_favorite_flag,
parse_non_negative_int,
solve_queens,
)
def test_parse_board_size_clamps_small_large_and_invalid_values():
assert parse_board_size("2") == 4
assert parse_board_size("99") == 13
assert parse_board_size("abc") == 8
def test_parse_favorite_flag_accepts_common_truthy_values():
assert parse_favorite_flag("true") is True
assert parse_favorite_flag("1") is True
assert parse_favorite_flag("on") is True
assert parse_favorite_flag("false") is False
def test_parse_non_negative_int_clamps_negative_and_invalid_values():
assert parse_non_negative_int("-7") == 0
assert parse_non_negative_int("15") == 15
assert parse_non_negative_int("abc", default=3) == 3
def test_normalize_board_converts_legacy_position_list_into_matrix():
normalized = normalize_board([1, 3, 0, 2], 4)
assert normalized == [
[0, 1, 0, 0],
[0, 0, 0, 1],
[1, 0, 0, 0],
[0, 0, 1, 0],
]
def test_normalize_board_reduces_matrix_values_to_zero_or_one():
normalized = normalize_board([[0, 2], [True, None]], 2)
assert normalized == [[0, 1], [1, 0]]
def test_solve_queens_returns_the_two_expected_solutions_for_4x4():
solutions = {tuple(solution) for solution in solve_queens(4)}
assert solutions == {
(1, 3, 0, 2),
(2, 0, 3, 1),
}
def test_build_rooks_trace_contains_one_placement_per_row_plus_solution():
trace = build_rooks_trace(5)
placement_steps = [step for step in trace["steps"] if step["type"] == "place"]
assert trace["solved"] is True
assert len(trace["steps"]) == 6
assert len(placement_steps) == 5
assert sorted(step["col"] for step in placement_steps) == [0, 1, 2, 3, 4]
assert count_placed_pieces(trace["steps"][-1]["board"]) == 5
+83
View File
@@ -0,0 +1,83 @@
# Testkonzept
## Ziel
Dieses Testkonzept beschreibt, wie die wichtigsten Funktionen der Webanwendung
vor der Abgabe ueberprueft werden. Der Fokus liegt auf den fachlich und
technisch kritischen Bereichen:
- Brettlogik fuer Damen und Tuerme
- Authentifizierung und Sicherheitsfunktionen
- Speichern, Laden und Filtern von Spielständen
## Randbedingungen / Umfeld
- Projekt: 8-Damen- und 8-Tuerme Solver
- Testdatum Planung: 2026-04-24
- Betriebssystem: Linux 6.6.87.2-microsoft-standard-WSL2 x86_64
- Python-Version: 3.12.3
- Testwerkzeug: pytest 8.3.4
- Webtestmittel: Flask Test Client
- Datenbank im Test: isolierte SQLite-Testdatenbank pro Testlauf
- Testordner: `test/`
Die Tests werden absichtlich getrennt von der produktiven Datenbank
durchgefuehrt. Dadurch wird verhindert, dass echte Benutzerdaten oder
Spielstände verändert werden.
## Testmittel und Methoden
Es werden zwei voneinander unabhängige Testebenen eingesetzt:
1. Unit-Tests
Diese prüfen einzelne Funktionen direkt, zum Beispiel Eingabevalidierung,
Brettnormalisierung oder TOTP-Prüfung.
2. Integrations- und Routentests
Diese prüfen das Zusammenspiel mehrerer Komponenten über HTTP-Routen, zum
Beispiel Registrierung, Login, Validierung, Speichern und Laden.
Eingesetzte Mittel:
- `pytest` für die Testausführung und Bewertung
- Flask Test Client für Request/Response-Tests
- temporäre SQLite-Datenbank für isolierte Testdaten
## Testfälle (Drehbuch)
| ID | Ebene | Testziel | Testmethode / Eingabe | Erwartetes Resultat |
| --- | --- | --- | --- | --- |
| U-MAIN-01 | Unit | Brettgroesse absichern | `parse_board_size()` mit zu kleinen, zu grossen und ungültigen Werten | Werte werden korrekt auf den erlaubten Bereich begrenzt |
| U-MAIN-02 | Unit | Favoriten-Flag normalisieren | `parse_favorite_flag()` mit typischen truthy/falsy Werten | Nur definierte truthy Werte werden als `True` interpretiert |
| U-MAIN-03 | Unit | Ganze Zahlen robust verarbeiten | `parse_non_negative_int()` mit negativen und ungültigen Werten | Negative Werte werden auf 0 begrenzt, ungültige Werte fallen auf den Default zurück |
| U-MAIN-04 | Unit | Alte Speicherformate unterstützen | `normalize_board()` mit Legacy-Positionsliste | Es entsteht ein korrektes Brettmatrix-Format |
| U-MAIN-05 | Unit | Uneinheitliche Matrixwerte normalisieren | `normalize_board()` mit truthy/falsy Werten | Es entstehen nur 0- und 1-Werte |
| U-MAIN-06 | Unit | Damen-Solver fachlich absichern | `solve_queens(4)` | Es werden genau die gültigen 4x4-Loesungen gefunden |
| U-MAIN-07 | Unit | Turm-Trace prüfen | `build_rooks_trace(5)` | Es gibt pro Reihe genau einen Platzierungsschritt plus Abschluss |
| U-LOGIN-01 | Unit | Gültigen Username akzeptieren | `validate_username()` mit gültigem Namen | Rückgabe ist `None` |
| U-LOGIN-02 | Unit | Ungültigen Username ablehnen | `validate_username()` mit unerlaubten Zeichen | Es wird eine Fehlermeldung geliefert |
| U-LOGIN-03 | Unit | Zu kurzes Passwort ablehnen | `validate_password()` mit zu kurzem Passwort | Es wird die Mindestlängen-Fehlermeldung geliefert |
| U-LOGIN-04 | Unit | Zu langes Passwort ablehnen | `validate_password()` mit zu langem Passwort | Es wird die Maximalfehler-Meldung geliefert |
| U-LOGIN-05 | Unit | 2FA-Secret vereinheitlichen | `normalize_two_factor_secret()` und `format_two_factor_secret()` | Secret wird korrekt bereinigt und formatiert |
| U-LOGIN-06 | Unit | TOTP im Normalfall prüfen | `generate_totp_code()` + `verify_totp_code()` zur gleichen Zeit | Gültiger Code wird akzeptiert |
| U-LOGIN-07 | Unit | TOTP mit Zeitfenster prüfen | Code wird mit 30 Sekunden Abweichung geprüft | Gültiger Code wird wegen Validierungsfenster akzeptiert |
| U-LOGIN-08 | Unit | Fehlerhafte TOTP-Eingabe ablehnen | `verify_totp_code()` mit falschem Format | Ungültige Codes werden abgelehnt |
| I-APP-01 | Integration | Schutz geschützter Seite prüfen | `GET /dashboard` ohne Login | Weiterleitung auf Login |
| I-APP-02 | Integration | Registrierung prüfen | `POST /register` mit gültigen Daten | Benutzer wird erstellt und Weiterleitung auf Login erfolgt |
| I-APP-03 | Integration | Anti-Brute-Force prüfen | `POST /login` mit gesperrter IP | Login wird mit Sperrmeldung abgelehnt |
| I-APP-04 | Integration | Fachregel für Brett prüfen | `POST /check` für Dame und Turm auf diagonalem Feld | Dame: ungültig, Turm: gültig |
| I-APP-05 | Integration | Speichern und Laden prüfen | Login, danach `POST /save` und `GET /load/<id>` | Spielstand wird korrekt gespeichert und identisch geladen |
| I-APP-06 | Integration | Favoritenlogik prüfen | Login, Save-Point speichern, Favorit toggeln, gefiltert abfragen | Filter liefert nur entsprechend markierte Save-Points |
## Erwartete Ergebnisse
- Alle Unit-Tests müssen erfolgreich durchlaufen.
- Alle Integrations-Tests müssen erfolgreich durchlaufen.
- Keine produktiven Daten dürfen verändert werden.
- Fehler müssen in `pytest` klar sichtbar sein.
- Die Suite umfasst 21 konkrete Pytest-Testfälle auf zwei Ebenen.
## Durchführung
Die geplanten Tests werden mit `pytest` aus dem Projektwurzelverzeichnis
ausgeführt. Die konkrete Bewertung und das Resultat werden in
`test/testkonzept.md` festgehalten.