121 lines
6.0 KiB
Python
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()
|