init CTFd source
This commit is contained in:
836
tests/challenges/test_standard_dynamic.py
Normal file
836
tests/challenges/test_standard_dynamic.py
Normal file
@@ -0,0 +1,836 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user