Files
cookbook/scripts/test_recipe_sharing.py

121 lines
6.0 KiB
Python

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()