837 lines
30 KiB
Python
837 lines
30 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from CTFd.models import Challenges
|
|
from CTFd.plugins.challenges import CTFdStandardChallenge
|
|
from CTFd.utils.security.signing import hmac
|
|
from tests.helpers import (
|
|
FakeRequest,
|
|
create_ctfd,
|
|
destroy_ctfd,
|
|
gen_flag,
|
|
gen_user,
|
|
login_as_user,
|
|
register_user,
|
|
)
|
|
|
|
|
|
def test_can_create_standard_dynamic_challenge():
|
|
"""Test that standard dynamic challenges can be made from the API/admin panel"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
register_user(app)
|
|
client = login_as_user(app, name="admin", password="password")
|
|
|
|
challenge_data = {
|
|
"name": "name",
|
|
"category": "category",
|
|
"description": "description",
|
|
"value": 100,
|
|
"function": "linear",
|
|
"initial": 100,
|
|
"decay": 20,
|
|
"minimum": 1,
|
|
"state": "hidden",
|
|
"type": "standard",
|
|
}
|
|
|
|
r = client.post("/api/v1/challenges", json=challenge_data)
|
|
assert r.get_json().get("data")["id"] == 1
|
|
|
|
challenges = Challenges.query.all()
|
|
assert len(challenges) == 1
|
|
|
|
challenge = challenges[0]
|
|
assert challenge.value == 100
|
|
assert challenge.initial == 100
|
|
assert challenge.decay == 20
|
|
assert challenge.minimum == 1
|
|
assert challenge.function == "linear"
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_can_update_standard_dynamic_challenge():
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
challenge_data = {
|
|
"name": "name",
|
|
"category": "category",
|
|
"description": "description",
|
|
"value": 100,
|
|
"state": "hidden",
|
|
"type": "standard",
|
|
}
|
|
req = FakeRequest(form=challenge_data)
|
|
challenge = CTFdStandardChallenge.create(req)
|
|
|
|
assert challenge.value == 100
|
|
assert challenge.initial is None
|
|
assert challenge.decay is None
|
|
assert challenge.minimum is None
|
|
assert challenge.function == "static"
|
|
|
|
challenge_data = {
|
|
"name": "new_name",
|
|
"category": "category",
|
|
"description": "new_description",
|
|
"value": "200",
|
|
"function": "linear",
|
|
"initial": "200",
|
|
"decay": "40",
|
|
"minimum": "5",
|
|
"max_attempts": "0",
|
|
"state": "visible",
|
|
}
|
|
|
|
req = FakeRequest(form=challenge_data)
|
|
challenge = CTFdStandardChallenge.update(challenge, req)
|
|
|
|
assert challenge.name == "new_name"
|
|
assert challenge.description == "new_description"
|
|
assert challenge.value == 200
|
|
assert challenge.initial == 200
|
|
assert challenge.decay == 40
|
|
assert challenge.minimum == 5
|
|
assert challenge.function == "linear"
|
|
assert challenge.state == "visible"
|
|
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_can_add_requirement_standard_dynamic_challenge():
|
|
"""Test that requirements can be added to dynamic challenges"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
challenge_data = {
|
|
"name": "name",
|
|
"category": "category",
|
|
"description": "description",
|
|
"function": "linear",
|
|
"value": 100,
|
|
"decay": 20,
|
|
"minimum": 1,
|
|
"state": "hidden",
|
|
"type": "standard",
|
|
}
|
|
req = FakeRequest(form=challenge_data)
|
|
challenge = CTFdStandardChallenge.create(req)
|
|
|
|
assert challenge.value == 100
|
|
assert challenge.initial == 100
|
|
assert challenge.decay == 20
|
|
assert challenge.minimum == 1
|
|
|
|
challenge_data = {
|
|
"name": "second_name",
|
|
"category": "category",
|
|
"description": "new_description",
|
|
"value": "200",
|
|
"initial": "200",
|
|
"decay": "40",
|
|
"minimum": "5",
|
|
"function": "logarithmic",
|
|
"max_attempts": "0",
|
|
"state": "visible",
|
|
}
|
|
|
|
req = FakeRequest(form=challenge_data)
|
|
challenge = CTFdStandardChallenge.create(req)
|
|
|
|
assert challenge.name == "second_name"
|
|
assert challenge.description == "new_description"
|
|
assert challenge.function == "logarithmic"
|
|
assert challenge.value == 200
|
|
assert challenge.initial == 200
|
|
assert challenge.decay == 40
|
|
assert challenge.minimum == 5
|
|
assert challenge.state == "visible"
|
|
|
|
challenge_data = {"requirements": [1]}
|
|
|
|
req = FakeRequest(form=challenge_data)
|
|
challenge = CTFdStandardChallenge.update(challenge, req)
|
|
|
|
assert challenge.requirements == [1]
|
|
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_can_delete_standard_dynamic_challenge():
|
|
"""Test that dynamic challenges can be deleted"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
register_user(app)
|
|
client = login_as_user(app, name="admin", password="password")
|
|
|
|
challenge_data = {
|
|
"name": "name",
|
|
"category": "category",
|
|
"description": "description",
|
|
"value": 100,
|
|
"initial": 100,
|
|
"decay": 20,
|
|
"minimum": 1,
|
|
"function": "linear",
|
|
"state": "hidden",
|
|
"type": "standard",
|
|
}
|
|
|
|
r = client.post("/api/v1/challenges", json=challenge_data)
|
|
assert r.get_json().get("data")["id"] == 1
|
|
|
|
challenges = Challenges.query.all()
|
|
assert len(challenges) == 1
|
|
|
|
challenge = challenges[0]
|
|
CTFdStandardChallenge.delete(challenge)
|
|
|
|
challenges = Challenges.query.all()
|
|
assert len(challenges) == 0
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_standard_dynamic_challenge_loses_value_properly():
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
register_user(app)
|
|
client = login_as_user(app, name="admin", password="password")
|
|
|
|
challenge_data = {
|
|
"name": "name",
|
|
"category": "category",
|
|
"description": "description",
|
|
"value": 100,
|
|
"initial": 100,
|
|
"decay": 20,
|
|
"minimum": 1,
|
|
"function": "logarithmic",
|
|
"state": "visible",
|
|
"type": "standard",
|
|
}
|
|
|
|
r = client.post("/api/v1/challenges", json=challenge_data)
|
|
assert r.get_json().get("data")["id"] == 1
|
|
|
|
gen_flag(app.db, challenge_id=1, content="flag")
|
|
|
|
for i, team_id in enumerate(range(2, 26)):
|
|
name = "user{}".format(team_id)
|
|
email = "user{}@examplectf.com".format(team_id)
|
|
# We need to bypass rate-limiting so gen_user instead of register_user
|
|
user = gen_user(app.db, name=name, email=email)
|
|
user_id = user.id
|
|
|
|
with app.test_client() as client:
|
|
# We need to bypass rate-limiting so creating a fake user instead of logging in
|
|
with client.session_transaction() as sess:
|
|
sess["id"] = user_id
|
|
sess["nonce"] = "fake-nonce"
|
|
sess["hash"] = hmac(user.password)
|
|
|
|
data = {"submission": "flag", "challenge_id": 1}
|
|
|
|
r = client.post("/api/v1/challenges/attempt", json=data)
|
|
resp = r.get_json()["data"]
|
|
assert resp["status"] == "correct"
|
|
|
|
chal = Challenges.query.filter_by(id=1).first()
|
|
if i >= 20:
|
|
assert chal.value == chal.minimum
|
|
else:
|
|
assert chal.initial >= chal.value
|
|
assert chal.value > chal.minimum
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_standard_dynamic_challenge_doesnt_lose_value_on_update():
|
|
"""Standard Dynamic challenge updates without changing any values or solves shouldn't change the current value. See #1043"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
challenge_data = {
|
|
"name": "name",
|
|
"category": "category",
|
|
"description": "description",
|
|
"value": 10000,
|
|
"initial": 10000,
|
|
"decay": 4,
|
|
"minimum": 10,
|
|
"function": "logarithmic",
|
|
"state": "visible",
|
|
"type": "standard",
|
|
}
|
|
req = FakeRequest(form=challenge_data)
|
|
challenge = CTFdStandardChallenge.create(req)
|
|
challenge_id = challenge.id
|
|
gen_flag(app.db, challenge_id=challenge.id, content="flag")
|
|
register_user(app)
|
|
with login_as_user(app) as client:
|
|
data = {"submission": "flag", "challenge_id": challenge_id}
|
|
r = client.post("/api/v1/challenges/attempt", json=data)
|
|
assert r.status_code == 200
|
|
assert r.get_json()["data"]["status"] == "correct"
|
|
chal = Challenges.query.filter_by(id=challenge_id).first()
|
|
prev_chal_value = chal.value
|
|
chal = CTFdStandardChallenge.update(chal, req)
|
|
assert prev_chal_value == chal.value
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_standard_dynamic_challenge_value_isnt_affected_by_hidden_users():
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
register_user(app)
|
|
client = login_as_user(app, name="admin", password="password")
|
|
|
|
challenge_data = {
|
|
"name": "name",
|
|
"category": "category",
|
|
"description": "description",
|
|
"value": 100,
|
|
"initial": 100,
|
|
"decay": 20,
|
|
"minimum": 1,
|
|
"function": "logarithmic",
|
|
"state": "visible",
|
|
"type": "standard",
|
|
}
|
|
|
|
r = client.post("/api/v1/challenges", json=challenge_data)
|
|
assert r.get_json().get("data")["id"] == 1
|
|
|
|
gen_flag(app.db, challenge_id=1, content="flag")
|
|
|
|
# Make a solve as a regular user. This should not affect the value.
|
|
data = {"submission": "flag", "challenge_id": 1}
|
|
|
|
r = client.post("/api/v1/challenges/attempt", json=data)
|
|
resp = r.get_json()["data"]
|
|
assert resp["status"] == "correct"
|
|
|
|
# Make solves as hidden users. Also should not affect value
|
|
for _, team_id in enumerate(range(2, 26)):
|
|
name = "user{}".format(team_id)
|
|
email = "user{}@examplectf.com".format(team_id)
|
|
# We need to bypass rate-limiting so gen_user instead of register_user
|
|
user = gen_user(app.db, name=name, email=email)
|
|
user.hidden = True
|
|
app.db.session.commit()
|
|
user_id = user.id
|
|
|
|
with app.test_client() as client:
|
|
# We need to bypass rate-limiting so creating a fake user instead of logging in
|
|
with client.session_transaction() as sess:
|
|
sess["id"] = user_id
|
|
sess["nonce"] = "fake-nonce"
|
|
sess["hash"] = hmac(user.password)
|
|
|
|
data = {"submission": "flag", "challenge_id": 1}
|
|
|
|
r = client.post("/api/v1/challenges/attempt", json=data)
|
|
assert r.status_code == 200
|
|
resp = r.get_json()["data"]
|
|
assert resp["status"] == "correct"
|
|
|
|
chal = Challenges.query.filter_by(id=1).first()
|
|
assert chal.value == chal.initial
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_standard_dynamic_challenges_reset():
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
client = login_as_user(app, name="admin", password="password")
|
|
|
|
challenge_data = {
|
|
"name": "name",
|
|
"category": "category",
|
|
"description": "description",
|
|
"value": 100,
|
|
"initial": 100,
|
|
"decay": 20,
|
|
"minimum": 1,
|
|
"function": "linear",
|
|
"state": "hidden",
|
|
"type": "standard",
|
|
}
|
|
|
|
r = client.post("/api/v1/challenges", json=challenge_data)
|
|
assert Challenges.query.count() == 1
|
|
|
|
with client.session_transaction() as sess:
|
|
data = {"nonce": sess.get("nonce"), "challenges": "on"}
|
|
r = client.post("/admin/reset", data=data)
|
|
assert r.location.endswith("/admin/statistics")
|
|
assert Challenges.query.count() == 0
|
|
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_standard_dynamic_challenge_linear_loses_value_properly():
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
register_user(app)
|
|
client = login_as_user(app, name="admin", password="password")
|
|
|
|
challenge_data = {
|
|
"name": "name",
|
|
"category": "category",
|
|
"description": "description",
|
|
"function": "linear",
|
|
"value": 100,
|
|
"initial": 100,
|
|
"decay": 5,
|
|
"minimum": 1,
|
|
"state": "visible",
|
|
"type": "standard",
|
|
}
|
|
|
|
r = client.post("/api/v1/challenges", json=challenge_data)
|
|
assert r.get_json().get("data")["id"] == 1
|
|
|
|
gen_flag(app.db, challenge_id=1, content="flag")
|
|
|
|
for i, team_id in enumerate(range(2, 26)):
|
|
name = "user{}".format(team_id)
|
|
email = "user{}@examplectf.com".format(team_id)
|
|
# We need to bypass rate-limiting so gen_user instead of register_user
|
|
user = gen_user(app.db, name=name, email=email)
|
|
user_id = user.id
|
|
|
|
with app.test_client() as client:
|
|
# We need to bypass rate-limiting so creating a fake user instead of logging in
|
|
with client.session_transaction() as sess:
|
|
sess["id"] = user_id
|
|
sess["nonce"] = "fake-nonce"
|
|
sess["hash"] = hmac(user.password)
|
|
|
|
data = {"submission": "flag", "challenge_id": 1}
|
|
r = client.post("/api/v1/challenges/attempt", json=data)
|
|
resp = r.get_json()["data"]
|
|
assert resp["status"] == "correct"
|
|
|
|
chal = Challenges.query.filter_by(id=1).first()
|
|
|
|
if i >= 20:
|
|
assert chal.value == chal.minimum
|
|
else:
|
|
assert chal.value == (chal.initial - (i * 5))
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_standard_challenges_default_to_static_function():
|
|
"""Test that standard challenges by default are created with static function"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
register_user(app)
|
|
client = login_as_user(app, name="admin", password="password")
|
|
|
|
# Test creating challenge without specifying function - should default to static
|
|
challenge_data = {
|
|
"name": "default_challenge",
|
|
"category": "category",
|
|
"description": "description",
|
|
"value": 100,
|
|
"state": "visible",
|
|
"type": "standard",
|
|
}
|
|
|
|
r = client.post("/api/v1/challenges", json=challenge_data)
|
|
assert r.get_json().get("data")["id"] == 1
|
|
|
|
challenge = Challenges.query.filter_by(id=1).first()
|
|
|
|
# Assert that function defaults to "static"
|
|
assert challenge.function == "static"
|
|
assert challenge.value == 100
|
|
assert challenge.initial is None
|
|
assert challenge.decay is None
|
|
assert challenge.minimum is None
|
|
|
|
# Test with direct model creation (via CTFdStandardChallenge.create)
|
|
challenge_data2 = {
|
|
"name": "direct_challenge",
|
|
"category": "category",
|
|
"description": "description",
|
|
"value": 200,
|
|
"state": "visible",
|
|
"type": "standard",
|
|
# Note: no function specified
|
|
}
|
|
|
|
req = FakeRequest(form=challenge_data2)
|
|
challenge2 = CTFdStandardChallenge.create(req)
|
|
|
|
# Assert that function defaults to "static"
|
|
assert challenge2.function == "static"
|
|
assert challenge2.value == 200
|
|
assert challenge2.initial is None
|
|
assert challenge2.decay is None
|
|
assert challenge2.minimum is None
|
|
|
|
# Test that static challenges maintain their value after solves
|
|
gen_flag(app.db, challenge_id=challenge2.id, content="flag")
|
|
|
|
# Create a user and solve the challenge
|
|
user = gen_user(app.db, name="solver", email="solver@example.com")
|
|
with app.test_client() as test_client:
|
|
with test_client.session_transaction() as sess:
|
|
sess["id"] = user.id
|
|
sess["nonce"] = "fake-nonce"
|
|
sess["hash"] = hmac(user.password)
|
|
|
|
data = {"submission": "flag", "challenge_id": challenge2.id}
|
|
r = test_client.post("/api/v1/challenges/attempt", json=data)
|
|
resp = r.get_json()["data"]
|
|
assert resp["status"] == "correct"
|
|
|
|
# Challenge value should remain unchanged for static challenges
|
|
challenge2_after = Challenges.query.filter_by(id=challenge2.id).first()
|
|
assert challenge2_after.value == 200
|
|
assert challenge2_after.function == "static"
|
|
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_standard_dynamic_challenge_update_overrides_manual_value():
|
|
"""Test that updating a dynamic challenge recalculates value even when a manual value is provided"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
# Create initial dynamic challenge
|
|
challenge_data = {
|
|
"name": "dynamic_challenge",
|
|
"category": "category",
|
|
"description": "description",
|
|
"function": "linear",
|
|
"initial": 100,
|
|
"decay": 10,
|
|
"minimum": 10,
|
|
"state": "visible",
|
|
"type": "standard",
|
|
}
|
|
req = FakeRequest(form=challenge_data)
|
|
challenge = CTFdStandardChallenge.create(req)
|
|
challenge_id = challenge.id
|
|
|
|
# Verify initial state
|
|
assert challenge.function == "linear"
|
|
assert challenge.value == 100 # Should match initial value
|
|
assert challenge.initial == 100
|
|
assert challenge.decay == 10
|
|
assert challenge.minimum == 10
|
|
|
|
# Add a flag and create some solves to change the dynamic value
|
|
gen_flag(app.db, challenge_id=challenge_id, content="flag")
|
|
|
|
# Create some users and solves to trigger value decay
|
|
for i in range(3):
|
|
user = gen_user(app.db, name=f"user{i}", email=f"user{i}@example.com")
|
|
with app.test_client() as client:
|
|
with client.session_transaction() as sess:
|
|
sess["id"] = user.id
|
|
sess["nonce"] = "fake-nonce"
|
|
sess["hash"] = hmac(user.password)
|
|
|
|
data = {"submission": "flag", "challenge_id": challenge_id}
|
|
r = client.post("/api/v1/challenges/attempt", json=data)
|
|
assert r.get_json()["data"]["status"] == "correct"
|
|
|
|
# Check that value has decreased due to linear decay
|
|
challenge_after_solves = Challenges.query.filter_by(id=challenge_id).first()
|
|
# Linear formula: initial - (decay * solve_count), with solve_count adjusted by -1
|
|
# So with 3 solves: 100 - (10 * 2) = 80
|
|
expected_value_after_solves = 80
|
|
assert challenge_after_solves.value == expected_value_after_solves
|
|
|
|
# Now update the challenge with a manual override value but keep same parameters
|
|
update_data = {
|
|
"name": "updated_dynamic_challenge",
|
|
"description": "updated description",
|
|
"value": 100, # This should be ignored since it's a dynamic challenge and value should be calculated on the fly
|
|
"function": "linear",
|
|
"initial": 100, # Keep same initial value
|
|
"decay": 10, # Keep same decay value
|
|
"minimum": 10, # Keep same minimum value
|
|
}
|
|
|
|
req = FakeRequest(form=update_data)
|
|
updated_challenge = CTFdStandardChallenge.update(challenge_after_solves, req)
|
|
|
|
# Verify that the manual value (100) was ignored and dynamic calculation maintained current value
|
|
# Since parameters didn't change and we have 3 solves, value should remain 80
|
|
assert (
|
|
updated_challenge.value == 80
|
|
) # Should stay at dynamically calculated value, not the sent value of 100
|
|
assert updated_challenge.initial == 100 # Parameters should remain the same
|
|
assert updated_challenge.decay == 10
|
|
assert updated_challenge.minimum == 10
|
|
assert updated_challenge.function == "linear"
|
|
|
|
# The name and description should be updated
|
|
assert updated_challenge.name == "updated_dynamic_challenge"
|
|
assert updated_challenge.description == "updated description"
|
|
|
|
# Test that subsequent solves continue to use the same parameters correctly
|
|
user4 = gen_user(app.db, name="user4", email="user4@example.com")
|
|
with app.test_client() as client:
|
|
with client.session_transaction() as sess:
|
|
sess["id"] = user4.id
|
|
sess["nonce"] = "fake-nonce"
|
|
sess["hash"] = hmac(user4.password)
|
|
|
|
data = {"submission": "flag", "challenge_id": challenge_id}
|
|
r = client.post("/api/v1/challenges/attempt", json=data)
|
|
assert r.get_json()["data"]["status"] == "correct"
|
|
|
|
# After 4 total solves: 100 - (10 * 3) = 70
|
|
final_challenge = Challenges.query.filter_by(id=challenge_id).first()
|
|
expected_final_value = 70
|
|
assert final_challenge.value == expected_final_value
|
|
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_standard_dynamic_challenge_creation_requires_all_parameters():
|
|
"""Test that creating a dynamic challenge without initial, minimum, and decay raises an error"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
from CTFd.exceptions.challenges import ChallengeCreateException
|
|
|
|
# Test missing initial parameter for linear function
|
|
challenge_data_missing_initial = {
|
|
"name": "missing_initial",
|
|
"category": "category",
|
|
"description": "description",
|
|
"function": "linear",
|
|
"decay": 10,
|
|
"minimum": 5,
|
|
"state": "visible",
|
|
"type": "standard",
|
|
}
|
|
|
|
req = FakeRequest(form=challenge_data_missing_initial)
|
|
try:
|
|
CTFdStandardChallenge.create(req)
|
|
raise AssertionError(
|
|
"Should have raised ChallengeCreateException for missing initial"
|
|
)
|
|
except ChallengeCreateException as e:
|
|
assert "Missing 'initial'" in str(e)
|
|
assert "function is linear" in str(e)
|
|
|
|
# Test missing decay parameter for logarithmic function
|
|
challenge_data_missing_decay = {
|
|
"name": "missing_decay",
|
|
"category": "category",
|
|
"description": "description",
|
|
"function": "logarithmic",
|
|
"initial": 100,
|
|
"minimum": 5,
|
|
"state": "visible",
|
|
"type": "standard",
|
|
}
|
|
|
|
req = FakeRequest(form=challenge_data_missing_decay)
|
|
try:
|
|
CTFdStandardChallenge.create(req)
|
|
raise AssertionError(
|
|
"Should have raised ChallengeCreateException for missing decay"
|
|
)
|
|
except ChallengeCreateException as e:
|
|
assert "Missing 'decay'" in str(e)
|
|
assert "function is logarithmic" in str(e)
|
|
|
|
# Test missing minimum parameter for linear function
|
|
challenge_data_missing_minimum = {
|
|
"name": "missing_minimum",
|
|
"category": "category",
|
|
"description": "description",
|
|
"function": "linear",
|
|
"initial": 100,
|
|
"decay": 10,
|
|
"state": "visible",
|
|
"type": "standard",
|
|
}
|
|
|
|
req = FakeRequest(form=challenge_data_missing_minimum)
|
|
try:
|
|
CTFdStandardChallenge.create(req)
|
|
raise AssertionError(
|
|
"Should have raised ChallengeCreateException for missing minimum"
|
|
)
|
|
except ChallengeCreateException as e:
|
|
assert "Missing 'minimum'" in str(e)
|
|
assert "function is linear" in str(e)
|
|
|
|
# Test missing all dynamic parameters
|
|
challenge_data_missing_all = {
|
|
"name": "missing_all",
|
|
"category": "category",
|
|
"description": "description",
|
|
"function": "logarithmic",
|
|
"value": 100, # Only static value provided
|
|
"state": "visible",
|
|
"type": "standard",
|
|
}
|
|
|
|
req = FakeRequest(form=challenge_data_missing_all)
|
|
try:
|
|
CTFdStandardChallenge.create(req)
|
|
raise AssertionError(
|
|
"Should have raised ChallengeCreateException for missing all dynamic parameters"
|
|
)
|
|
except ChallengeCreateException as e:
|
|
# Should catch the first missing parameter (likely 'initial')
|
|
assert "Missing" in str(e)
|
|
assert "function is logarithmic" in str(e)
|
|
|
|
# Test that static challenges don't require dynamic parameters
|
|
static_challenge_data = {
|
|
"name": "static_challenge",
|
|
"category": "category",
|
|
"description": "description",
|
|
"function": "static",
|
|
"value": 100,
|
|
"state": "visible",
|
|
"type": "standard",
|
|
}
|
|
|
|
req = FakeRequest(form=static_challenge_data)
|
|
# This should NOT raise an exception
|
|
static_challenge = CTFdStandardChallenge.create(req)
|
|
assert static_challenge.function == "static"
|
|
assert static_challenge.value == 100
|
|
assert static_challenge.initial is None
|
|
assert static_challenge.decay is None
|
|
assert static_challenge.minimum is None
|
|
|
|
# Test that providing all required parameters works
|
|
valid_dynamic_challenge_data = {
|
|
"name": "valid_dynamic",
|
|
"category": "category",
|
|
"description": "description",
|
|
"function": "linear",
|
|
"initial": 200,
|
|
"decay": 15,
|
|
"minimum": 10,
|
|
"state": "visible",
|
|
"type": "standard",
|
|
}
|
|
|
|
req = FakeRequest(form=valid_dynamic_challenge_data)
|
|
# This should NOT raise an exception
|
|
valid_challenge = CTFdStandardChallenge.create(req)
|
|
assert valid_challenge.function == "linear"
|
|
assert valid_challenge.value == 200
|
|
assert valid_challenge.initial == 200
|
|
assert valid_challenge.decay == 15
|
|
assert valid_challenge.minimum == 10
|
|
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_dynamic_challenge_update_requires_all_parameters():
|
|
"""Test that updating a challenge to dynamic function without initial, minimum, and decay raises an error"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
register_user(app)
|
|
client = login_as_user(app, name="admin", password="password")
|
|
|
|
# First create a static challenge via API
|
|
initial_challenge_data = {
|
|
"name": "static_challenge",
|
|
"category": "category",
|
|
"description": "description",
|
|
"value": 100,
|
|
"state": "visible",
|
|
"type": "standard",
|
|
}
|
|
|
|
r = client.post("/api/v1/challenges", json=initial_challenge_data)
|
|
assert r.status_code == 200
|
|
challenge_id = r.get_json().get("data")["id"]
|
|
|
|
# Verify it's initially static
|
|
challenge = Challenges.query.get(1)
|
|
assert challenge.function == "static"
|
|
assert challenge.value == 100
|
|
assert challenge.initial is None
|
|
assert challenge.decay is None
|
|
assert challenge.minimum is None
|
|
|
|
# Test updating to linear function missing initial parameter
|
|
update_data_missing_initial = {
|
|
"function": "linear",
|
|
"decay": 10,
|
|
"minimum": 5,
|
|
}
|
|
|
|
r = client.patch(
|
|
f"/api/v1/challenges/{challenge_id}", json=update_data_missing_initial
|
|
)
|
|
assert "Missing 'initial'" in r.get_json()["errors"][""][0]
|
|
assert r.status_code == 500 # Should fail validation
|
|
|
|
challenge = Challenges.query.get(challenge_id)
|
|
assert challenge.function == "static"
|
|
assert challenge.initial is None
|
|
assert challenge.decay is None
|
|
assert challenge.minimum is None
|
|
|
|
# Test updating to logarithmic function missing decay parameter
|
|
update_data_missing_decay = {
|
|
"function": "logarithmic",
|
|
"initial": 100,
|
|
"minimum": 5,
|
|
}
|
|
|
|
r = client.patch(
|
|
f"/api/v1/challenges/{challenge_id}", json=update_data_missing_decay
|
|
)
|
|
assert "Missing 'decay'" in r.get_json()["errors"][""][0]
|
|
assert r.status_code == 500 # Should fail validation
|
|
|
|
challenge = Challenges.query.get(challenge_id)
|
|
assert challenge.function == "static"
|
|
assert challenge.initial is None
|
|
assert challenge.decay is None
|
|
assert challenge.minimum is None
|
|
|
|
# Test updating to logarithmic function missing minimum parameter
|
|
update_data_missing_minimum = {
|
|
"function": "logarithmic",
|
|
"initial": 100,
|
|
"decay": 10,
|
|
}
|
|
|
|
r = client.patch(
|
|
f"/api/v1/challenges/{challenge_id}", json=update_data_missing_minimum
|
|
)
|
|
assert "Missing 'minimum'" in r.get_json()["errors"][""][0]
|
|
assert r.status_code == 500 # Should fail validation
|
|
|
|
challenge = Challenges.query.get(challenge_id)
|
|
assert challenge.function == "static"
|
|
assert challenge.initial is None
|
|
assert challenge.decay is None
|
|
assert challenge.minimum is None
|
|
|
|
# Test that updating with all valid parameters works
|
|
valid_update_data = {
|
|
"function": "linear",
|
|
"initial": 200,
|
|
"decay": 15,
|
|
"minimum": 10,
|
|
}
|
|
|
|
r = client.patch(f"/api/v1/challenges/{challenge_id}", json=valid_update_data)
|
|
assert r.status_code == 200
|
|
|
|
# Verify the challenge was updated correctly
|
|
challenge = Challenges.query.get(challenge_id)
|
|
assert challenge.function == "linear"
|
|
assert challenge.initial == 200
|
|
assert challenge.decay == 15
|
|
assert challenge.minimum == 10
|
|
|
|
destroy_ctfd(app)
|