import os import sys # Ensure project root is in sys.path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from fastapi.testclient import TestClient from app.main import app, create_auth_token, COOKIE_NAME from app.database import ( create_recipe_share, get_recipe_share_by_token, get_recipe_shares_for_recipe, delete_recipe_share, get_recipe ) def run_tests(): client = TestClient(app) recipe = get_recipe(1) assert recipe is not None, "Recipe ID 1 must exist for testing" recipe_id = recipe["id"] print("--- 1. Testing Database Share Creation & Expiry Logic ---") # Share for 24 hours s24 = create_recipe_share(recipe_id, expires_in_seconds=86400, label="WhatsApp Familie") assert s24["token"], "Must have generated token" assert s24["is_expired"] is False, "Should not be expired" assert "Noch 23" in s24["relative_status"] or "Noch 24" in s24["relative_status"] or "Noch 1 Tag" in s24["relative_status"] print(f"[OK] 24h share created: {s24['token']} -> {s24['relative_status']}") # Unlimited share sunlim = create_recipe_share(recipe_id, expires_in_seconds=None, label="Dauerhaft") assert sunlim["expires_at"] is None, "Unlimited share should have None expires_at" assert sunlim["relative_status"] == "Unbegrenzt gültig" print(f"[OK] Unlimited share created: {sunlim['token']} -> {sunlim['relative_status']}") print("\n--- 2. Testing Public Access to Shared Recipe (No Auth Cookie) ---") resp = client.get(f"/shared/recipe/{s24['token']}") assert resp.status_code == 200, f"Expected 200 OK for public share link, got {resp.status_code}" assert recipe["title"] in resp.text, "Shared page must contain recipe title" assert "recipe-shared-banner" in resp.text, "Must render shared guest banner" assert "Gastansicht" in resp.text, "Must have guest badge" assert "Kochmodus" in resp.text, "Guest must have access to Kochmodus" # Ensure private admin buttons are hidden in guest view assert "Alle Rezepte" not in resp.text or "cb-nav-actions" not in resp.text or "/logout" not in resp.text print("[OK] Public access without auth verified: Recipe renders in guest view with Kochmodus!") # Check view count incremented fetched = get_recipe_share_by_token(s24["token"]) assert fetched["views_count"] >= 1, f"Views count should be >= 1, got {fetched['views_count']}" print(f"[OK] View count incremented: {fetched['views_count']} views recorded") print("\n--- 3. Testing Non-Existent & Expired Links ---") resp_invalid = client.get("/shared/recipe/does_not_exist_xyz123") assert resp_invalid.status_code == 404, f"Expected 404, got {resp_invalid.status_code}" assert "Link nicht mehr" in resp_invalid.text or "ungültig" in resp_invalid.text print("[OK] Invalid token handled with friendly 404 page") # Expired token test (past expiry) from datetime import datetime, timedelta from app.database import execute_query expired_token = "test_expired_token_123" past_date = (datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d %H:%M:%S") execute_query("DELETE FROM recipe_shares WHERE token = ?", (expired_token,), fetch="none", commit=True) execute_query("INSERT INTO recipe_shares (recipe_id, token, expires_at, label) VALUES (?, ?, ?, ?)", (recipe_id, expired_token, past_date, "Old"), fetch="none", commit=True) resp_exp = client.get(f"/shared/recipe/{expired_token}") assert resp_exp.status_code == 410, f"Expected 410 Gone for expired link, got {resp_exp.status_code}" assert "abgelaufen" in resp_exp.text.lower() print("[OK] Expired link correctly returns 410 Gone with expiration notice") print("\n--- 4. Testing Authenticated Share Management APIs ---") # Anonymous request to API must fail resp_unauth = client.get(f"/api/recipes/{recipe_id}/shares") assert resp_unauth.status_code == 401, f"Expected 401 Unauthorized for API without cookie, got {resp_unauth.status_code}" print("[OK] Unauthenticated API request correctly rejected with 401") # Authenticated request auth_token = create_auth_token("admin", days=1) client.cookies.set(COOKIE_NAME, auth_token) # List shares resp_list = client.get(f"/api/recipes/{recipe_id}/shares") assert resp_list.status_code == 200, f"Expected 200, got {resp_list.status_code}" shares = resp_list.json()["shares"] assert any(s["token"] == s24["token"] for s in shares) print(f"[OK] Authenticated GET /api/recipes/{recipe_id}/shares returned {len(shares)} shares") # Create new share via API resp_create = client.post( f"/api/recipes/{recipe_id}/shares", json={"expires_in": "3d", "label": "Kollege"} ) assert resp_create.status_code == 200, f"Expected 200, got {resp_create.status_code}" created_share = resp_create.json()["share"] assert created_share["token"] assert "share_url" in created_share assert "Noch 2" in created_share["relative_status"] or "Noch 3" in created_share["relative_status"] print(f"[OK] POST /api/recipes/{recipe_id}/shares created: {created_share['share_url']}") # Delete share via API resp_del = client.delete(f"/api/recipes/{recipe_id}/shares/{created_share['id']}") assert resp_del.status_code == 200, f"Expected 200, got {resp_del.status_code}" # Verify it is no longer accessible resp_after_del = client.get(f"/shared/recipe/{created_share['token']}") assert resp_after_del.status_code in (404, 410) print("[OK] DELETE /api/recipes/{recipe_id}/shares/{share_id} deleted and revoked access") # Clean up test shares delete_recipe_share(s24["id"], recipe_id) delete_recipe_share(sunlim["id"], recipe_id) execute_query("DELETE FROM recipe_shares WHERE token = ?", (expired_token,), fetch="none", commit=True) print("[OK] Test data cleanup complete") print("\n==========================================") print("ALL RECIPE SHARING TESTS PASSED SUCCESSFULLY!") print("==========================================") if __name__ == "__main__": run_tests()