init CTFd source
Some checks are pending
Linting / Linting (3.11) (push) Waiting to run
Mirror core-theme / mirror (push) Waiting to run

This commit is contained in:
gkr
2025-12-25 09:39:21 +08:00
commit 2e06f92c64
1047 changed files with 150349 additions and 0 deletions

View File

@@ -0,0 +1,598 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from CTFd.cache import clear_ratings
from CTFd.models import Ratings
from CTFd.utils import set_config
from tests.helpers import (
create_ctfd,
destroy_ctfd,
gen_challenge,
gen_flag,
gen_rating,
gen_solve,
login_as_user,
register_user,
)
def test_ratings_public_config():
"""Test that users can see and leave ratings when configuration is set to public"""
app = create_ctfd()
with app.app_context():
set_config("challenge_ratings", "public")
# Create users and challenge
register_user(app, name="user1", email="user1@example.com")
user1_id = 2
register_user(app, name="user2", email="user2@example.com")
user2_id = 3
register_user(app, name="user3", email="user3@example.com")
user3_id = 4
challenge = gen_challenge(app.db, name="Test Challenge", value=100)
challenge_id = challenge.id
gen_flag(app.db, challenge_id=challenge_id, content="flag{test}")
# Solve the challenge for both users (required to rate)
gen_solve(
app.db, user_id=user1_id, challenge_id=challenge_id, provided="flag{test}"
)
gen_solve(
app.db, user_id=user2_id, challenge_id=challenge_id, provided="flag{test}"
)
gen_solve(
app.db, user_id=user3_id, challenge_id=challenge_id, provided="flag{test}"
)
client1 = login_as_user(app, name="user1", password="password")
client2 = login_as_user(app, name="user2", password="password")
client3 = login_as_user(app, name="user3", password="password")
# Test that users can leave ratings (PUT request)
rating_data1 = {
"value": 1,
"review": "Great challenge! Really enjoyed solving it.",
}
r = client1.put(f"/api/v1/challenges/{challenge_id}/ratings", json=rating_data1)
assert r.status_code == 200
data = r.get_json()
assert data["success"] is True
assert data["data"]["value"] == 1
assert data["data"]["review"] == "Great challenge! Really enjoyed solving it."
assert data["data"]["challenge_id"] == challenge_id
# Second user can also rate
rating_data2 = {"value": 1, "review": "Excellent challenge!"}
r = client2.put(f"/api/v1/challenges/{challenge_id}/ratings", json=rating_data2)
assert r.status_code == 200
data = r.get_json()
assert data["success"] is True
assert data["data"]["value"] == 1
assert data["data"]["review"] == "Excellent challenge!"
# Second user can also rate
rating_data3 = {"value": -1, "review": "Hated it!"}
r = client3.put(f"/api/v1/challenges/{challenge_id}/ratings", json=rating_data3)
assert r.status_code == 200
data = r.get_json()
assert data["success"] is True
assert data["data"]["value"] == -1
assert data["data"]["review"] == "Hated it!"
# Test that challenge detail includes rating info when public
r = client1.get(f"/api/v1/challenges/{challenge_id}")
assert r.status_code == 200
data = r.get_json()
assert data["success"] is True
assert "ratings" in data["data"]
assert data["data"]["ratings"]["up"] == 2
assert data["data"]["ratings"]["down"] == 1
assert data["data"]["ratings"]["count"] == 3
assert "rating" in data["data"] # User's own rating
assert data["data"]["rating"]["value"] == 1
assert (
data["data"]["rating"]["review"]
== "Great challenge! Really enjoyed solving it."
)
assert len(Ratings.query.all()) == 3
destroy_ctfd(app)
def test_ratings_private_config():
"""Test that users can only leave ratings but cannot see aggregated ratings when set to private"""
app = create_ctfd()
with app.app_context():
set_config("challenge_ratings", "private")
# Create users and challenge
register_user(app, name="user1", email="user1@example.com")
user1_id = 2
register_user(app, name="user2", email="user2@example.com")
user2_id = 3
challenge = gen_challenge(app.db, name="Test Challenge", value=100)
challenge_id = challenge.id
gen_flag(app.db, challenge_id=challenge_id, content="flag{test}")
# Solve the challenge for both users (required to rate)
gen_solve(
app.db, user_id=user1_id, challenge_id=challenge_id, provided="flag{test}"
)
gen_solve(
app.db, user_id=user2_id, challenge_id=challenge_id, provided="flag{test}"
)
client1 = login_as_user(app, name="user1", password="password")
client2 = login_as_user(app, name="user2", password="password")
admin_client = login_as_user(app, name="admin", password="password")
# Test that non-admin users cannot see aggregated ratings (GET request should fail)
r = client1.get(f"/api/v1/challenges/{challenge_id}/ratings", json=True)
assert r.status_code == 403 # Forbidden
# Test that users can still leave ratings (PUT request)
rating_data1 = {"value": 1, "review": "Good challenge with private rating"}
r = client1.put(f"/api/v1/challenges/{challenge_id}/ratings", json=rating_data1)
assert r.status_code == 200
data = r.get_json()
assert data["success"] is True
assert data["data"]["value"] == 1
assert data["data"]["review"] == "Good challenge with private rating"
# Second user can also rate
rating_data2 = {"value": 1, "review": "Excellent private challenge!"}
r = client2.put(f"/api/v1/challenges/{challenge_id}/ratings", json=rating_data2)
assert r.status_code == 200
data = r.get_json()
assert data["success"] is True
assert data["data"]["value"] == 1
assert data["data"]["review"] == "Excellent private challenge!"
# Test that challenge detail doesn't include aggregated ratings for regular users
r = client1.get(f"/api/v1/challenges/{challenge_id}")
assert r.status_code == 200
data = r.get_json()
assert data["success"] is True
assert data["data"]["ratings"] is None # No aggregated ratings shown
assert "rating" in data["data"] # But user can see their own rating
assert data["data"]["rating"]["value"] == 1
assert data["data"]["rating"]["review"] == "Good challenge with private rating"
# Test that admin can still see aggregated ratings
r = admin_client.get(f"/api/v1/challenges/{challenge_id}/ratings")
assert r.status_code == 200
data = r.get_json()
assert data["success"] is True
assert data["meta"]["summary"]["up"] == 2
assert data["meta"]["summary"]["down"] == 0
assert data["meta"]["summary"]["count"] == 2
assert len(Ratings.query.all()) == 2
destroy_ctfd(app)
def test_ratings_disabled_config():
"""Test that users cannot see or leave ratings when ratings are disabled"""
app = create_ctfd()
with app.app_context():
set_config("challenge_ratings", "disabled")
# Create users and challenge
register_user(app, name="user1", email="user1@example.com")
user1_id = 2
challenge = gen_challenge(app.db, name="Test Challenge", value=100)
challenge_id = challenge.id
gen_flag(app.db, challenge_id=challenge_id, content="flag{test}")
# Solve the challenge (required to rate)
gen_solve(
app.db, user_id=user1_id, challenge_id=challenge_id, provided="flag{test}"
)
client1 = login_as_user(app, name="user1", password="password")
admin_client = login_as_user(app, name="admin", password="password")
# Test that users cannot see ratings (GET request should fail)
r = client1.get(f"/api/v1/challenges/{challenge_id}/ratings", json=True)
assert r.status_code == 403 # Forbidden
# Test that users cannot leave ratings (PUT request should fail)
rating_data = {"value": 2, "review": "Trying to rate when disabled"}
r = client1.put(f"/api/v1/challenges/{challenge_id}/ratings", json=rating_data)
assert r.status_code == 403 # Forbidden
assert len(Ratings.query.all()) == 0
# Test that challenge detail doesn't include any rating info
r = client1.get(f"/api/v1/challenges/{challenge_id}")
assert r.status_code == 200
data = r.get_json()
assert data["success"] is True
assert data["data"]["ratings"] is None
assert data["data"]["rating"] is None
# Test that even admin cannot leave ratings when disabled
r = admin_client.put(
f"/api/v1/challenges/{challenge_id}/ratings", json=rating_data
)
assert r.status_code == 403 # Forbidden
# But admin can still see the endpoint (though it will be empty)
r = admin_client.get(f"/api/v1/challenges/{challenge_id}/ratings", json=True)
data = r.get_json()
assert len(data["data"]) == 0
assert len(Ratings.query.all()) == 0
destroy_ctfd(app)
def test_rating_requires_solve():
"""Test that users can only rate challenges they have solved"""
app = create_ctfd()
with app.app_context():
set_config("challenge_ratings", "public")
# Create a user and challenge
register_user(app, name="user1", email="user1@example.com")
user_id = 2
challenge = gen_challenge(app.db, name="Test Challenge", value=100)
chal_id = challenge.id
gen_flag(app.db, challenge_id=chal_id, content="flag{test}")
client = login_as_user(app, name="user1", password="password")
# Try to rate without solving first
rating_data = {"value": 1, "review": "Great challenge!"}
r = client.put(f"/api/v1/challenges/{chal_id}/ratings", json=rating_data)
assert r.status_code == 403
data = r.get_json()
assert data["success"] is False
assert "You must solve this challenge before rating it" in data["errors"][""][0]
assert len(Ratings.query.all()) == 0
# Now solve the challenge
gen_solve(app.db, user_id=user_id, challenge_id=chal_id, provided="flag{test}")
# Now rating should work
r = client.put(f"/api/v1/challenges/{chal_id}/ratings", json=rating_data)
assert r.status_code == 200
data = r.get_json()
assert data["success"] is True
assert data["data"]["value"] == 1
assert data["data"]["review"] == "Great challenge!"
assert len(Ratings.query.all()) == 1
destroy_ctfd(app)
def test_rating_validation():
"""Test rating input validation"""
app = create_ctfd()
with app.app_context():
set_config("challenge_ratings", "public")
# Create a user and challenge
register_user(app, name="user1", email="user1@example.com")
user_id = 2
challenge = gen_challenge(app.db, name="Test Challenge", value=100)
chal_id = challenge.id
gen_flag(app.db, challenge_id=chal_id, content="flag{test}")
# Solve the challenge
gen_solve(app.db, user_id=user_id, challenge_id=chal_id, provided="flag{test}")
client = login_as_user(app, name="user1", password="password")
# Test missing rating value
r = client.put(f"/api/v1/challenges/{chal_id}/ratings", json={})
assert r.status_code == 400
data = r.get_json()
assert data["success"] is False
assert "Rating value is required" in data["errors"]["value"][0]
# Test invalid rating value (not -1 or 1)
rating_data = {"value": 0}
r = client.put(f"/api/v1/challenges/{chal_id}/ratings", json=rating_data)
assert r.status_code == 400
data = r.get_json()
assert data["success"] is False
assert "Rating value must be either 1 or -1" in data["errors"]["value"][0]
# Test invalid rating value (too high)
rating_data = {"value": 2}
r = client.put(f"/api/v1/challenges/{chal_id}/ratings", json=rating_data)
assert r.status_code == 400
data = r.get_json()
assert data["success"] is False
assert "Rating value must be either 1 or -1" in data["errors"]["value"][0]
# Test invalid rating value (non-integer)
rating_data = {"value": "invalid"}
r = client.put(f"/api/v1/challenges/{chal_id}/ratings", json=rating_data)
assert r.status_code == 400
data = r.get_json()
assert data["success"] is False
assert "Rating value must be an integer" in data["errors"]["value"][0]
# Test review text too long
long_review = "x" * 2001 # Exceeds 2000 character limit
rating_data = {"value": 1, "review": long_review}
r = client.put(f"/api/v1/challenges/{chal_id}/ratings", json=rating_data)
assert r.status_code == 400
data = r.get_json()
assert data["success"] is False
assert (
"Review text cannot exceed 2000 characters" in data["errors"]["review"][0]
)
assert len(Ratings.query.all()) == 0
# Test valid rating with review
rating_data = {
"value": -1,
"review": "This is a valid review within the character limit.",
}
r = client.put(f"/api/v1/challenges/{chal_id}/ratings", json=rating_data)
assert r.status_code == 200
data = r.get_json()
assert data["success"] is True
assert data["data"]["value"] == -1
assert (
data["data"]["review"]
== "This is a valid review within the character limit."
)
assert len(Ratings.query.all()) == 1
destroy_ctfd(app)
def test_rating_update():
"""Test that users can update their existing ratings"""
app = create_ctfd()
with app.app_context():
set_config("challenge_ratings", "public")
# Create a user and challenge
register_user(app, name="user1", email="user1@example.com")
user_id = 2
challenge = gen_challenge(app.db, name="Test Challenge", value=100)
chal_id = challenge.id
gen_flag(app.db, challenge_id=challenge.id, content="flag{test}")
# Solve the challenge
gen_solve(app.db, user_id=user_id, challenge_id=chal_id, provided="flag{test}")
client = login_as_user(app, name="user1", password="password")
# Create initial rating
initial_rating = {"value": 1, "review": "Initial review"}
r = client.put(f"/api/v1/challenges/{chal_id}/ratings", json=initial_rating)
assert r.status_code == 200
data = r.get_json()
assert data["success"] is True
assert data["data"]["value"] == 1
assert data["data"]["review"] == "Initial review"
initial_id = data["data"]["id"]
assert len(Ratings.query.all()) == 1
r = Ratings.query.get(1)
assert r.value == 1
assert r.review == "Initial review"
# Update the rating
updated_rating = {
"value": -1,
"review": "Updated review after thinking more about it",
}
r = client.put(f"/api/v1/challenges/{chal_id}/ratings", json=updated_rating)
assert r.status_code == 200
data = r.get_json()
assert data["success"] is True
assert data["data"]["value"] == -1
assert data["data"]["review"] == "Updated review after thinking more about it"
assert data["data"]["id"] == initial_id # Same rating record, just updated
assert len(Ratings.query.all()) == 1
r = Ratings.query.get(1)
assert r.value == -1
assert r.review == "Updated review after thinking more about it"
# Verify only one rating exists in database
ratings = Ratings.query.filter_by(user_id=user_id, challenge_id=chal_id).all()
assert len(ratings) == 1
assert ratings[0].value == -1
assert ratings[0].review == "Updated review after thinking more about it"
destroy_ctfd(app)
def test_rating_without_authentication():
"""Test that unauthenticated users cannot rate challenges"""
app = create_ctfd()
with app.app_context():
set_config("challenge_ratings", "public")
challenge = gen_challenge(app.db, name="Test Challenge", value=100)
gen_flag(app.db, challenge_id=challenge.id, content="flag{test}")
# Try to rate without being logged in
with app.test_client() as client:
rating_data = {"value": 4, "review": "Great challenge!"}
r = client.put(
f"/api/v1/challenges/{challenge.id}/ratings", json=rating_data
)
assert r.status_code == 403
assert len(Ratings.query.all()) == 0
destroy_ctfd(app)
def test_ratings_upvote_downvote_count():
"""Test that upvote and downvote calculations are correct with multiple users and ratings"""
app = create_ctfd()
with app.app_context():
set_config("challenge_ratings", "public")
# Define test data for upvote/downvote ratings
user_ratings = [
{
"name": "user1",
"email": "user1@example.com",
"value": 1, # upvote
"review": "Excellent challenge!",
},
{
"name": "user2",
"email": "user2@example.com",
"value": -1, # downvote
"review": "Not great",
},
{
"name": "user3",
"email": "user3@example.com",
"value": 1, # upvote
"review": "Pretty good!",
},
]
# Expected upvotes/downvotes after each rating submission
expected_counts = [
{"up": 1, "down": 0, "count": 1}, # After user1
{"up": 1, "down": 1, "count": 2}, # After user2
{"up": 2, "down": 1, "count": 3}, # After user3
]
# Create users and challenge
users = []
user_ids = []
user_id_start = 2
for rating_data in user_ratings:
user = register_user(
app, name=rating_data["name"], email=rating_data["email"]
)
users.append(user)
user_ids.append(user_id_start) # Store ID immediately
user_id_start += 1
# Create admin client for accessing ratings endpoint
admin_client = login_as_user(app, name="admin", password="password")
challenge = gen_challenge(app.db, name="Test Challenge", value=100)
challenge_id = challenge.id # Store ID immediately
gen_flag(app.db, challenge_id=challenge_id, content="flag{test}")
# Solve the challenge for all users (required to rate)
for user_id in user_ids:
gen_solve(
app.db,
user_id=user_id,
challenge_id=challenge_id,
provided="flag{test}",
)
# Initially no ratings - check admin can see empty ratings endpoint
r = admin_client.get(f"/api/v1/challenges/{challenge_id}/ratings")
assert r.status_code == 200
data = r.get_json()
assert data["success"] is True
assert data["meta"]["summary"]["up"] == 0
assert data["meta"]["summary"]["down"] == 0
assert data["meta"]["summary"]["count"] == 0
# Submit ratings progressively and verify calculations using database creation
for i, rating_data in enumerate(user_ratings):
# Create rating directly in database using helper
gen_rating(
app.db,
user_id=user_ids[i],
challenge_id=challenge_id,
value=rating_data["value"],
review=rating_data["review"],
)
assert len(Ratings.query.all()) == i + 1
# Check admin ratings endpoint has correct upvote/downvote counts
r = admin_client.get(f"/api/v1/challenges/{challenge_id}/ratings")
assert r.status_code == 200
data = r.get_json()
assert data["success"] is True
assert data["meta"]["summary"]["up"] == expected_counts[i]["up"]
assert data["meta"]["summary"]["down"] == expected_counts[i]["down"]
assert data["meta"]["summary"]["count"] == expected_counts[i]["count"]
# Verify each user can see their own correct rating in challenge detail
for rating_data in user_ratings:
user_client = login_as_user(
app, name=rating_data["name"], password="password"
)
r = user_client.get(f"/api/v1/challenges/{challenge_id}")
data = r.get_json()
assert data["data"]["rating"]["value"] == rating_data["value"]
assert data["data"]["rating"]["review"] == rating_data["review"]
# Test rating update - user 1 changes their rating from upvote (1) to downvote (-1)
# Update rating directly in database
first_rating = Ratings.query.filter_by(
user_id=user_ids[0], challenge_id=challenge_id
).first()
first_rating.value = -1 # Change from upvote to downvote
first_rating.review = "Actually, not so great after reconsidering"
app.db.session.commit()
clear_ratings()
assert len(Ratings.query.all()) == len(
user_ratings
) # Still same number of ratings
# Update the expected values list for verification
user_ratings[0]["value"] = -1
user_ratings[0]["review"] = "Actually, not so great after reconsidering"
# Expected counts after update: user1 changed from upvote to downvote
# So now we have: user1(-1), user2(-1), user3(1) = 1 upvote, 2 downvotes
expected_updated_counts = {"up": 1, "down": 2, "count": 3}
# Check counts after update using admin ratings endpoint
r = admin_client.get(f"/api/v1/challenges/{challenge_id}/ratings")
assert r.status_code == 200
data = r.get_json()
print(data)
assert data["success"] is True
assert data["meta"]["summary"]["up"] == expected_updated_counts["up"]
assert data["meta"]["summary"]["down"] == expected_updated_counts["down"]
assert data["meta"]["summary"]["count"] == expected_updated_counts["count"]
# Verify updated user sees their new rating in challenge detail
user1_client = login_as_user(app, name="user1", password="password")
r = user1_client.get(f"/api/v1/challenges/{challenge_id}")
data = r.get_json()
assert data["data"]["rating"]["value"] == -1
assert (
data["data"]["rating"]["review"]
== "Actually, not so great after reconsidering"
)
# Verify all other users still see their original ratings in challenge detail
for user in user_ratings:
user_client = login_as_user(
app, name=user["name"], password="password", raise_for_error=False
)
r = user_client.get(f"/api/v1/challenges/{challenge_id}")
data = r.get_json()
assert data["data"]["rating"]["value"] == user["value"]
assert data["data"]["rating"]["review"] == user["review"]
# Verify database consistency
all_ratings = Ratings.query.filter_by(challenge_id=challenge_id).all()
assert len(all_ratings) == len(user_ratings)
rating_values = [r.value for r in all_ratings]
expected_values = sorted([rating["value"] for rating in user_ratings])
assert sorted(rating_values) == expected_values
# Verify counts match database reality
upvotes = sum(1 for value in rating_values if value == 1)
downvotes = sum(1 for value in rating_values if value == -1)
assert upvotes == expected_updated_counts["up"]
assert downvotes == expected_updated_counts["down"]
destroy_ctfd(app)