init CTFd source
This commit is contained in:
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
0
tests/admin/__init__.py
Normal file
0
tests/admin/__init__.py
Normal file
95
tests/admin/test_challenges.py
Normal file
95
tests/admin/test_challenges.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from CTFd.models import Challenges
|
||||
from CTFd.utils import set_config
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_flag,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_create_new_challenge():
|
||||
"""Test that an admin can create a challenge 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,
|
||||
"state": "hidden",
|
||||
"type": "standard",
|
||||
}
|
||||
|
||||
r = client.post("/api/v1/challenges", json=challenge_data)
|
||||
assert r.get_json().get("data")["id"] == 1
|
||||
r = client.get("/admin/challenges/1")
|
||||
assert r.status_code == 200
|
||||
r = client.get("/api/v1/challenges/1")
|
||||
assert r.get_json().get("data")["id"] == 1
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_hidden_challenge_is_reachable():
|
||||
"""Test that hidden challenges are visible for admins"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
chal = gen_challenge(app.db, state="hidden")
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
chal_id = chal.id
|
||||
|
||||
assert Challenges.query.count() == 1
|
||||
|
||||
r = client.get("/api/v1/challenges", json="")
|
||||
data = r.get_json().get("data")
|
||||
assert data == []
|
||||
|
||||
r = client.get("/api/v1/challenges/1", json="")
|
||||
assert r.status_code == 200
|
||||
data = r.get_json().get("data")
|
||||
assert data["name"] == "chal_name"
|
||||
|
||||
data = {"submission": "flag", "challenge_id": chal_id}
|
||||
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 404
|
||||
|
||||
r = client.post("/api/v1/challenges/attempt?preview=true", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "correct"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenges_admin_only_as_user():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("challenge_visibility", "admins")
|
||||
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
|
||||
gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=1, content="flag")
|
||||
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 403
|
||||
|
||||
r = client.get("/api/v1/challenges", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
r = client.get("/api/v1/challenges/1", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
data = {"submission": "flag", "challenge_id": 1}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
253
tests/admin/test_config.py
Normal file
253
tests/admin/test_config.py
Normal file
@@ -0,0 +1,253 @@
|
||||
import random
|
||||
|
||||
from CTFd.models import (
|
||||
Awards,
|
||||
Challenges,
|
||||
Fails,
|
||||
Files,
|
||||
Flags,
|
||||
Hints,
|
||||
Notifications,
|
||||
Pages,
|
||||
Solves,
|
||||
Submissions,
|
||||
Tags,
|
||||
Teams,
|
||||
Tracking,
|
||||
Unlocks,
|
||||
Users,
|
||||
)
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_award,
|
||||
gen_challenge,
|
||||
gen_fail,
|
||||
gen_file,
|
||||
gen_flag,
|
||||
gen_hint,
|
||||
gen_solve,
|
||||
gen_team,
|
||||
gen_tracking,
|
||||
gen_user,
|
||||
login_as_user,
|
||||
)
|
||||
|
||||
|
||||
def test_reset():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
base_user = "user"
|
||||
|
||||
for x in range(10):
|
||||
chal = gen_challenge(app.db, name="chal_name{}".format(x))
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
gen_hint(app.db, challenge_id=chal.id)
|
||||
gen_file(
|
||||
app.db,
|
||||
location="{name}/{name}.file".format(name=chal.name),
|
||||
challenge_id=chal.id,
|
||||
)
|
||||
|
||||
for x in range(10):
|
||||
user = base_user + str(x)
|
||||
user_email = user + "@examplectf.com"
|
||||
user_obj = gen_user(app.db, name=user, email=user_email)
|
||||
gen_award(app.db, user_id=user_obj.id)
|
||||
gen_solve(app.db, user_id=user_obj.id, challenge_id=random.randint(1, 10))
|
||||
gen_fail(app.db, user_id=user_obj.id, challenge_id=random.randint(1, 10))
|
||||
gen_tracking(app.db, user_id=user_obj.id)
|
||||
|
||||
# Add PageFiles
|
||||
for x in range(5):
|
||||
gen_file(
|
||||
app.db,
|
||||
location="page_file{name}/page_file{name}.file".format(name=x),
|
||||
page_id=1,
|
||||
)
|
||||
|
||||
assert Users.query.count() == 11 # 11 because of the first admin user
|
||||
assert Challenges.query.count() == 10
|
||||
assert (
|
||||
Files.query.count() == 15
|
||||
) # This should be 11 because ChallengeFiles=10 and PageFiles=5
|
||||
assert Flags.query.count() == 10
|
||||
assert Hints.query.count() == 10
|
||||
assert Submissions.query.count() == 20
|
||||
assert Pages.query.count() == 1
|
||||
assert Tracking.query.count() == 10
|
||||
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "pages": "on"}
|
||||
r = client.post("/admin/reset", data=data)
|
||||
assert r.location.endswith("/admin/statistics")
|
||||
assert Pages.query.count() == 0
|
||||
assert Users.query.count() == 11
|
||||
assert Challenges.query.count() == 10
|
||||
assert Tracking.query.count() == 11
|
||||
assert Files.query.count() == 10
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "notifications": "on"}
|
||||
r = client.post("/admin/reset", data=data)
|
||||
assert r.location.endswith("/admin/statistics")
|
||||
assert Notifications.query.count() == 0
|
||||
assert Users.query.count() == 11
|
||||
assert Challenges.query.count() == 10
|
||||
assert Tracking.query.count() == 11
|
||||
|
||||
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
|
||||
assert Flags.query.count() == 0
|
||||
assert Hints.query.count() == 0
|
||||
assert Files.query.count() == 0
|
||||
assert Tags.query.count() == 0
|
||||
assert Users.query.count() == 11
|
||||
assert Tracking.query.count() == 11
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "submissions": "on"}
|
||||
r = client.post("/admin/reset", data=data)
|
||||
assert r.location.endswith("/admin/statistics")
|
||||
assert Submissions.query.count() == 0
|
||||
assert Solves.query.count() == 0
|
||||
assert Fails.query.count() == 0
|
||||
assert Awards.query.count() == 0
|
||||
assert Unlocks.query.count() == 0
|
||||
assert Users.query.count() == 11
|
||||
assert Challenges.query.count() == 0
|
||||
assert Flags.query.count() == 0
|
||||
assert Tracking.query.count() == 0
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "accounts": "on"}
|
||||
r = client.post("/admin/reset", data=data)
|
||||
assert r.location.endswith("/setup")
|
||||
assert Users.query.count() == 0
|
||||
assert Solves.query.count() == 0
|
||||
assert Fails.query.count() == 0
|
||||
assert Tracking.query.count() == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_reset_team_mode():
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
base_user = "user"
|
||||
base_team = "team"
|
||||
|
||||
for x in range(10):
|
||||
chal = gen_challenge(app.db, name="chal_name{}".format(x))
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
gen_hint(app.db, challenge_id=chal.id)
|
||||
gen_file(
|
||||
app.db,
|
||||
location="{name}/{name}.file".format(name=chal.name),
|
||||
challenge_id=chal.id,
|
||||
)
|
||||
|
||||
for x in range(10):
|
||||
user = base_user + str(x)
|
||||
user_email = user + "@examplectf.com"
|
||||
user_obj = gen_user(app.db, name=user, email=user_email)
|
||||
team_obj = gen_team(
|
||||
app.db,
|
||||
name=base_team + str(x),
|
||||
email=base_team + str(x) + "@examplectf.com",
|
||||
)
|
||||
team_obj.members.append(user_obj)
|
||||
team_obj.captain_id = user_obj.id
|
||||
app.db.session.commit()
|
||||
gen_award(app.db, user_id=user_obj.id)
|
||||
gen_solve(app.db, user_id=user_obj.id, challenge_id=random.randint(1, 10))
|
||||
gen_fail(app.db, user_id=user_obj.id, challenge_id=random.randint(1, 10))
|
||||
gen_tracking(app.db, user_id=user_obj.id)
|
||||
|
||||
# Add PageFiles
|
||||
for x in range(5):
|
||||
gen_file(
|
||||
app.db,
|
||||
location="page_file{name}/page_file{name}.file".format(name=x),
|
||||
page_id=1,
|
||||
)
|
||||
|
||||
assert Teams.query.count() == 10
|
||||
# 10 random users, 40 users (10 teams * 4), 1 admin user
|
||||
assert Users.query.count() == 51
|
||||
assert Challenges.query.count() == 10
|
||||
assert (
|
||||
Files.query.count() == 15
|
||||
) # This should be 11 because ChallengeFiles=10 and PageFiles=5
|
||||
assert Flags.query.count() == 10
|
||||
assert Hints.query.count() == 10
|
||||
assert Submissions.query.count() == 20
|
||||
assert Solves.query.count() == 10
|
||||
assert Fails.query.count() == 10
|
||||
assert Tracking.query.count() == 10
|
||||
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "pages": "on"}
|
||||
r = client.post("/admin/reset", data=data)
|
||||
assert r.location.endswith("/admin/statistics")
|
||||
assert Pages.query.count() == 0
|
||||
assert Teams.query.count() == 10
|
||||
assert Users.query.count() == 51
|
||||
assert Challenges.query.count() == 10
|
||||
assert Tracking.query.count() == 11
|
||||
assert Files.query.count() == 10
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "notifications": "on"}
|
||||
r = client.post("/admin/reset", data=data)
|
||||
assert r.location.endswith("/admin/statistics")
|
||||
assert Notifications.query.count() == 0
|
||||
assert Teams.query.count() == 10
|
||||
assert Users.query.count() == 51
|
||||
assert Challenges.query.count() == 10
|
||||
assert Tracking.query.count() == 11
|
||||
|
||||
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
|
||||
assert Flags.query.count() == 0
|
||||
assert Hints.query.count() == 0
|
||||
assert Files.query.count() == 0
|
||||
assert Tags.query.count() == 0
|
||||
assert Teams.query.count() == 10
|
||||
assert Users.query.count() == 51
|
||||
assert Tracking.query.count() == 11
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "submissions": "on"}
|
||||
r = client.post("/admin/reset", data=data)
|
||||
assert r.location.endswith("/admin/statistics")
|
||||
assert Submissions.query.count() == 0
|
||||
assert Solves.query.count() == 0
|
||||
assert Fails.query.count() == 0
|
||||
assert Awards.query.count() == 0
|
||||
assert Unlocks.query.count() == 0
|
||||
assert Teams.query.count() == 10
|
||||
assert Users.query.count() == 51
|
||||
assert Challenges.query.count() == 0
|
||||
assert Flags.query.count() == 0
|
||||
assert Tracking.query.count() == 0
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "accounts": "on"}
|
||||
r = client.post("/admin/reset", data=data)
|
||||
assert r.location.endswith("/setup")
|
||||
assert Users.query.count() == 0
|
||||
assert Teams.query.count() == 0
|
||||
assert Solves.query.count() == 0
|
||||
assert Fails.query.count() == 0
|
||||
assert Tracking.query.count() == 0
|
||||
destroy_ctfd(app)
|
||||
143
tests/admin/test_csv.py
Normal file
143
tests/admin/test_csv.py
Normal file
@@ -0,0 +1,143 @@
|
||||
import io
|
||||
|
||||
from CTFd.models import Challenges, Flags, Hints, Teams, Users
|
||||
from CTFd.utils.crypto import verify_password
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, gen_challenge, login_as_user
|
||||
|
||||
|
||||
def test_export_csv_works():
|
||||
"""Test that CSV exports work properly"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
csv_data = client.get("/admin/export/csv?table=challenges").get_data(
|
||||
as_text=True
|
||||
)
|
||||
assert len(csv_data) > 0
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_import_csv_works():
|
||||
"""Test that CSV imports work properly"""
|
||||
USERS_CSV = b"""name,email,password
|
||||
user1,user1@examplectf.com,password
|
||||
user2,user2@examplectf.com,password"""
|
||||
|
||||
TEAMS_CSV = b"""name,email,password
|
||||
team1,team1@examplectf.com,password
|
||||
team2,team2@examplectf.com,password"""
|
||||
|
||||
CHALLENGES_CSV = b'''name,category,description,value,flags,tags,hints
|
||||
challenge1,category1,description1,100,"flag1,flag2,flag3","tag1,tag2,tag3","hint1,hint2,hint3"'''
|
||||
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"csv_type": "users",
|
||||
"csv_file": (io.BytesIO(USERS_CSV), "users.csv"),
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
|
||||
client.post("/admin/import/csv", data=data, content_type="multipart/form-data")
|
||||
assert Users.query.count() == 3
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.name == "user1"
|
||||
assert user.email == "user1@examplectf.com"
|
||||
assert verify_password("password", user.password)
|
||||
user = Users.query.filter_by(id=3).first()
|
||||
assert user.name == "user2"
|
||||
assert user.email == "user2@examplectf.com"
|
||||
assert verify_password("password", user.password)
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"csv_type": "teams",
|
||||
"csv_file": (io.BytesIO(TEAMS_CSV), "users.csv"),
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
client.post("/admin/import/csv", data=data, content_type="multipart/form-data")
|
||||
assert Teams.query.count() == 2
|
||||
team = Teams.query.filter_by(id=1).first()
|
||||
assert team.name == "team1"
|
||||
assert team.email == "team1@examplectf.com"
|
||||
assert verify_password("password", team.password)
|
||||
team = Teams.query.filter_by(id=2).first()
|
||||
assert team.name == "team2"
|
||||
assert team.email == "team2@examplectf.com"
|
||||
assert verify_password("password", team.password)
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"csv_type": "challenges",
|
||||
"csv_file": (io.BytesIO(CHALLENGES_CSV), "challenges.csv"),
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
|
||||
client.post("/admin/import/csv", data=data, content_type="multipart/form-data")
|
||||
assert Challenges.query.count() == 1
|
||||
challenge = Challenges.query.filter_by(id=1).first()
|
||||
assert challenge.name == "challenge1"
|
||||
assert challenge.category == "category1"
|
||||
assert challenge.description == "description1"
|
||||
assert challenge.value == 100
|
||||
assert len(challenge.flags) == 3
|
||||
assert len(challenge.tags) == 3
|
||||
assert len(challenge.hints) == 3
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_import_challenge_csv_with_json():
|
||||
|
||||
CHALLENGES_CSV = b'''name,category,description,value,flags,tags,hints
|
||||
challenge1,category1,description1,100,"[{""type"": ""static"", ""content"": ""flag1"", ""data"": ""case_insensitive""}, {""type"": ""regex"", ""content"": ""(.*)"", ""data"": ""case_insensitive""}, {""type"": ""static"", ""content"": ""flag3""}]","tag1,tag2,tag3","[{""content"": ""hint1"", ""cost"": 10}, {""content"": ""hint2"", ""cost"": 20}, {""content"": ""hint3"", ""cost"": 30}]"'''
|
||||
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"csv_type": "challenges",
|
||||
"csv_file": (io.BytesIO(CHALLENGES_CSV), "challenges.csv"),
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
|
||||
client.post("/admin/import/csv", data=data, content_type="multipart/form-data")
|
||||
assert Challenges.query.count() == 1
|
||||
challenge = Challenges.query.filter_by(id=1).first()
|
||||
assert challenge.name == "challenge1"
|
||||
assert challenge.category == "category1"
|
||||
assert challenge.description == "description1"
|
||||
assert challenge.value == 100
|
||||
assert len(challenge.flags) == 3
|
||||
assert len(challenge.tags) == 3
|
||||
assert len(challenge.hints) == 3
|
||||
|
||||
for i in range(1, 4):
|
||||
h = Hints.query.filter_by(id=i).first()
|
||||
assert h.cost == i * 10
|
||||
assert h.content == f"hint{i}"
|
||||
|
||||
f = Flags.query.filter_by(id=1).first()
|
||||
assert f.type == "static"
|
||||
assert f.content == "flag1"
|
||||
assert f.data == "case_insensitive"
|
||||
|
||||
f = Flags.query.filter_by(id=2).first()
|
||||
assert f.type == "regex"
|
||||
assert f.content == "(.*)"
|
||||
assert f.data == "case_insensitive"
|
||||
|
||||
f = Flags.query.filter_by(id=3).first()
|
||||
assert f.type == "static"
|
||||
assert f.content == "flag3"
|
||||
assert f.data is None
|
||||
|
||||
destroy_ctfd(app)
|
||||
94
tests/admin/test_fields.py
Normal file
94
tests/admin/test_fields.py
Normal file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Users
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_field,
|
||||
gen_team,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_admin_view_fields():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
|
||||
gen_field(
|
||||
app.db, name="CustomField1", required=True, public=True, editable=True
|
||||
)
|
||||
gen_field(
|
||||
app.db, name="CustomField2", required=False, public=True, editable=True
|
||||
)
|
||||
gen_field(
|
||||
app.db, name="CustomField3", required=False, public=False, editable=True
|
||||
)
|
||||
gen_field(
|
||||
app.db, name="CustomField4", required=False, public=False, editable=False
|
||||
)
|
||||
|
||||
with login_as_user(app, name="admin") as admin:
|
||||
# Admins should see all user fields regardless of public or editable
|
||||
r = admin.get("/admin/users/2")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" in resp
|
||||
assert "CustomField4" in resp
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_admin_view_team_fields():
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
team = gen_team(app.db)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
user.team_id = team.id
|
||||
app.db.session.commit()
|
||||
|
||||
gen_field(
|
||||
app.db,
|
||||
name="CustomField1",
|
||||
type="team",
|
||||
required=True,
|
||||
public=True,
|
||||
editable=True,
|
||||
)
|
||||
gen_field(
|
||||
app.db,
|
||||
name="CustomField2",
|
||||
type="team",
|
||||
required=False,
|
||||
public=True,
|
||||
editable=True,
|
||||
)
|
||||
gen_field(
|
||||
app.db,
|
||||
name="CustomField3",
|
||||
type="team",
|
||||
required=False,
|
||||
public=False,
|
||||
editable=True,
|
||||
)
|
||||
gen_field(
|
||||
app.db,
|
||||
name="CustomField4",
|
||||
type="team",
|
||||
required=False,
|
||||
public=False,
|
||||
editable=False,
|
||||
)
|
||||
|
||||
with login_as_user(app, name="admin") as admin:
|
||||
# Admins should see all team fields regardless of public or editable
|
||||
r = admin.get("/admin/teams/1")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" in resp
|
||||
assert "CustomField4" in resp
|
||||
destroy_ctfd(app)
|
||||
0
tests/admin/test_notifications.py
Normal file
0
tests/admin/test_notifications.py
Normal file
95
tests/admin/test_pages.py
Normal file
95
tests/admin/test_pages.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_page,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_previewing_pages_works():
|
||||
"""Test that pages can be previewed properly"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"title": "title",
|
||||
"route": "route",
|
||||
"content": "content_testing",
|
||||
"nonce": sess.get("nonce"),
|
||||
"draft": True,
|
||||
"hidden": True,
|
||||
"auth_required": True,
|
||||
}
|
||||
|
||||
r = client.post("/admin/pages/preview", data=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "content_testing" in resp
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_previewing_page_with_format_works():
|
||||
"""Test that pages can be previewed properly"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"title": "title",
|
||||
"route": "route",
|
||||
"content": "# content_testing",
|
||||
"format": "markdown",
|
||||
"nonce": sess.get("nonce"),
|
||||
"draft": "y",
|
||||
"hidden": "y",
|
||||
"auth_required": "y",
|
||||
}
|
||||
|
||||
r = client.post("/admin/pages/preview", data=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "<h1>content_testing</h1>" in resp
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"title": "title",
|
||||
"route": "route",
|
||||
"content": "<h1>content_testing</h1>",
|
||||
"format": "html",
|
||||
"nonce": sess.get("nonce"),
|
||||
"draft": "y",
|
||||
"hidden": "y",
|
||||
"auth_required": "y",
|
||||
}
|
||||
|
||||
r = client.post("/admin/pages/preview", data=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "<h1>content_testing</h1>" in resp
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_pages_with_link_target():
|
||||
"""Test that target=_blank links show in public interface"""
|
||||
app = create_ctfd(ctf_theme="core")
|
||||
with app.app_context():
|
||||
gen_page(
|
||||
app.db,
|
||||
title="Title",
|
||||
route="this-is-a-route",
|
||||
content="This is some HTML",
|
||||
link_target="_blank",
|
||||
)
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
with client.session_transaction():
|
||||
r = client.get("/")
|
||||
html = r.get_data(as_text=True)
|
||||
assert "_blank" in html
|
||||
destroy_ctfd(app)
|
||||
54
tests/admin/test_scoreboard.py
Normal file
54
tests/admin/test_scoreboard.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from CTFd.models import Users
|
||||
from CTFd.utils import set_config
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
login_as_user,
|
||||
register_user,
|
||||
simulate_user_activity,
|
||||
)
|
||||
|
||||
|
||||
def test_admins_can_see_scores_with_hidden_scores():
|
||||
"""Test that admins can see user scores when Score Visibility is set to hidden"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
simulate_user_activity(app.db, user=user)
|
||||
|
||||
admin = login_as_user(app, name="admin", password="password")
|
||||
user = login_as_user(app)
|
||||
|
||||
set_config("score_visibility", "hidden")
|
||||
|
||||
# Users can see their own data
|
||||
r = user.get("/api/v1/users/me/fails", json="")
|
||||
assert r.status_code == 200
|
||||
r = user.get("/api/v1/users/me/solves", json="")
|
||||
assert r.status_code == 200
|
||||
|
||||
# Users cannot see public data
|
||||
r = user.get("/api/v1/users/2/solves", json="")
|
||||
assert r.status_code == 403
|
||||
r = user.get("/api/v1/users/2/fails", json="")
|
||||
assert r.status_code == 403
|
||||
r = user.get("/scoreboard")
|
||||
assert r.status_code == 403
|
||||
r = user.get("/api/v1/scoreboard", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# Admins can see user data
|
||||
r = admin.get("/api/v1/users/2/fails", json="")
|
||||
assert r.status_code != 403
|
||||
|
||||
# Admins can see the scoreboard
|
||||
r = admin.get("/scoreboard")
|
||||
assert r.status_code != 403
|
||||
assert "Scores are not currently visible to users" in r.get_data(as_text=True)
|
||||
|
||||
# Admins can see the scoreboard
|
||||
r = admin.get("/api/v1/scoreboard", json="")
|
||||
assert r.status_code != 403
|
||||
|
||||
destroy_ctfd(app)
|
||||
0
tests/admin/test_statistics.py
Normal file
0
tests/admin/test_statistics.py
Normal file
41
tests/admin/test_submissions.py
Normal file
41
tests/admin/test_submissions.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from CTFd.models import Users
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
login_as_user,
|
||||
register_user,
|
||||
simulate_user_activity,
|
||||
)
|
||||
|
||||
|
||||
def test_browse_admin_submissions():
|
||||
"""Test that an admin can create a challenge properly"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="RegisteredUser")
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
simulate_user_activity(app.db, user)
|
||||
|
||||
admin = login_as_user(app, name="admin", password="password")
|
||||
|
||||
# It's difficult to do better checks here becase we're just doing string search.
|
||||
# incorrect includes the word correct and the navbar has correct and incorrect in it
|
||||
r = admin.get("/admin/submissions")
|
||||
assert r.status_code == 200
|
||||
assert "RegisteredUser" in r.get_data(as_text=True)
|
||||
assert "correct" in r.get_data(as_text=True)
|
||||
assert "incorrect" in r.get_data(as_text=True)
|
||||
|
||||
r = admin.get("/admin/submissions/correct")
|
||||
assert r.status_code == 200
|
||||
assert "RegisteredUser" in r.get_data(as_text=True)
|
||||
assert "correct" in r.get_data(as_text=True)
|
||||
|
||||
r = admin.get("/admin/submissions/incorrect")
|
||||
assert r.status_code == 200
|
||||
assert "RegisteredUser" in r.get_data(as_text=True)
|
||||
|
||||
r = admin.get("/admin/submissions/correct?field=challenge_id&q=1")
|
||||
assert r.status_code == 200
|
||||
assert "RegisteredUser" in r.get_data(as_text=True)
|
||||
destroy_ctfd(app)
|
||||
0
tests/admin/test_teams.py
Normal file
0
tests/admin/test_teams.py
Normal file
49
tests/admin/test_users.py
Normal file
49
tests/admin/test_users.py
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_tracking,
|
||||
gen_user,
|
||||
login_as_user,
|
||||
)
|
||||
|
||||
|
||||
def test_admin_user_ip_search():
|
||||
"""Can an admin search user IPs"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
u1 = gen_user(app.db, name="user1", email="user1@examplectf.com")
|
||||
gen_tracking(app.db, user_id=u1.id, ip="1.1.1.1")
|
||||
|
||||
u2 = gen_user(app.db, name="user2", email="user2@examplectf.com")
|
||||
gen_tracking(app.db, user_id=u2.id, ip="2.2.2.2")
|
||||
|
||||
u3 = gen_user(app.db, name="user3", email="user3@examplectf.com")
|
||||
gen_tracking(app.db, user_id=u3.id, ip="3.3.3.3")
|
||||
|
||||
u4 = gen_user(app.db, name="user4", email="user4@examplectf.com")
|
||||
gen_tracking(app.db, user_id=u4.id, ip="3.3.3.3")
|
||||
gen_tracking(app.db, user_id=u4.id, ip="4.4.4.4")
|
||||
|
||||
with login_as_user(app, name="admin", password="password") as admin:
|
||||
r = admin.get("/admin/users?field=ip&q=1.1.1.1")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "user1" in resp
|
||||
assert "user2" not in resp
|
||||
assert "user3" not in resp
|
||||
|
||||
r = admin.get("/admin/users?field=ip&q=2.2.2.2")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "user1" not in resp
|
||||
assert "user2" in resp
|
||||
assert "user3" not in resp
|
||||
|
||||
r = admin.get("/admin/users?field=ip&q=3.3.3.3")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "user1" not in resp
|
||||
assert "user2" not in resp
|
||||
assert "user3" in resp
|
||||
assert "user4" in resp
|
||||
destroy_ctfd(app)
|
||||
82
tests/admin/test_views.py
Normal file
82
tests/admin/test_views.py
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from flask import Flask
|
||||
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_page,
|
||||
gen_team,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def get_bp_urls(blueprint):
|
||||
temp_app = Flask(__name__)
|
||||
temp_app.register_blueprint(blueprint)
|
||||
return [str(p) for p in temp_app.url_map.iter_rules()]
|
||||
|
||||
|
||||
def test_admin_access():
|
||||
"""Can a user access admin pages?"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_page(app.db, title="title", route="/route", content="content")
|
||||
gen_challenge(app.db)
|
||||
gen_team(app.db)
|
||||
routes = [
|
||||
"/admin/challenges/new",
|
||||
"/admin/export/csv",
|
||||
# '/admin/pages/preview',
|
||||
"/admin/pages/new",
|
||||
"/admin/teams/new",
|
||||
"/admin/users/new",
|
||||
"/admin/notifications",
|
||||
"/admin/challenges",
|
||||
"/admin/scoreboard",
|
||||
"/admin/statistics",
|
||||
"/admin/export",
|
||||
"/admin/config",
|
||||
"/admin/pages",
|
||||
"/admin/teams",
|
||||
"/admin/users",
|
||||
"/admin",
|
||||
"/admin/submissions/correct",
|
||||
"/admin/submissions/incorrect",
|
||||
"/admin/submissions",
|
||||
"/admin/challenges/1",
|
||||
# '/admin/plugins/<plugin>',
|
||||
"/admin/pages/1",
|
||||
"/admin/teams/1",
|
||||
"/admin/users/1",
|
||||
]
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
|
||||
for route in routes:
|
||||
r = client.get(route)
|
||||
assert r.status_code == 302
|
||||
assert r.location.startswith("/login")
|
||||
|
||||
admin = login_as_user(app, name="admin")
|
||||
routes.remove("/admin")
|
||||
routes.remove("/admin/export/csv")
|
||||
routes.remove("/admin/export")
|
||||
for route in routes:
|
||||
r = admin.get(route)
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_get_admin_as_user():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/admin")
|
||||
assert r.status_code == 302
|
||||
assert r.location.startswith("/login")
|
||||
destroy_ctfd(app)
|
||||
0
tests/api/__init__.py
Normal file
0
tests/api/__init__.py
Normal file
105
tests/api/test_tokens.py
Normal file
105
tests/api/test_tokens.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import datetime
|
||||
import os
|
||||
from io import BytesIO
|
||||
|
||||
from CTFd.exceptions import UserNotFoundException, UserTokenExpiredException
|
||||
from CTFd.models import Files, Tokens, Users
|
||||
from CTFd.utils.security.auth import generate_user_token, lookup_user_token
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, gen_token, gen_user
|
||||
|
||||
|
||||
def test_generate_user_token():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
user = gen_user(app.db)
|
||||
token = generate_user_token(user, expiration=None)
|
||||
assert token.user_id == user.id
|
||||
assert token.expiration > datetime.datetime.utcnow()
|
||||
assert Tokens.query.count() == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_lookup_user_token():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
user = gen_user(app.db)
|
||||
# Good Token
|
||||
token = gen_token(app.db, user_id=user.id)
|
||||
user = lookup_user_token(token.value)
|
||||
assert user.id == token.user_id
|
||||
|
||||
# Expired Token
|
||||
expiration = datetime.datetime.utcnow() + datetime.timedelta(days=-1)
|
||||
token = gen_token(app.db, user_id=user.id, expiration=expiration)
|
||||
try:
|
||||
lookup_user_token(token.value)
|
||||
except UserTokenExpiredException:
|
||||
pass
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
# Nonexistant token
|
||||
try:
|
||||
lookup_user_token("wat")
|
||||
except UserNotFoundException:
|
||||
pass
|
||||
except Exception as e:
|
||||
raise e
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_token_access():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/users/me", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
with app.test_client() as client:
|
||||
user = gen_user(app.db, name="user2", email="user2@examplectf.com")
|
||||
expiration = datetime.datetime.utcnow() + datetime.timedelta(days=-1)
|
||||
token = generate_user_token(user, expiration=expiration)
|
||||
headers = {"Authorization": "token " + token.value}
|
||||
r = client.get("/api/v1/users/me", headers=headers, json="")
|
||||
assert r.status_code == 401
|
||||
|
||||
with app.test_client() as client:
|
||||
headers = {"Authorization": "token invalid_token"}
|
||||
r = client.get("/api/v1/users/me", headers=headers, json="")
|
||||
assert r.status_code == 401
|
||||
|
||||
with app.test_client() as client:
|
||||
user = gen_user(app.db, name="user1", email="user1@examplectf.com")
|
||||
token = generate_user_token(user, expiration=None)
|
||||
headers = {"Authorization": "token " + token.value}
|
||||
r = client.get("/api/v1/users/me", headers=headers, json="")
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["email"] == "user1@examplectf.com"
|
||||
assert resp["data"]["name"] == "user1"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_token_api_file_upload():
|
||||
"""Test that tokens can upload files with multipart/form-data content type"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
admin = Users.query.filter_by(id=1).first()
|
||||
token = generate_user_token(admin, expiration=None)
|
||||
with app.test_client() as client:
|
||||
headers = {"Authorization": "token " + token.value}
|
||||
r = client.post(
|
||||
"/api/v1/files",
|
||||
headers=headers,
|
||||
content_type="multipart/form-data",
|
||||
data={
|
||||
"file": (BytesIO(b"test file content"), "test.txt"),
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
f = Files.query.filter_by(id=1).first()
|
||||
filepath = os.path.join(app.config["UPLOAD_FOLDER"] + "/" + f.location)
|
||||
with open(filepath) as f:
|
||||
assert f.read() == "test file content"
|
||||
os.remove(filepath)
|
||||
destroy_ctfd(app)
|
||||
0
tests/api/v1/__init__.py
Normal file
0
tests/api/v1/__init__.py
Normal file
171
tests/api/v1/challenges/requirements/test_requirements.py
Normal file
171
tests/api/v1/challenges/requirements/test_requirements.py
Normal file
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Users
|
||||
from CTFd.utils import set_config
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_solve,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_api_challenges_admins_can_bypass_requirements():
|
||||
"""Test that admins can bypass requirements checks with admin capabilities and view-admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Create challenges
|
||||
prereq_id = gen_challenge(app.db).id
|
||||
chal_obj = gen_challenge(app.db)
|
||||
chal_obj.requirements = {"prerequisites": [prereq_id]}
|
||||
|
||||
register_user(app)
|
||||
# Confirm that regular users cannot see prerequisites
|
||||
with login_as_user(app) as client:
|
||||
# Locked challenges aren't shown
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 200
|
||||
data = r.get_json()["data"]
|
||||
assert len(data) == 1
|
||||
assert data[0]["id"] == 1
|
||||
|
||||
# Not even with tricks
|
||||
r = client.get("/api/v1/challenges?view=admin")
|
||||
assert r.status_code == 200
|
||||
data = r.get_json()["data"]
|
||||
assert len(data) == 1
|
||||
assert data[0]["id"] == 1
|
||||
|
||||
# Not even with forced browsing
|
||||
r = client.get("/api/v1/challenges/2")
|
||||
assert r.status_code == 403
|
||||
|
||||
# Confirm that admins
|
||||
with login_as_user(app, name="admin") as admin:
|
||||
# Admins see as regular users
|
||||
r = admin.get("/api/v1/challenges")
|
||||
assert r.status_code == 200
|
||||
data = r.get_json()["data"]
|
||||
assert len(data) == 1
|
||||
assert data[0]["id"] == 1
|
||||
|
||||
# Now admins can see all challenges
|
||||
r = admin.get("/api/v1/challenges?view=admin")
|
||||
assert r.status_code == 200
|
||||
data = r.get_json()["data"]
|
||||
assert len(data) == 2
|
||||
assert data[0]["id"] == 1
|
||||
assert data[1]["id"] == 2
|
||||
|
||||
# Admins can force browse to challenges
|
||||
r = admin.get("/api/v1/challenges/2")
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"]
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenges_challenge_with_requirements():
|
||||
"""Does the challenge list API show challenges with requirements met?"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
prereq_id = gen_challenge(app.db).id
|
||||
chal_obj = gen_challenge(app.db)
|
||||
chal_obj.requirements = {"prerequisites": [prereq_id]}
|
||||
chal_id = chal_obj.id
|
||||
# Create a new user which will solve the prerequisite
|
||||
register_user(app)
|
||||
# Confirm that only the prerequisite challenge is listed initially
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 200
|
||||
(chal_data,) = r.get_json()["data"]
|
||||
assert chal_data["id"] == prereq_id
|
||||
# Generate a solve and then confirm the second challenge is visible
|
||||
gen_solve(app.db, user_id=2, challenge_id=prereq_id)
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 200
|
||||
data = r.get_json()["data"]
|
||||
assert len(data) == 2
|
||||
chal_ids = {c["id"] for c in r.get_json()["data"]}
|
||||
assert chal_ids == {prereq_id, chal_id}
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenges_challenge_with_requirements_hidden_user():
|
||||
"""Does the challenge list API show gated challenges to a hidden user?"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
prereq_id = gen_challenge(app.db).id
|
||||
chal_obj = gen_challenge(app.db)
|
||||
chal_obj.requirements = {"prerequisites": [prereq_id]}
|
||||
chal_id = chal_obj.id
|
||||
# Create a new user which will solve the prerequisite and hide them
|
||||
register_user(app)
|
||||
Users.query.get(2).hidden = True
|
||||
app.db.session.commit()
|
||||
# Confirm that only the prerequisite challenge is listed initially
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 200
|
||||
(chal_data,) = r.get_json()["data"]
|
||||
assert chal_data["id"] == prereq_id
|
||||
# Generate a solve and then confirm the second challenge is visible
|
||||
gen_solve(app.db, user_id=2, challenge_id=prereq_id)
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 200
|
||||
data = r.get_json()["data"]
|
||||
assert len(data) == 2
|
||||
chal_ids = {c["id"] for c in r.get_json()["data"]}
|
||||
assert chal_ids == {prereq_id, chal_id}
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenges_challenge_with_requirements_banned_user():
|
||||
"""Does the challenge list API show gated challenges to a banned user?"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
prereq_id = gen_challenge(app.db).id
|
||||
chal_obj = gen_challenge(app.db)
|
||||
chal_obj.requirements = {"prerequisites": [prereq_id]}
|
||||
# Create a new user which will solve the prerequisite and ban them
|
||||
register_user(app)
|
||||
Users.query.get(2).banned = True
|
||||
app.db.session.commit()
|
||||
# Generate a solve just in case and confirm the API 403s
|
||||
gen_solve(app.db, user_id=2, challenge_id=prereq_id)
|
||||
with login_as_user(app) as client:
|
||||
assert client.get("/api/v1/challenges").status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenges_challenge_with_requirements_no_user():
|
||||
"""Does the challenge list API show gated challenges to the public?"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("challenge_visibility", "public")
|
||||
prereq_id = gen_challenge(app.db).id
|
||||
chal_obj = gen_challenge(app.db)
|
||||
chal_obj.requirements = {"prerequisites": [prereq_id]}
|
||||
# Create a new user which will solve the prerequisite
|
||||
register_user(app)
|
||||
# Confirm that only the prerequisite challenge is listed publicly
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 200
|
||||
initial_data = r.get_json()["data"]
|
||||
(chal_data,) = initial_data
|
||||
assert chal_data["id"] == prereq_id
|
||||
# Fix up the solve count for later comparison with `initial_data`
|
||||
chal_data["solves"] += 1
|
||||
# Generate a solve and then confirm the response is unchanged
|
||||
gen_solve(app.db, user_id=2, challenge_id=prereq_id)
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"] == initial_data
|
||||
destroy_ctfd(app)
|
||||
0
tests/api/v1/statistics/__init__.py
Normal file
0
tests/api/v1/statistics/__init__.py
Normal file
48
tests/api/v1/statistics/test_scores.py
Normal file
48
tests/api/v1/statistics/test_scores.py
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Users
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
login_as_user,
|
||||
register_user,
|
||||
simulate_user_activity,
|
||||
)
|
||||
|
||||
|
||||
def test_api_statistics_score_distribution():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Handle zero data case
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
r = client.get("/api/v1/statistics/scores/distribution")
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["brackets"] == {}
|
||||
|
||||
# Add user data
|
||||
register_user(app)
|
||||
user = Users.query.filter_by(email="user@examplectf.com").first()
|
||||
simulate_user_activity(app.db, user=user)
|
||||
|
||||
# Test again
|
||||
r = client.get("/api/v1/statistics/scores/distribution")
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["brackets"]
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_browse_admin_submissions():
|
||||
"""Test that an admin can create a challenge properly"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(db=app.db)
|
||||
admin = login_as_user(app, name="admin", password="password")
|
||||
r = admin.get(
|
||||
"/api/v1/statistics/challenges/category?function=sum&target=value"
|
||||
)
|
||||
resp = r.get_json()
|
||||
assert resp["data"]
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
0
tests/api/v1/teams/__init__.py
Normal file
0
tests/api/v1/teams/__init__.py
Normal file
90
tests/api/v1/teams/test_scoring.py
Normal file
90
tests/api/v1/teams/test_scoring.py
Normal file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Users
|
||||
from CTFd.utils import set_config
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, gen_award, gen_team, login_as_user
|
||||
|
||||
|
||||
def test_api_team_place_score_hidden_if_scores_hidden():
|
||||
"""/api/v1/teams/me should not reveal team place if scores aren't visible"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
gen_team(app.db)
|
||||
app.db.session.commit()
|
||||
|
||||
gen_award(app.db, user_id=2, team_id=1)
|
||||
|
||||
u = Users.query.filter_by(id=2).first()
|
||||
|
||||
with login_as_user(app, name=u.name) as client:
|
||||
r = client.get("/api/v1/teams/me", json="")
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["place"] == "1st"
|
||||
assert resp["data"]["score"] == 100
|
||||
|
||||
set_config("score_visibility", "hidden")
|
||||
with login_as_user(app, name=u.name) as client:
|
||||
r = client.get("/api/v1/teams/me", json="")
|
||||
resp = r.get_json()
|
||||
# Teams can see their own score but they cannot see their place
|
||||
# This is because a team can always sum up their own score but
|
||||
# they cannot determine their place without social information
|
||||
assert resp["data"]["place"] is None
|
||||
assert resp["data"]["score"] == 100
|
||||
|
||||
set_config("score_visibility", "admins")
|
||||
with login_as_user(app, name=u.name) as client:
|
||||
r = client.get("/api/v1/teams/me", json="")
|
||||
resp = r.get_json()
|
||||
# The same behavior as above applies even under admins only score mode
|
||||
# The rationale is the same. Teams can always sum their own score
|
||||
assert resp["data"]["place"] is None
|
||||
assert resp["data"]["score"] == 100
|
||||
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.get("/api/v1/teams/1", json="")
|
||||
resp = r.get_json()
|
||||
print(resp)
|
||||
assert resp["data"]["place"] == "1st"
|
||||
assert resp["data"]["score"] == 100
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_public_team_place_score_hidden_if_scores_hidden():
|
||||
"""/api/v1/teams/<team_id> should not reveal team place if scores aren't visible"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
gen_team(app.db)
|
||||
app.db.session.commit()
|
||||
|
||||
gen_award(app.db, user_id=2, team_id=1)
|
||||
|
||||
u = Users.query.filter_by(id=2).first()
|
||||
|
||||
with login_as_user(app, name=u.name) as client:
|
||||
r = client.get("/api/v1/teams/1", json="")
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["place"] == "1st"
|
||||
assert resp["data"]["place"] is not None
|
||||
|
||||
set_config("score_visibility", "hidden")
|
||||
with login_as_user(app, name=u.name) as client:
|
||||
r = client.get("/api/v1/teams/1", json="")
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["place"] is None
|
||||
assert resp["data"]["score"] is None
|
||||
|
||||
set_config("score_visibility", "admins")
|
||||
with login_as_user(app, name=u.name) as client:
|
||||
r = client.get("/api/v1/teams/1", json="")
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["place"] is None
|
||||
assert resp["data"]["score"] is None
|
||||
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.get("/api/v1/teams/1", json="")
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["place"] == "1st"
|
||||
assert resp["data"]["score"] is not None
|
||||
destroy_ctfd(app)
|
||||
152
tests/api/v1/teams/test_team_members.py
Normal file
152
tests/api/v1/teams/test_team_members.py
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Awards, Solves, Submissions, Unlocks, Users
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_team,
|
||||
gen_user,
|
||||
login_as_user,
|
||||
simulate_user_activity,
|
||||
)
|
||||
|
||||
|
||||
def test_api_team_get_members():
|
||||
"""Can a user get /api/v1/teams/<team_id>/members only if admin"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
gen_team(app.db)
|
||||
app.db.session.commit()
|
||||
|
||||
gen_user(app.db, name="user_name")
|
||||
with login_as_user(app, name="user_name") as client:
|
||||
r = client.get("/api/v1/teams/1/members", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.get("/api/v1/teams/1/members", json="")
|
||||
assert r.status_code == 200
|
||||
|
||||
resp = r.get_json()
|
||||
# The following data is sorted b/c in Postgres data isn't necessarily returned ordered.
|
||||
assert sorted(resp["data"]) == sorted([2, 3, 4, 5])
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_remove_members():
|
||||
"""Can a user remove /api/v1/teams/<team_id>/members only if admin"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
team = gen_team(app.db)
|
||||
assert len(team.members) == 4
|
||||
app.db.session.commit()
|
||||
|
||||
gen_user(app.db, name="user1")
|
||||
with login_as_user(app, name="user1") as client:
|
||||
r = client.delete("/api/v1/teams/1/members", json={"user_id": 2})
|
||||
assert r.status_code == 403
|
||||
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.delete("/api/v1/teams/1/members", json={"user_id": 2})
|
||||
assert r.status_code == 200
|
||||
|
||||
resp = r.get_json()
|
||||
# The following data is sorted b/c in Postgres data isn't necessarily returned ordered.
|
||||
assert sorted(resp["data"]) == sorted([3, 4, 5])
|
||||
|
||||
r = client.delete("/api/v1/teams/1/members", json={"user_id": 2})
|
||||
|
||||
resp = r.get_json()
|
||||
assert "User is not part of this team" in resp["errors"]["id"]
|
||||
assert r.status_code == 400
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_removing_members_deletes_information():
|
||||
"""If an admin removes a user, their score information should also be removed"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
team = gen_team(app.db)
|
||||
assert len(team.members) == 4
|
||||
app.db.session.commit()
|
||||
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
simulate_user_activity(app.db, user)
|
||||
assert Solves.query.filter_by(user_id=2).count() == 1
|
||||
assert Submissions.query.filter_by(user_id=2).count() == 6
|
||||
assert Awards.query.filter_by(user_id=2).count() == 1
|
||||
assert Unlocks.query.filter_by(user_id=2).count() == 1
|
||||
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.delete("/api/v1/teams/1/members", json={"user_id": 2})
|
||||
assert r.status_code == 200
|
||||
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert Solves.query.filter_by(user_id=2).count() == 0
|
||||
assert Submissions.query.filter_by(user_id=2).count() == 0
|
||||
assert Awards.query.filter_by(user_id=2).count() == 0
|
||||
assert Unlocks.query.filter_by(user_id=2).count() == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_admin_can_change_captain():
|
||||
"""Can admins/captains change captains for teams"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user1 = gen_user(app.db, name="user1", email="user1@examplectf.com") # ID 2
|
||||
user2 = gen_user(app.db, name="user2", email="user2@examplectf.com") # ID 3
|
||||
team = gen_team(app.db)
|
||||
team.members.append(user1)
|
||||
team.members.append(user2)
|
||||
team.captain_id = 2
|
||||
user1.team_id = team.id
|
||||
user2.team_id = team.id
|
||||
app.db.session.commit()
|
||||
|
||||
# I am not the captain
|
||||
with login_as_user(app, name="user2") as client:
|
||||
r = client.patch("/api/v1/teams/1", json={"captain_id": 3})
|
||||
assert r.status_code == 403
|
||||
|
||||
# Look at me, I'm the captain now
|
||||
with login_as_user(app, name="user1") as client:
|
||||
r = client.patch("/api/v1/teams/1", json={"captain_id": 3})
|
||||
# We should still receive a 403 because admins are the only people who can change captains for specific teams
|
||||
assert r.status_code == 403
|
||||
|
||||
# Escalate to admin
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.patch("/api/v1/teams/1", json={"captain_id": 3})
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["captain_id"] == 3
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_users_can_change_captain_on_self_team():
|
||||
"""Can admins/captains change captains for their own team"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user1 = gen_user(app.db, name="user1", email="user1@examplectf.com") # ID 2
|
||||
user2 = gen_user(app.db, name="user2", email="user2@examplectf.com") # ID 3
|
||||
team = gen_team(app.db)
|
||||
team.members.append(user1)
|
||||
team.members.append(user2)
|
||||
team.captain_id = 2
|
||||
user1.team_id = team.id
|
||||
user2.team_id = team.id
|
||||
app.db.session.commit()
|
||||
|
||||
# I am not the captain
|
||||
with login_as_user(app, name="user2") as client:
|
||||
r = client.patch("/api/v1/teams/me", json={"captain_id": 3})
|
||||
assert r.status_code == 403
|
||||
|
||||
# Look at me, I'm the captain now
|
||||
with login_as_user(app, name="user1") as client:
|
||||
r = client.patch("/api/v1/teams/me", json={"captain_id": 3})
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["captain_id"] == 3
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
92
tests/api/v1/teams/test_teams.py
Normal file
92
tests/api/v1/teams/test_teams.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Teams, Users, db
|
||||
from CTFd.utils.crypto import verify_password
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_team,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_api_can_query_by_team_emails():
|
||||
"""Can an admin user query /api/v1/teams using a teams's email address"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
gen_team(app.db, email="team@findme.com")
|
||||
register_user(app, name="testuser", email="user@findme.com")
|
||||
with login_as_user(app, "testuser") as client:
|
||||
r = client.get("/api/v1/teams?field=email&q=findme", json=True)
|
||||
assert r.status_code == 400
|
||||
assert r.get_json()["errors"].get("field")
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.get("/api/v1/teams?field=email&q=findme", json=True)
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"][0]["id"] == 1
|
||||
assert r.get_json()["data"][0]["name"] == "team_name"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_can_update_password_if_none_not_if_set():
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
# Create a user with a null password. Use raw SQL to bypass SQLAlchemy validates
|
||||
gen_team(app.db, name="testteam", email="team@examplectf.com")
|
||||
db.session.execute("UPDATE teams SET password=NULL WHERE name='testteam'")
|
||||
team = Teams.query.filter_by(id=1).first()
|
||||
db.session.commit()
|
||||
assert team.password is None
|
||||
|
||||
# Login and test that we are authed
|
||||
captain = Users.query.filter_by(id=2).first()
|
||||
normal_user = Users.query.filter_by(id=3).first()
|
||||
with login_as_user(app, captain.name) as client:
|
||||
r = client.get("/api/v1/teams/me", json=True)
|
||||
assert r.get_json()["data"]["id"] == team.id
|
||||
assert r.status_code == 200
|
||||
|
||||
# Patch the team's password from NULL
|
||||
team = Teams.query.filter_by(name="testteam").first()
|
||||
assert team.password is None
|
||||
data = {"password": "12345", "confirm": "password"}
|
||||
r = client.patch("/api/v1/teams/me", json=data)
|
||||
assert r.status_code == 200
|
||||
|
||||
# Verify password is now set
|
||||
team = Teams.query.filter_by(name="testteam").first()
|
||||
assert verify_password(plaintext="12345", ciphertext=team.password)
|
||||
|
||||
# Verify that password cannot be changed without valid password
|
||||
data = {"password": "noset", "confirm": "noset"}
|
||||
r = client.patch("/api/v1/teams/me", json=data)
|
||||
resp = r.get_json()
|
||||
assert resp["errors"]["confirm"] == ["Your previous password is incorrect"]
|
||||
assert r.status_code == 400
|
||||
|
||||
# Verify that a normal user cannot change the team password
|
||||
with login_as_user(app, normal_user.name) as client:
|
||||
# Try changing the password for the team
|
||||
data = {"password": "newpassword", "confirm": "12345"}
|
||||
r = client.patch("/api/v1/teams/me", json=data)
|
||||
assert r.status_code == 403
|
||||
|
||||
# Verify that team password has not changed
|
||||
team = Teams.query.filter_by(name="testteam").first()
|
||||
assert verify_password(plaintext="12345", ciphertext=team.password)
|
||||
|
||||
# Create a new team
|
||||
new_team = gen_team(app.db, name="newteam", email="newteam@examplectf.com")
|
||||
new_captain = Users.query.filter_by(id=new_team.captain_id).first()
|
||||
|
||||
# Verify that the captain from the new team cannot change the password of the original team
|
||||
with login_as_user(app, new_captain.name) as client:
|
||||
data = {"password": "newpassword", "confirm": "12345"}
|
||||
r = client.patch("/api/v1/teams/1", json=data)
|
||||
assert r.status_code == 403
|
||||
|
||||
# Verify that old test team password has not changed
|
||||
team = Teams.query.filter_by(name="testteam").first()
|
||||
assert verify_password(plaintext="12345", ciphertext=team.password)
|
||||
121
tests/api/v1/test_awards.py
Normal file
121
tests/api/v1/test_awards.py
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Awards
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_award,
|
||||
gen_team,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_api_awards_access_non_admin():
|
||||
"""Can a user post /api/v1/awards if not admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.post("/api/v1/awards", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_award_get_non_admin
|
||||
"""Can a user get /api/v1/awards/<award_id> if not admin"""
|
||||
r = client.get("/api/v1/awards/1", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_award_delete_non_admin
|
||||
"""Can a user delete /api/v1/awards/<award_id> if not admin"""
|
||||
r = client.delete("/api/v1/awards/1", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_awards_post_admin():
|
||||
"""Can a user post /api/v1/awards if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.post(
|
||||
"/api/v1/awards",
|
||||
json={
|
||||
"name": "Name",
|
||||
"value": "100",
|
||||
"category": "Cate",
|
||||
"description": "Desc",
|
||||
"user_id": 2,
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["success"] is True
|
||||
r = client.post("/api/v1/awards", json="")
|
||||
assert r.status_code == 400
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_awards_post_admin_teams_mode():
|
||||
"""Can a user post /api/v1/awards if admin in team mode"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.post(
|
||||
"/api/v1/awards",
|
||||
json={
|
||||
"name": "Name",
|
||||
"value": "100",
|
||||
"category": "Cate",
|
||||
"description": "Desc",
|
||||
"user_id": 2,
|
||||
},
|
||||
)
|
||||
|
||||
# This should fail because the user doesn't have a team
|
||||
assert r.status_code == 400
|
||||
assert "team_id" in r.get_json()["errors"].keys()
|
||||
assert r.get_json()["success"] is False
|
||||
|
||||
gen_team(app.db)
|
||||
r = client.post(
|
||||
"/api/v1/awards",
|
||||
json={
|
||||
"name": "Name",
|
||||
"value": "100",
|
||||
"category": "Cate",
|
||||
"description": "Desc",
|
||||
"user_id": 3,
|
||||
},
|
||||
)
|
||||
|
||||
# This should pass as we should auto determine the user's team
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["success"] is True
|
||||
award = Awards.query.filter_by(id=1).first()
|
||||
assert award.user_id == 3
|
||||
assert award.team_id == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_award_get_admin():
|
||||
"""Can a user get /api/v1/awards/<award_id> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_award(app.db, 1)
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.get("/api/v1/awards/1", json="")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_award_delete_admin():
|
||||
"""Can a user delete /api/v1/awards/<award_id> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_award(app.db, 1)
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.delete("/api/v1/awards/1", json="")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
122
tests/api/v1/test_brackets.py
Normal file
122
tests/api/v1/test_brackets.py
Normal file
@@ -0,0 +1,122 @@
|
||||
from CTFd.models import Brackets, Users
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_bracket,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_brackets_get_api():
|
||||
"""Test that brackets API GET endpiont is behaving propertly"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_bracket(app.db, name="players1")
|
||||
with app.test_client() as client:
|
||||
client.get("/register")
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"password": "password",
|
||||
"bracket_id": 1,
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
client.post("/register", data=data)
|
||||
client = login_as_user(app, raise_for_error=True)
|
||||
r = client.get("/api/v1/brackets?type=users")
|
||||
resp = r.get_json()
|
||||
print(resp)
|
||||
assert r.status_code == 200
|
||||
assert resp["data"][0]["name"] == "players1"
|
||||
assert resp["data"][0]["description"] == "players who are part of the test"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_brackets_post_api():
|
||||
"""Test that brackets API POST endpiont is behaving propertly"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
data = {
|
||||
"name": "testplayers",
|
||||
"description": "Test players bracket",
|
||||
"type": "users",
|
||||
}
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
r = client.post("/api/v1/brackets", json=data)
|
||||
assert r.status_code == 403
|
||||
assert Brackets.query.count() == 0
|
||||
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.post("/api/v1/brackets", json=data)
|
||||
assert r.status_code == 200
|
||||
assert Brackets.query.count() == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_brackets_patch_api():
|
||||
"""Test that brackets API PATCH endpiont is behaving propertly"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_bracket(app.db, name="players1")
|
||||
assert Brackets.query.count() == 1
|
||||
|
||||
register_user(app, bracket_id=1)
|
||||
with login_as_user(app) as client:
|
||||
r = client.patch("/api/v1/brackets/1", json={"name": "newplayers"})
|
||||
assert r.status_code == 403
|
||||
assert Brackets.query.filter_by(id=1).first().name == "players1"
|
||||
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.patch("/api/v1/brackets/1", json={"name": "newplayers"})
|
||||
assert r.status_code == 200
|
||||
assert Brackets.query.filter_by(id=1).first().name == "newplayers"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_brackets_delete_api():
|
||||
"""Test that brackets API DELETE endpiont is behaving propertly"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_bracket(app.db, name="players1")
|
||||
assert Brackets.query.count() == 1
|
||||
|
||||
register_user(app, bracket_id=1)
|
||||
with login_as_user(app) as client:
|
||||
r = client.delete("/api/v1/brackets/1", json="")
|
||||
assert r.status_code == 403
|
||||
assert Brackets.query.count() == 1
|
||||
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.delete("/api/v1/brackets/1", json="")
|
||||
print(r.get_json())
|
||||
assert r.status_code == 200
|
||||
assert Brackets.query.count() == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_bracket_changing():
|
||||
"""Test that admins can change user's brackets via the API"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_bracket(app.db, name="players1")
|
||||
gen_bracket(app.db, name="players2")
|
||||
with app.test_client() as client:
|
||||
client.get("/register")
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"password": "password",
|
||||
"bracket_id": 1,
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
client.post("/register", data=data)
|
||||
with login_as_user(app, name="admin") as client:
|
||||
assert Users.query.filter_by(id=2).first().bracket_id == 1
|
||||
r = client.patch("/api/v1/users/2", json={"bracket_id": 2})
|
||||
assert r.status_code == 200
|
||||
assert Users.query.filter_by(id=2).first().bracket_id == 2
|
||||
destroy_ctfd(app)
|
||||
1475
tests/api/v1/test_challenges.py
Normal file
1475
tests/api/v1/test_challenges.py
Normal file
File diff suppressed because it is too large
Load Diff
108
tests/api/v1/test_comments.py
Normal file
108
tests/api/v1/test_comments.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Comments
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_comment,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_api_post_comments():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
with login_as_user(app, "admin") as admin:
|
||||
r = admin.post(
|
||||
"/api/v1/comments",
|
||||
json={
|
||||
"content": "this is a challenge comment",
|
||||
"type": "challenge",
|
||||
"challenge_id": 1,
|
||||
},
|
||||
)
|
||||
# Check that POST response has comment data
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["content"] == "this is a challenge comment"
|
||||
assert "this is a challenge comment" in resp["data"]["html"]
|
||||
assert resp["data"]["type"] == "challenge"
|
||||
|
||||
# Check that the comment shows up in the list of comments for the given challenge
|
||||
r = admin.get("/api/v1/comments?challenge_id=1", json="")
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
assert resp["data"][0]["content"] == "this is a challenge comment"
|
||||
assert "this is a challenge comment" in resp["data"][0]["html"]
|
||||
assert resp["data"][0]["type"] == "challenge"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_post_comments_with_invalid_author_id():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
register_user(app)
|
||||
with login_as_user(app, "admin") as admin:
|
||||
r = admin.post(
|
||||
"/api/v1/comments",
|
||||
json={
|
||||
"content": "this is a challenge comment",
|
||||
"type": "challenge",
|
||||
"challenge_id": 1,
|
||||
"author_id": 2,
|
||||
},
|
||||
)
|
||||
# Check that POST response has comment data
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["author_id"] == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_get_comments():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
with login_as_user(app, "admin") as admin:
|
||||
gen_comment(
|
||||
app.db,
|
||||
content="this is a challenge comment",
|
||||
author_id=1,
|
||||
challenge_id=1,
|
||||
)
|
||||
r = admin.get("/api/v1/comments", json="")
|
||||
|
||||
# Check that the comment shows up in the list of all comments
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
assert resp["data"][0]["content"] == "this is a challenge comment"
|
||||
assert "this is a challenge comment" in resp["data"][0]["html"]
|
||||
assert resp["data"][0]["type"] == "challenge"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_delete_comments():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
with login_as_user(app, "admin") as admin:
|
||||
gen_comment(
|
||||
app.db,
|
||||
content="this is a challenge comment",
|
||||
author_id=1,
|
||||
challenge_id=1,
|
||||
)
|
||||
assert Comments.query.count() == 1
|
||||
|
||||
# Check that the comment can be deleted
|
||||
r = admin.delete("/api/v1/comments/1", json="")
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
assert Comments.query.count() == 0
|
||||
assert resp["success"] is True
|
||||
destroy_ctfd(app)
|
||||
219
tests/api/v1/test_config.py
Normal file
219
tests/api/v1/test_config.py
Normal file
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from CTFd.models import Teams, Users
|
||||
from CTFd.utils import get_config, set_config
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, gen_team, login_as_user
|
||||
|
||||
|
||||
def test_api_configs_get_non_admin():
|
||||
"""Can a user get /api/v1/configs if not admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/configs")
|
||||
assert r.status_code == 302
|
||||
|
||||
# test_api_configs_post_non_admin
|
||||
"""Can a user post /api/v1/configs if not admin"""
|
||||
r = client.post("/api/v1/configs", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_configs_patch_non_admin
|
||||
"""Can a user patch /api/v1/configs if not admin"""
|
||||
r = client.patch("/api/v1/configs", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_config_get_non_admin
|
||||
"""Can a user get /api/v1/configs/<config_key> if not admin"""
|
||||
r = client.get("/api/v1/configs/ctf_name")
|
||||
assert r.status_code == 302
|
||||
|
||||
# test_api_config_patch_non_admin
|
||||
"""Can a user patch /api/v1/configs/<config_key> if not admin"""
|
||||
r = client.patch("/api/v1/configs/ctf_name", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_config_delete_non_admin
|
||||
"""Can a user delete /api/v1/configs/<config_key> if not admin"""
|
||||
r = client.delete("/api/v1/configs/ctf_name", json="")
|
||||
assert r.status_code == 403
|
||||
assert get_config("ctf_name") == "CTFd"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_configs_get_admin():
|
||||
"""Can a user get /api/v1/configs if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as admin:
|
||||
r = admin.get("/api/v1/configs")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_configs_post_admin():
|
||||
"""Can a user post /api/v1/configs if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as admin:
|
||||
r = admin.post("/api/v1/configs", json={"value": "9.9.9", "key": "test"})
|
||||
assert r.status_code == 200
|
||||
assert get_config("test") == "9.9.9"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_configs_patch_admin():
|
||||
"""Can a user patch /api/v1/configs if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as admin:
|
||||
r = admin.patch("/api/v1/configs", json={"ctf_name": "Changed_Name"})
|
||||
assert r.status_code == 200
|
||||
assert get_config("ctf_name") == "Changed_Name"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_config_get_admin():
|
||||
"""Can a user get /api/v1/configs/<config_key> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as admin:
|
||||
r = admin.get("/api/v1/configs/ctf_name")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_config_patch_admin():
|
||||
"""Can a user patch /api/v1/configs/<config_key> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as admin:
|
||||
r = admin.patch("/api/v1/configs/ctf_name", json={"value": "Changed_Name"})
|
||||
assert r.status_code == 200
|
||||
assert get_config("ctf_name") == "Changed_Name"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_config_delete_admin():
|
||||
"""Can a user delete /api/v1/configs/<config_key> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as admin:
|
||||
set_config("temp_config", "testing")
|
||||
r = admin.get("/api/v1/configs/temp_config", json="")
|
||||
data = r.get_json()
|
||||
assert r.status_code == 200
|
||||
assert data["data"]["value"] == "testing"
|
||||
|
||||
r = admin.delete("/api/v1/configs/temp_config", json="")
|
||||
assert r.status_code == 200
|
||||
assert get_config("temp_config") is None
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_config_value_types():
|
||||
"""Test that we properly receive values according to schema"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as admin:
|
||||
# Test new configs error out if too long
|
||||
long_text = "a" * 65536
|
||||
r = admin.post(
|
||||
"/api/v1/configs", json={"key": "new_ctf_config", "value": long_text}
|
||||
)
|
||||
data = r.get_json()
|
||||
assert data["errors"]["value"][0] == "new_ctf_config config is too long"
|
||||
assert r.status_code == 400
|
||||
r = admin.post(
|
||||
"/api/v1/configs", json={"key": "new_ctf_config", "value": "test"}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert get_config("new_ctf_config") == "test"
|
||||
|
||||
# Test strings too long error out
|
||||
r = admin.patch("/api/v1/configs", json={"ctf_footer": long_text})
|
||||
data = r.get_json()
|
||||
assert data["errors"]["value"][0] == "ctf_footer config is too long"
|
||||
assert r.status_code == 400
|
||||
|
||||
# Test regular length strings
|
||||
r = admin.patch(
|
||||
"/api/v1/configs",
|
||||
json={"ctf_footer": "// regular length string"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert get_config("ctf_footer") == "// regular length string"
|
||||
|
||||
# Test booleans can be received
|
||||
r = admin.patch("/api/v1/configs", json={"view_after_ctf": True})
|
||||
assert r.status_code == 200
|
||||
assert bool(get_config("view_after_ctf")) == True
|
||||
|
||||
# Test None can be received
|
||||
assert get_config("mail_username") is None
|
||||
r = admin.patch("/api/v1/configs", json={"mail_username": "testusername"})
|
||||
assert r.status_code == 200
|
||||
assert get_config("mail_username") == "testusername"
|
||||
r = admin.patch("/api/v1/configs", json={"mail_username": None})
|
||||
assert r.status_code == 200
|
||||
assert get_config("mail_username") is None
|
||||
|
||||
# Test integers can be received
|
||||
r = admin.patch("/api/v1/configs", json={"mail_port": 12345})
|
||||
assert r.status_code == 200
|
||||
assert get_config("mail_port") == 12345
|
||||
|
||||
# Test specific config key
|
||||
r = admin.patch(
|
||||
"/api/v1/configs/long_config_test", json={"value": long_text}
|
||||
)
|
||||
data = r.get_json()
|
||||
assert data["errors"]["value"][0] == "long_config_test config is too long"
|
||||
assert r.status_code == 400
|
||||
assert get_config("long_config_test") is None
|
||||
r = admin.patch(
|
||||
"/api/v1/configs/config_test", json={"value": "config_value_test"}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert get_config("config_test") == "config_value_test"
|
||||
r = admin.patch(
|
||||
"/api/v1/configs/mail_username", json={"value": "testusername"}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert get_config("mail_username") == "testusername"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_teams_are_removed_after_migrating_from_team_mode_to_user_mode():
|
||||
"""Are teams, and user.team relations removed when migrating from team mode to user mode"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as admin:
|
||||
r = admin.patch("/api/v1/configs", json={"user_mode": "teams"})
|
||||
assert r.status_code == 200
|
||||
assert get_config("user_mode") == "teams"
|
||||
|
||||
gen_team(app.db)
|
||||
|
||||
assert Users.query.count() == 5
|
||||
assert Teams.query.count() == 1
|
||||
|
||||
r = admin.patch("/api/v1/configs", json={"user_mode": "users"})
|
||||
assert r.status_code == 200
|
||||
assert get_config("user_mode") == "users"
|
||||
|
||||
with admin.session_transaction() as sess:
|
||||
data = {
|
||||
"user_mode": "users",
|
||||
"submissions": "true",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = admin.post("/admin/reset", data=data)
|
||||
assert r.status_code == 302
|
||||
|
||||
assert Users.query.count() == 5
|
||||
assert Teams.query.count() == 0
|
||||
|
||||
for user in Users.query.all():
|
||||
assert user.team is None
|
||||
destroy_ctfd(app)
|
||||
44
tests/api/v1/test_csrf.py
Normal file
44
tests/api/v1/test_csrf.py
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from flask.testing import FlaskClient
|
||||
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, login_as_user
|
||||
|
||||
|
||||
def test_api_csrf_failure():
|
||||
"""Test that API requests require the CSRF-Token header"""
|
||||
app = create_ctfd()
|
||||
app.test_client_class = FlaskClient
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.post(
|
||||
"/api/v1/challenges",
|
||||
json={
|
||||
"name": "chal",
|
||||
"category": "cate",
|
||||
"description": "desc",
|
||||
"value": "100",
|
||||
"state": "hidden",
|
||||
"type": "standard",
|
||||
},
|
||||
)
|
||||
assert r.status_code == 403
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
nonce = sess.get("nonce")
|
||||
|
||||
r = client.post(
|
||||
"/api/v1/challenges",
|
||||
headers={"CSRF-Token": nonce},
|
||||
json={
|
||||
"name": "chal",
|
||||
"category": "cate",
|
||||
"description": "desc",
|
||||
"value": "100",
|
||||
"state": "hidden",
|
||||
"type": "standard",
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
49
tests/api/v1/test_exports.py
Normal file
49
tests/api/v1/test_exports.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_api_export_csv():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
data = {
|
||||
"type": "csv",
|
||||
"args": {"table": "challenges"},
|
||||
}
|
||||
|
||||
with login_as_user(app, name="admin", password="password") as client:
|
||||
r = client.post("/api/v1/exports/raw", json=data)
|
||||
assert r.status_code == 200
|
||||
assert r.headers["Content-Type"].startswith("text/csv")
|
||||
assert "chal_name" in r.get_data(as_text=True)
|
||||
|
||||
# Test that regular users cannot access the endpoint
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
response = client.post("/api/v1/exports/raw", json=data)
|
||||
assert response.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_export():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
data = {}
|
||||
|
||||
with login_as_user(app, name="admin", password="password") as client:
|
||||
r = client.post("/api/v1/exports/raw", json=data)
|
||||
assert r.status_code == 200
|
||||
assert r.headers["Content-Type"].startswith("application/zip")
|
||||
|
||||
# Test that regular users cannot access the endpoint
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
response = client.post("/api/v1/exports/raw", json=data)
|
||||
assert response.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
416
tests/api/v1/test_fields.py
Normal file
416
tests/api/v1/test_fields.py
Normal file
@@ -0,0 +1,416 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Fields, TeamFieldEntries, Teams, UserFieldEntries, Users
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_field,
|
||||
gen_team,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_api_custom_fields():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
gen_field(app.db, name="CustomField1")
|
||||
gen_field(app.db, name="CustomField2")
|
||||
|
||||
with login_as_user(app) as user:
|
||||
r = user.get("/api/v1/configs/fields", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
with login_as_user(app, name="admin") as admin:
|
||||
r = admin.get("/api/v1/configs/fields", json="")
|
||||
resp = r.get_json()
|
||||
|
||||
assert resp == {
|
||||
"success": True,
|
||||
"data": [
|
||||
{
|
||||
"public": True,
|
||||
"required": True,
|
||||
"type": "user",
|
||||
"editable": True,
|
||||
"id": 1,
|
||||
"field_type": "text",
|
||||
"description": "CustomFieldDescription",
|
||||
"name": "CustomField1",
|
||||
},
|
||||
{
|
||||
"public": True,
|
||||
"required": True,
|
||||
"type": "user",
|
||||
"editable": True,
|
||||
"id": 2,
|
||||
"field_type": "text",
|
||||
"description": "CustomFieldDescription",
|
||||
"name": "CustomField2",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
r = admin.post(
|
||||
"/api/v1/configs/fields",
|
||||
json={
|
||||
"public": True,
|
||||
"required": True,
|
||||
"editable": True,
|
||||
"id": 2,
|
||||
"type": "user",
|
||||
"field_type": "text",
|
||||
"description": "CustomFieldDescription",
|
||||
"name": "CustomField3",
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
r = admin.get("/api/v1/configs/fields", json="")
|
||||
resp = r.get_json()
|
||||
assert resp == {
|
||||
"success": True,
|
||||
"data": [
|
||||
{
|
||||
"public": True,
|
||||
"required": True,
|
||||
"type": "user",
|
||||
"editable": True,
|
||||
"id": 1,
|
||||
"field_type": "text",
|
||||
"description": "CustomFieldDescription",
|
||||
"name": "CustomField1",
|
||||
},
|
||||
{
|
||||
"public": True,
|
||||
"required": True,
|
||||
"type": "user",
|
||||
"editable": True,
|
||||
"id": 2,
|
||||
"field_type": "text",
|
||||
"description": "CustomFieldDescription",
|
||||
"name": "CustomField2",
|
||||
},
|
||||
{
|
||||
"public": True,
|
||||
"required": True,
|
||||
"editable": True,
|
||||
"id": 3,
|
||||
"type": "user",
|
||||
"field_type": "text",
|
||||
"description": "CustomFieldDescription",
|
||||
"name": "CustomField3",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
r = admin.patch(
|
||||
"/api/v1/configs/fields/3",
|
||||
json={
|
||||
"public": False,
|
||||
"required": False,
|
||||
"editable": False,
|
||||
"id": 4,
|
||||
"type": "user",
|
||||
"field_type": "text",
|
||||
"description": "CustomFieldDescription",
|
||||
"name": "PatchedCustomField3",
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"] == {
|
||||
"public": False,
|
||||
"required": False,
|
||||
"editable": False,
|
||||
"id": 3,
|
||||
"type": "user",
|
||||
"field_type": "text",
|
||||
"description": "CustomFieldDescription",
|
||||
"name": "PatchedCustomField3",
|
||||
}
|
||||
|
||||
r = admin.get("/api/v1/configs/fields/3", json="")
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"] == {
|
||||
"public": False,
|
||||
"required": False,
|
||||
"editable": False,
|
||||
"id": 3,
|
||||
"type": "user",
|
||||
"field_type": "text",
|
||||
"description": "CustomFieldDescription",
|
||||
"name": "PatchedCustomField3",
|
||||
}
|
||||
|
||||
r = admin.delete("/api/v1/configs/fields/3", json="")
|
||||
assert r.status_code == 200
|
||||
|
||||
r = admin.get("/api/v1/configs/fields/3", json="")
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_self_fields_permissions():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_field(app.db, name="CustomField1", public=False, editable=False)
|
||||
gen_field(app.db, name="CustomField2", public=True, editable=True)
|
||||
|
||||
with app.test_client() as client:
|
||||
client.get("/register")
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
"fields[1]": "CustomValue1",
|
||||
"fields[2]": "CustomValue2",
|
||||
}
|
||||
r = client.post("/register", data=data)
|
||||
with client.session_transaction() as sess:
|
||||
assert sess["id"]
|
||||
|
||||
with login_as_user(app) as user, login_as_user(app, name="admin") as admin:
|
||||
r = user.get("/api/v1/users/me")
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["fields"] == [
|
||||
{
|
||||
"value": "CustomValue2",
|
||||
"name": "CustomField2",
|
||||
"description": "CustomFieldDescription",
|
||||
"type": "text",
|
||||
"field_id": 2,
|
||||
}
|
||||
]
|
||||
|
||||
r = admin.get("/api/v1/users/2")
|
||||
resp = r.get_json()
|
||||
assert len(resp["data"]["fields"]) == 2
|
||||
|
||||
field = Fields.query.filter_by(id=1).first()
|
||||
field.public = True
|
||||
app.db.session.commit()
|
||||
r = user.get("/api/v1/users/me")
|
||||
resp = r.get_json()
|
||||
assert len(resp["data"]["fields"]) == 2
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_partial_field_update():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
gen_field(app.db, name="CustomField1")
|
||||
gen_field(app.db, name="CustomField2")
|
||||
|
||||
with login_as_user(app) as user:
|
||||
r = user.patch(
|
||||
"/api/v1/users/me",
|
||||
json={
|
||||
"fields": [
|
||||
{"field_id": 1, "value": "CustomValue1"},
|
||||
{"field_id": 2, "value": "CustomValue2"},
|
||||
]
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert UserFieldEntries.query.count() == 2
|
||||
|
||||
r = user.patch(
|
||||
"/api/v1/users/me",
|
||||
json={"fields": [{"field_id": 2, "value": "NewCustomValue2"}]},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert UserFieldEntries.query.count() == 2
|
||||
assert (
|
||||
UserFieldEntries.query.filter_by(field_id=1, user_id=2).first().value
|
||||
== "CustomValue1"
|
||||
)
|
||||
assert (
|
||||
UserFieldEntries.query.filter_by(field_id=2, user_id=2).first().value
|
||||
== "NewCustomValue2"
|
||||
)
|
||||
|
||||
with login_as_user(app, name="admin") as admin:
|
||||
r = admin.patch(
|
||||
"/api/v1/users/2",
|
||||
json={"fields": [{"field_id": 2, "value": "AdminNewCustomValue2"}]},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert UserFieldEntries.query.count() == 2
|
||||
assert (
|
||||
UserFieldEntries.query.filter_by(field_id=1, user_id=2).first().value
|
||||
== "CustomValue1"
|
||||
)
|
||||
assert (
|
||||
UserFieldEntries.query.filter_by(field_id=2, user_id=2).first().value
|
||||
== "AdminNewCustomValue2"
|
||||
)
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_self_fields_permissions():
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
team = gen_team(app.db)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
user.team_id = team.id
|
||||
app.db.session.commit()
|
||||
team = Teams.query.filter_by(id=1).first()
|
||||
team.captain_id = 2
|
||||
app.db.session.commit()
|
||||
|
||||
gen_field(
|
||||
app.db, name="CustomField1", type="team", public=False, editable=False
|
||||
)
|
||||
gen_field(app.db, name="CustomField2", type="team", public=True, editable=True)
|
||||
|
||||
app.db.session.add(
|
||||
TeamFieldEntries(type="team", value="CustomValue1", team_id=1, field_id=1)
|
||||
)
|
||||
app.db.session.add(
|
||||
TeamFieldEntries(type="team", value="CustomValue2", team_id=1, field_id=2)
|
||||
)
|
||||
app.db.session.commit()
|
||||
|
||||
assert len(team.field_entries) == 2
|
||||
|
||||
with login_as_user(app) as user, login_as_user(app, name="admin") as admin:
|
||||
r = user.get("/api/v1/teams/me")
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["fields"] == [
|
||||
{
|
||||
"value": "CustomValue2",
|
||||
"name": "CustomField2",
|
||||
"description": "CustomFieldDescription",
|
||||
"type": "text",
|
||||
"field_id": 2,
|
||||
}
|
||||
]
|
||||
assert len(resp["data"]["fields"]) == 1
|
||||
|
||||
# Admin gets data and should see all fields
|
||||
r = admin.get("/api/v1/teams/1")
|
||||
resp = r.get_json()
|
||||
assert len(resp["data"]["fields"]) == 2
|
||||
|
||||
r = user.patch(
|
||||
"/api/v1/teams/me",
|
||||
json={
|
||||
"fields": [
|
||||
{"field_id": 1, "value": "NewCustomValue1"},
|
||||
{"field_id": 2, "value": "NewCustomValue2"},
|
||||
]
|
||||
},
|
||||
)
|
||||
assert r.get_json() == {
|
||||
"success": False,
|
||||
"errors": {"fields": ["Field 'CustomField1' cannot be editted"]},
|
||||
}
|
||||
assert r.status_code == 400
|
||||
assert (
|
||||
TeamFieldEntries.query.filter_by(id=1).first().value == "CustomValue1"
|
||||
)
|
||||
assert (
|
||||
TeamFieldEntries.query.filter_by(id=2).first().value == "CustomValue2"
|
||||
)
|
||||
|
||||
# After making the field public the user should see both fields
|
||||
field = Fields.query.filter_by(id=1).first()
|
||||
field.public = True
|
||||
app.db.session.commit()
|
||||
r = user.get("/api/v1/teams/me")
|
||||
resp = r.get_json()
|
||||
assert len(resp["data"]["fields"]) == 2
|
||||
|
||||
# Captain should be able to edit their values after it's made editable
|
||||
field = Fields.query.filter_by(id=1).first()
|
||||
field.editable = True
|
||||
app.db.session.commit()
|
||||
r = user.patch(
|
||||
"/api/v1/teams/me",
|
||||
json={
|
||||
"fields": [
|
||||
{"field_id": 1, "value": "NewCustomValue1"},
|
||||
{"field_id": 2, "value": "NewCustomValue2"},
|
||||
]
|
||||
},
|
||||
)
|
||||
print(r.get_json())
|
||||
assert r.status_code == 200
|
||||
assert (
|
||||
TeamFieldEntries.query.filter_by(id=1).first().value
|
||||
== "NewCustomValue1"
|
||||
)
|
||||
assert (
|
||||
TeamFieldEntries.query.filter_by(id=2).first().value
|
||||
== "NewCustomValue2"
|
||||
)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_team_partial_field_update():
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
team = gen_team(app.db)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
user.team_id = team.id
|
||||
team = Teams.query.filter_by(id=1).first()
|
||||
team.captain_id = 2
|
||||
app.db.session.commit()
|
||||
|
||||
gen_field(app.db, name="CustomField1", type="team")
|
||||
gen_field(app.db, name="CustomField2", type="team")
|
||||
|
||||
with login_as_user(app) as user:
|
||||
r = user.patch(
|
||||
"/api/v1/teams/me",
|
||||
json={
|
||||
"fields": [
|
||||
{"field_id": 1, "value": "CustomValue1"},
|
||||
{"field_id": 2, "value": "CustomValue2"},
|
||||
]
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert TeamFieldEntries.query.count() == 2
|
||||
|
||||
r = user.patch(
|
||||
"/api/v1/teams/me",
|
||||
json={"fields": [{"field_id": 2, "value": "NewCustomValue2"}]},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert TeamFieldEntries.query.count() == 2
|
||||
assert (
|
||||
TeamFieldEntries.query.filter_by(field_id=1, team_id=1).first().value
|
||||
== "CustomValue1"
|
||||
)
|
||||
assert (
|
||||
TeamFieldEntries.query.filter_by(field_id=2, team_id=1).first().value
|
||||
== "NewCustomValue2"
|
||||
)
|
||||
|
||||
with login_as_user(app, name="admin") as admin:
|
||||
r = admin.patch(
|
||||
"/api/v1/teams/1",
|
||||
json={"fields": [{"field_id": 2, "value": "AdminNewCustomValue2"}]},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert TeamFieldEntries.query.count() == 2
|
||||
assert (
|
||||
TeamFieldEntries.query.filter_by(field_id=1, team_id=1).first().value
|
||||
== "CustomValue1"
|
||||
)
|
||||
assert (
|
||||
TeamFieldEntries.query.filter_by(field_id=2, team_id=1).first().value
|
||||
== "AdminNewCustomValue2"
|
||||
)
|
||||
|
||||
destroy_ctfd(app)
|
||||
242
tests/api/v1/test_files.py
Normal file
242
tests/api/v1/test_files.py
Normal file
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
from io import BytesIO
|
||||
|
||||
from CTFd.models import ChallengeFiles, Challenges, Files
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_file,
|
||||
login_as_user,
|
||||
)
|
||||
|
||||
|
||||
def test_api_files_get_non_admin():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
chal = gen_challenge(app.db)
|
||||
gen_file(
|
||||
app.db,
|
||||
location="0bf1a55a5cd327c07af15df260979668/bird.swf",
|
||||
challenge_id=chal.id,
|
||||
)
|
||||
|
||||
with app.test_client() as client:
|
||||
# test_api_files_get_non_admin
|
||||
"""Can a user get /api/v1/files if not admin"""
|
||||
r = client.get("/api/v1/files", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_files_post_non_admin
|
||||
"""Can a user post /api/v1/files if not admin"""
|
||||
r = client.post("/api/v1/files")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_file_get_non_admin
|
||||
"""Can a user get /api/v1/files/<file_id> if not admin"""
|
||||
r = client.get("/api/v1/files/1", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_file_delete_non_admin
|
||||
"""Can a user delete /api/v1/files/<file_id> if not admin"""
|
||||
r = client.delete("/api/v1/files/1", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_files_get_admin():
|
||||
"""Can a user get /api/v1/files if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.get("/api/v1/files", json="")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_files_post_admin():
|
||||
"""Can a user post /api/v1/files if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, name="admin") as client:
|
||||
with client.session_transaction() as sess:
|
||||
nonce = sess.get("nonce")
|
||||
r = client.post(
|
||||
"/api/v1/files",
|
||||
content_type="multipart/form-data",
|
||||
data={
|
||||
"file": (BytesIO(b"test file content"), "test.txt"),
|
||||
"nonce": nonce,
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
f = Files.query.filter_by(id=1).first()
|
||||
assert f.sha1sum == "9032bbc224ed8b39183cb93b9a7447727ce67f9d"
|
||||
os.remove(os.path.join(app.config["UPLOAD_FOLDER"] + "/" + f.location))
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_file_get_admin():
|
||||
"""Can a user get /api/v1/files/<file_id> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
chal = gen_challenge(app.db)
|
||||
f = gen_file(
|
||||
app.db,
|
||||
location="0bf1a55a5cd327c07af15df260979668/bird.swf",
|
||||
challenge_id=chal.id,
|
||||
)
|
||||
assert Files.query.count() == 1
|
||||
assert ChallengeFiles.query.count() == 1
|
||||
assert f in chal.files
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.get("/api/v1/files/1", json="")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_file_delete_admin():
|
||||
"""Can a user delete /api/v1/files/<file_id> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
chal = gen_challenge(app.db)
|
||||
path = os.path.join(
|
||||
app.config["UPLOAD_FOLDER"], "0bf1a55a5cd327c07af15df260979668", "bird.swf"
|
||||
)
|
||||
try:
|
||||
# Create a fake file
|
||||
os.makedirs(os.path.dirname(path))
|
||||
open(path, "a").close()
|
||||
f = gen_file(
|
||||
app.db,
|
||||
location="0bf1a55a5cd327c07af15df260979668/bird.swf",
|
||||
challenge_id=chal.id,
|
||||
)
|
||||
assert Files.query.count() == 1
|
||||
assert ChallengeFiles.query.count() == 1
|
||||
assert f in chal.files
|
||||
|
||||
# Make sure the file was created
|
||||
assert os.path.exists(path)
|
||||
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.delete("/api/v1/files/1", json="")
|
||||
assert r.status_code == 200
|
||||
assert Files.query.count() == 0
|
||||
assert ChallengeFiles.query.count() == 0
|
||||
chal = Challenges.query.filter_by(id=1).first()
|
||||
assert f not in chal.files
|
||||
|
||||
# Make sure the API call deleted the file
|
||||
assert os.path.exists(path) is False
|
||||
finally:
|
||||
# Always make sure the file is deleted
|
||||
shutil.rmtree(os.path.dirname(path), ignore_errors=True)
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_file_custom_location():
|
||||
"""
|
||||
Test file uploading with custom location
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, name="admin") as client:
|
||||
with client.session_transaction() as sess:
|
||||
nonce = sess.get("nonce")
|
||||
r = client.post(
|
||||
"/api/v1/files",
|
||||
content_type="multipart/form-data",
|
||||
data={
|
||||
"file": (BytesIO(b"test file content"), "test.txt"),
|
||||
"location": "testing/asdf.txt",
|
||||
"nonce": nonce,
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
f = Files.query.filter_by(id=1).first()
|
||||
assert f.sha1sum == "9032bbc224ed8b39183cb93b9a7447727ce67f9d"
|
||||
assert f.location == "testing/asdf.txt"
|
||||
r = client.get("/files/" + f.location)
|
||||
assert r.get_data(as_text=True) == "test file content"
|
||||
|
||||
r = client.get("/api/v1/files/1")
|
||||
response = r.get_json()
|
||||
assert (
|
||||
response["data"]["sha1sum"]
|
||||
== "9032bbc224ed8b39183cb93b9a7447727ce67f9d"
|
||||
)
|
||||
assert response["data"]["location"] == "testing/asdf.txt"
|
||||
|
||||
# Test deletion
|
||||
r = client.delete("/api/v1/files/1", json="")
|
||||
assert r.status_code == 200
|
||||
assert Files.query.count() == 0
|
||||
|
||||
target = pathlib.Path(app.config["UPLOAD_FOLDER"]) / f.location
|
||||
assert target.exists() is False
|
||||
|
||||
# Test invalid locations
|
||||
invalid_paths = [
|
||||
"testing/prefix/asdf.txt",
|
||||
"/testing/asdf.txt",
|
||||
"asdf.txt",
|
||||
]
|
||||
for path in invalid_paths:
|
||||
r = client.post(
|
||||
"/api/v1/files",
|
||||
content_type="multipart/form-data",
|
||||
data={
|
||||
"file": (BytesIO(b"test file content"), "test.txt"),
|
||||
"location": path,
|
||||
"nonce": nonce,
|
||||
},
|
||||
)
|
||||
assert r.status_code == 400
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_file_overwrite_by_location():
|
||||
"""
|
||||
Test file overwriting with a specific location
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, name="admin") as client:
|
||||
with client.session_transaction() as sess:
|
||||
nonce = sess.get("nonce")
|
||||
r = client.post(
|
||||
"/api/v1/files",
|
||||
content_type="multipart/form-data",
|
||||
data={
|
||||
"file": (BytesIO(b"test file content"), "test.txt"),
|
||||
"location": "testing/asdf.txt",
|
||||
"nonce": nonce,
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
f = Files.query.filter_by(id=1).first()
|
||||
r = client.get("/files/" + f.location)
|
||||
assert r.get_data(as_text=True) == "test file content"
|
||||
|
||||
r = client.post(
|
||||
"/api/v1/files",
|
||||
content_type="multipart/form-data",
|
||||
data={
|
||||
"file": (BytesIO(b"testing new uploaded file content"), "test.txt"),
|
||||
"location": "testing/asdf.txt",
|
||||
"nonce": nonce,
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
f = Files.query.filter_by(id=1).first()
|
||||
r = client.get("/files/" + f.location)
|
||||
assert f.sha1sum == "0ee7eb85ac0b8d8ae03f3080589157cde553b13f"
|
||||
assert r.get_data(as_text=True) == "testing new uploaded file content"
|
||||
destroy_ctfd(app)
|
||||
240
tests/api/v1/test_flags.py
Normal file
240
tests/api/v1/test_flags.py
Normal file
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Flags
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_flag,
|
||||
login_as_user,
|
||||
)
|
||||
|
||||
|
||||
def test_api_flags_get_non_admin():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
gen_flag(app.db, 1)
|
||||
|
||||
with app.test_client() as client:
|
||||
# test_api_flags_get_non_admin
|
||||
"""Can a user get /api/v1/flags if not admin"""
|
||||
r = client.get("/api/v1/flags", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_flags_post_non_admin
|
||||
"""Can a user post /api/v1/flags if not admin"""
|
||||
r = client.post("/api/v1/flags")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_flag_types_get_non_admin
|
||||
"""Can a user get /api/v1/flags/types[/<type_name>] if not admin"""
|
||||
r = client.get("/api/v1/flags/types", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_flag_get_non_admin
|
||||
"""Can a user get /api/v1/flags/<flag_id> if not admin"""
|
||||
r = client.get("/api/v1/flags/1", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_flag_patch_non_admin
|
||||
"""Can a user patch /api/v1/flags/<flag_id> if not admin"""
|
||||
r = client.patch("/api/v1/flags/1", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_flag_delete_non_admin
|
||||
"""Can a user delete /api/v1/flags/<flag_id> if not admin"""
|
||||
r = client.delete("/api/v1/flags/1", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_flags_get_admin():
|
||||
"""Can a user get /api/v1/flags if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.get("/api/v1/flags", json="")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_flags_post_admin():
|
||||
"""Can a user post /api/v1/flags if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.post(
|
||||
"/api/v1/flags",
|
||||
json={"content": "flag", "type": "static", "challenge": 1},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_flag_types_get_admin():
|
||||
"""Can a user get /api/v1/flags/types[/<type_name>] if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.get("/api/v1/flags/types", json="")
|
||||
assert r.status_code == 200
|
||||
r = client.get("/api/v1/flags/types/static", json="")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_flag_get_admin():
|
||||
"""Can a user get /api/v1/flags/<flag_id> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
gen_flag(app.db, 1)
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.get("/api/v1/flags/1", json="")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_flag_patch_admin():
|
||||
"""Can a user patch /api/v1/flags/<flag_id> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
gen_flag(app.db, 1)
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.patch(
|
||||
"/api/v1/flags/1",
|
||||
json={"content": "flag_edit", "data": "", "type": "static", "id": "1"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"]["content"] == "flag_edit"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_flag_delete_admin():
|
||||
"""Can a user patch /api/v1/flags/<flag_id> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
gen_flag(app.db, 1)
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.delete("/api/v1/flags/1", json="")
|
||||
assert r.status_code == 200
|
||||
assert r.get_json().get("data") is None
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_flag_content_stripped_on_create_and_update():
|
||||
"""Test that flag content is stripped of whitespace on create and update"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
with login_as_user(app, "admin") as client:
|
||||
# Create flag with whitespace
|
||||
r = client.post(
|
||||
"/api/v1/flags",
|
||||
json={
|
||||
"content": " flag_with_spaces ",
|
||||
"type": "static",
|
||||
"challenge": 1,
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
data = r.get_json()["data"]
|
||||
assert data["content"] == "flag_with_spaces"
|
||||
|
||||
flag_id = data["id"]
|
||||
|
||||
f = Flags.query.filter_by(id=flag_id).first()
|
||||
assert f.content == "flag_with_spaces"
|
||||
|
||||
# Update flag with whitespace
|
||||
r = client.patch(
|
||||
f"/api/v1/flags/{flag_id}",
|
||||
json={"content": " updated_flag ", "type": "static"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
data = r.get_json()["data"]
|
||||
assert data["content"] == "updated_flag"
|
||||
|
||||
f = Flags.query.filter_by(id=flag_id).first()
|
||||
assert f.content == "updated_flag"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_flag_content_stripped_on_create_and_update_regex():
|
||||
"""Test that regex flag content is stripped of whitespace on create and update"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
with login_as_user(app, "admin") as client:
|
||||
# Create regex flag with whitespace
|
||||
r = client.post(
|
||||
"/api/v1/flags",
|
||||
json={
|
||||
"content": " ^flag\\d+$ ",
|
||||
"type": "regex",
|
||||
"challenge": 1,
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
data = r.get_json()["data"]
|
||||
assert data["content"] == "^flag\\d+$"
|
||||
|
||||
flag_id = data["id"]
|
||||
|
||||
f = Flags.query.filter_by(id=flag_id).first()
|
||||
assert f.content == "^flag\\d+$"
|
||||
|
||||
# Update regex flag with whitespace
|
||||
r = client.patch(
|
||||
f"/api/v1/flags/{flag_id}",
|
||||
json={"content": " ^updated_flag\\d+$ ", "type": "regex"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
data = r.get_json()["data"]
|
||||
assert data["content"] == "^updated_flag\\d+$"
|
||||
|
||||
f = Flags.query.filter_by(id=flag_id).first()
|
||||
assert f.content == "^updated_flag\\d+$"
|
||||
|
||||
|
||||
def test_flag_content_not_stripped_on_other_types():
|
||||
"""Test that flag content is not stripped for non-static and non-regex types"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
with login_as_user(app, "admin") as client:
|
||||
# Create flag with a custom type
|
||||
r = client.post(
|
||||
"/api/v1/flags",
|
||||
json={
|
||||
"content": " custom_flag ",
|
||||
"type": "custom",
|
||||
"challenge": 1,
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
data = r.get_json()["data"]
|
||||
# Should not be stripped
|
||||
assert data["content"] == " custom_flag "
|
||||
|
||||
flag_id = data["id"]
|
||||
|
||||
f = Flags.query.filter_by(id=flag_id).first()
|
||||
assert f.content == " custom_flag "
|
||||
|
||||
# Update flag with whitespace
|
||||
r = client.patch(
|
||||
f"/api/v1/flags/{flag_id}",
|
||||
json={"content": " updated_custom_flag ", "type": "custom"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
data = r.get_json()["data"]
|
||||
assert data["content"] == " updated_custom_flag "
|
||||
|
||||
f = Flags.query.filter_by(id=flag_id).first()
|
||||
assert f.content == " updated_custom_flag "
|
||||
149
tests/api/v1/test_hints.py
Normal file
149
tests/api/v1/test_hints.py
Normal file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Hints
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_hint,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_api_hint_get_non_admin():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
# test_api_hint_get_non_admin
|
||||
"""Can the users get /api/v1/hints if not admin"""
|
||||
r = client.get("/api/v1/hints", json="")
|
||||
assert r.status_code == 403
|
||||
assert Hints.query.count() == 0
|
||||
|
||||
# test_api_hint_post_non_admin
|
||||
"""Can the users post /api/v1/hints if not admin"""
|
||||
r = client.post("/api/v1/hints", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_hint_get_admin():
|
||||
"""Can the users get /api/v1/hints if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.get("/api/v1/hints", json="")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_hint_post_admin():
|
||||
"""Can the users post /api/v1/hints if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.post(
|
||||
"/api/v1/hints", json={"content": "hint", "cost": "1", "challenge": 1}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert Hints.query.count() == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_admins_can_preview_hints():
|
||||
"""Test that admins are able to bypass restrictions and preview hints with ?preview=true"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
gen_hint(app.db, challenge_id=1, cost=100)
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 200
|
||||
hint = r.get_json()
|
||||
assert hint.get("content") is None
|
||||
|
||||
r = client.get("/api/v1/hints/1?preview=true")
|
||||
assert r.status_code == 200
|
||||
hint = r.get_json()
|
||||
assert hint["data"]["content"] == "This is a hint"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_users_cannot_preview_hints():
|
||||
"""Test that users aren't able to preview hints"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
gen_hint(app.db, challenge_id=1, cost=100)
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 200
|
||||
hint = r.get_json()
|
||||
assert hint.get("content") is None
|
||||
|
||||
r = client.get("/api/v1/hints/1?preview=true")
|
||||
assert r.status_code == 200
|
||||
hint = r.get_json()
|
||||
assert hint["data"].get("content") is None
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_admin_cannot_unlock_hint_with_prerequisite():
|
||||
"""
|
||||
Test that admins cannot unlock hints that have a prerequisite unless the prerequisite is unlocked.
|
||||
Allow admin preview access with ?preview=true
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Create a challenge and two hints, where hint2 requires hint1 as a prerequisite
|
||||
chal = gen_challenge(app.db)
|
||||
hint1 = gen_hint(app.db, challenge_id=chal.id, content="First hint")
|
||||
hint1_id = hint1.id
|
||||
hint2 = gen_hint(
|
||||
app.db,
|
||||
challenge_id=chal.id,
|
||||
content="Second hint",
|
||||
)
|
||||
hint2.requirements = {"prerequisites": [1]}
|
||||
hint2_id = hint2.id
|
||||
app.db.session.commit()
|
||||
|
||||
# Login as admin
|
||||
client = login_as_user(app, name="admin")
|
||||
|
||||
# Try to access the second hint without unlocking the prerequisite
|
||||
r = client.get(f"/api/v1/hints/{hint2_id}")
|
||||
assert r.status_code == 403
|
||||
data = r.get_json()
|
||||
assert "requirements" in data.get("errors", {})
|
||||
|
||||
# Try to access with preview=true (should succeed for admin)
|
||||
r = client.get(f"/api/v1/hints/{hint2_id}?preview=true")
|
||||
assert r.status_code == 200
|
||||
data = r.get_json()
|
||||
assert data["data"]["content"] == "Second hint"
|
||||
|
||||
# Unlock the first hint
|
||||
r = client.post("/api/v1/unlocks", json={"target": hint1_id, "type": "hints"})
|
||||
assert r.status_code == 200
|
||||
|
||||
# Now try to access the second hint (should fail b/c missing unlock)
|
||||
r = client.get(f"/api/v1/hints/{hint2_id}")
|
||||
data = r.get_json()
|
||||
assert data["data"].get("content") is None
|
||||
|
||||
# Unlock the second hint
|
||||
r = client.post("/api/v1/unlocks", json={"target": hint2_id, "type": "hints"})
|
||||
assert r.status_code == 200
|
||||
|
||||
# Now try to access the second hint (should succeed)
|
||||
r = client.get(f"/api/v1/hints/{hint2_id}")
|
||||
assert r.status_code == 200
|
||||
data = r.get_json()
|
||||
assert data["data"]["content"] == "Second hint"
|
||||
destroy_ctfd(app)
|
||||
81
tests/api/v1/test_notifications.py
Normal file
81
tests/api/v1/test_notifications.py
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Notifications
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_notification,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_api_notifications_get():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
gen_notification(app.db)
|
||||
with login_as_user(app) as client:
|
||||
# test_api_notifications_get
|
||||
"""Can the users get /api/v1/notifications"""
|
||||
r = client.get("/api/v1/notifications", json="")
|
||||
assert r.status_code == 200
|
||||
assert len(r.get_json()["data"]) == 1
|
||||
|
||||
# test_api_get_notification_detail
|
||||
r = client.get("/api/v1/notifications/1", json="")
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["title"] == "title"
|
||||
assert resp["data"]["content"] == "content"
|
||||
|
||||
# test_api_notifications_post_non_admin
|
||||
"""Can the users post /api/v1/notifications if not admin"""
|
||||
r = client.post("/api/v1/notifications", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_notifications_post_admin():
|
||||
"""Can the users post /api/v1/notifications if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.post(
|
||||
"/api/v1/notifications", json={"title": "title", "content": "content"}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_delete_notifications_by_admin():
|
||||
"""Test that an admin can delete notifications"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
gen_notification(app.db)
|
||||
assert Notifications.query.count() == 1
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.delete("/api/v1/notifications/1", json="")
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["success"] is True
|
||||
assert Notifications.query.count() == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_delete_notifications_by_user():
|
||||
"""Test that a non-admin cannot delete notifications"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
gen_challenge(app.db)
|
||||
gen_notification(app.db)
|
||||
assert Notifications.query.count() == 1
|
||||
with login_as_user(app) as client:
|
||||
r = client.delete("/api/v1/notifications/1", json="")
|
||||
assert r.status_code == 403
|
||||
assert Notifications.query.count() == 1
|
||||
destroy_ctfd(app)
|
||||
126
tests/api/v1/test_pages.py
Normal file
126
tests/api/v1/test_pages.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_page,
|
||||
login_as_user,
|
||||
)
|
||||
|
||||
|
||||
def test_api_pages_get_non_admin():
|
||||
"""Can a user get /api/v1/pages if not admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
gen_page(app.db, title="title", route="/route", content="content")
|
||||
|
||||
r = client.get("/api/v1/pages", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_pages_post_non_admin
|
||||
"""Can a user post /api/v1/pages if not admin"""
|
||||
r = client.post("/api/v1/pages")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_page_get_non_admin
|
||||
"""Can a user get /api/v1/pages/<page_id> if not admin"""
|
||||
r = client.get("/api/v1/pages/2", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_page_patch_non_admin
|
||||
r = client.patch("/api/v1/pages/2", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_page_delete_non_admin
|
||||
"""Can a user delete /api/v1/pages/<page_id> if not admin"""
|
||||
r = client.delete("/api/v1/pages/2", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_pages_get_admin():
|
||||
"""Can a user get /api/v1/pages if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.get("/api/v1/pages", json="")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_pages_post_admin():
|
||||
"""Can a user post /api/v1/pages if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
with login_as_user(app, name="admin") as client:
|
||||
with client.session_transaction() as sess:
|
||||
nonce = sess.get("nonce")
|
||||
r = client.post(
|
||||
"/api/v1/pages",
|
||||
json={
|
||||
"title": "testing_page_title",
|
||||
"route": "/route",
|
||||
"content": "testing_page_content",
|
||||
"nonce": nonce,
|
||||
"auth_required": False,
|
||||
},
|
||||
)
|
||||
r = client.get("/")
|
||||
assert r.status_code == 200
|
||||
assert "testing_page_title" in r.get_data(as_text=True)
|
||||
|
||||
r = client.get("/route")
|
||||
assert r.status_code == 200
|
||||
assert "testing_page_content" in r.get_data(as_text=True)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_page_get_admin():
|
||||
"""Can a user get /api/v1/pages/<page_id> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_page(app.db, title="title", route="/route", content="content")
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.get("/api/v1/pages/2", json="")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_page_patch_admin():
|
||||
"""Can a user patch /api/v1/pages/<page_id> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_page(app.db, title="title", route="/route", content="content")
|
||||
with login_as_user(app, "admin") as client:
|
||||
with client.session_transaction() as sess:
|
||||
nonce = sess.get("nonce")
|
||||
r = client.patch(
|
||||
"/api/v1/pages/2",
|
||||
json={
|
||||
"title": "Title",
|
||||
"route": "/route",
|
||||
"content": "content_edit",
|
||||
"id": "2",
|
||||
"nonce": nonce,
|
||||
"auth_required": False,
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"]["content"] == "content_edit"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_page_delete_admin():
|
||||
"""Can a user patch /api/v1/pages/<page_id> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_page(app.db, title="title", route="/route", content="content")
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.delete("/api/v1/pages/2", json="")
|
||||
assert r.status_code == 200
|
||||
assert r.get_json().get("data") is None
|
||||
destroy_ctfd(app)
|
||||
193
tests/api/v1/test_scoreboard.py
Normal file
193
tests/api/v1/test_scoreboard.py
Normal file
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from flask import jsonify
|
||||
|
||||
from CTFd.cache import clear_standings
|
||||
from CTFd.models import Users
|
||||
from CTFd.utils.scoreboard import get_scoreboard_detail
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_award,
|
||||
gen_challenge,
|
||||
gen_flag,
|
||||
gen_solve,
|
||||
gen_team,
|
||||
gen_user,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_scoreboard_is_cached():
|
||||
"""Test that /api/v1/scoreboard is properly cached and cleared"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# create user1
|
||||
register_user(app, name="user1", email="user1@examplectf.com")
|
||||
|
||||
# create challenge
|
||||
chal = gen_challenge(app.db, value=100)
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
chal_id = chal.id
|
||||
|
||||
# create a solve for the challenge for user1. (the id is 2 because of the admin)
|
||||
gen_solve(app.db, user_id=2, challenge_id=chal_id)
|
||||
|
||||
# Initial get_scoreboard_detail cache key version
|
||||
saved = app.cache.get("CTFd.utils.scoreboard.get_scoreboard_detail_memver")
|
||||
|
||||
with login_as_user(app, "user1") as client:
|
||||
# Check basic scoreboard data
|
||||
assert app.cache.get("view/api.scoreboard_scoreboard_list") is None
|
||||
client.get("/api/v1/scoreboard")
|
||||
assert app.cache.get("view/api.scoreboard_scoreboard_list")
|
||||
|
||||
# Check detailed scoreboard data
|
||||
orig = jsonify(get_scoreboard_detail.uncached(count=10)).get_json()
|
||||
assert (
|
||||
app.cache.get("CTFd.utils.scoreboard.get_scoreboard_detail_memver")
|
||||
== saved
|
||||
)
|
||||
cached = client.get("/api/v1/scoreboard/top/10").get_json()
|
||||
assert cached["data"] == orig
|
||||
assert app.cache.get("CTFd.utils.scoreboard.get_scoreboard_detail_memver")
|
||||
|
||||
# Empty standings and check that the cached data is gone
|
||||
clear_standings()
|
||||
assert app.cache.get("view/api.scoreboard_scoreboard_list") is None
|
||||
# Clearing an entire function bumps flask-cachings version identify instead of setting it to null
|
||||
new = app.cache.get("CTFd.utils.scoreboard.get_scoreboard_detail_memver")
|
||||
assert new != saved
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_scoreboard_tie_break_ordering_with_awards():
|
||||
"""
|
||||
Test that scoreboard tie break ordering respects the addition of awards
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# create user1
|
||||
register_user(app, name="user1", email="user1@examplectf.com")
|
||||
# create user2
|
||||
register_user(app, name="user2", email="user2@examplectf.com")
|
||||
|
||||
chal = gen_challenge(app.db, value=100)
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
|
||||
chal = gen_challenge(app.db, value=200)
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
|
||||
# create solves for the challenges. (the user_ids are off by 1 because of the admin)
|
||||
gen_solve(app.db, user_id=2, challenge_id=1)
|
||||
gen_solve(app.db, user_id=3, challenge_id=2)
|
||||
|
||||
with login_as_user(app, "user1") as client:
|
||||
r = client.get("/api/v1/scoreboard")
|
||||
resp = r.get_json()
|
||||
assert len(resp["data"]) == 2
|
||||
assert resp["data"][0]["name"] == "user2"
|
||||
assert resp["data"][0]["score"] == 200
|
||||
assert resp["data"][1]["name"] == "user1"
|
||||
assert resp["data"][1]["score"] == 100
|
||||
|
||||
# Give user1 an award for 100 points.
|
||||
# At this point user2 should still be ahead
|
||||
gen_award(app.db, user_id=2, value=100)
|
||||
|
||||
with login_as_user(app, "user1") as client:
|
||||
r = client.get("/api/v1/scoreboard")
|
||||
resp = r.get_json()
|
||||
assert len(resp["data"]) == 2
|
||||
assert resp["data"][0]["name"] == "user2"
|
||||
assert resp["data"][0]["score"] == 200
|
||||
assert resp["data"][1]["name"] == "user1"
|
||||
assert resp["data"][1]["score"] == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_scoreboard_tie_break_ordering_with_awards_under_teams():
|
||||
"""
|
||||
Test that team mode scoreboard tie break ordering respects the addition of awards
|
||||
"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
gen_team(app.db, name="team1", email="team1@examplectf.com")
|
||||
gen_team(app.db, name="team2", email="team2@examplectf.com")
|
||||
|
||||
chal = gen_challenge(app.db, value=100)
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
|
||||
chal = gen_challenge(app.db, value=200)
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
|
||||
# create solves for the challenges. (the user_ids are off by 1 because of the admin)
|
||||
gen_solve(app.db, user_id=2, team_id=1, challenge_id=1)
|
||||
gen_solve(app.db, user_id=6, team_id=2, challenge_id=2)
|
||||
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
|
||||
with login_as_user(app, user.name) as client:
|
||||
r = client.get("/api/v1/scoreboard")
|
||||
resp = r.get_json()
|
||||
print(resp)
|
||||
assert len(resp["data"]) == 2
|
||||
assert resp["data"][0]["name"] == "team2"
|
||||
assert resp["data"][0]["score"] == 200
|
||||
assert resp["data"][1]["name"] == "team1"
|
||||
assert resp["data"][1]["score"] == 100
|
||||
|
||||
# Give a user on the team an award for 100 points.
|
||||
# At this point team2 should still be ahead
|
||||
gen_award(app.db, user_id=3, team_id=1, value=100)
|
||||
|
||||
with login_as_user(app, user.name) as client:
|
||||
r = client.get("/api/v1/scoreboard")
|
||||
resp = r.get_json()
|
||||
print(resp)
|
||||
assert len(resp["data"]) == 2
|
||||
assert resp["data"][0]["name"] == "team2"
|
||||
assert resp["data"][0]["score"] == 200
|
||||
assert resp["data"][1]["name"] == "team1"
|
||||
assert resp["data"][1]["score"] == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_scoreboard_detail_returns_different_counts():
|
||||
"""
|
||||
Test that /api/v1/scoreboard/top/10 and /api/v1/scoreboard/top/1
|
||||
return different amounts of values even when cached
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Create multiple users
|
||||
for i in range(2, 13):
|
||||
gen_user(app.db, name=f"user{i}", email=f"user{i}@examplectf.com")
|
||||
|
||||
# Create a challenge
|
||||
chal = gen_challenge(app.db, value=100)
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
|
||||
# Generate solves for the challenge for multiple users
|
||||
for user_id in range(2, 13): # User IDs start from 2 (admin is 1)
|
||||
gen_solve(app.db, user_id=user_id, challenge_id=chal.id)
|
||||
|
||||
with login_as_user(app, name="user2") as client:
|
||||
# Fetch top 10 scores
|
||||
top_10_resp = client.get("/api/v1/scoreboard/top/10").get_json()
|
||||
assert len(top_10_resp["data"]) == 10
|
||||
|
||||
# Fetch top 1 score
|
||||
top_1_resp = client.get("/api/v1/scoreboard/top/1").get_json()
|
||||
assert len(top_1_resp["data"]) == 1
|
||||
|
||||
# Ensure the results are different
|
||||
assert top_10_resp["data"] != top_1_resp["data"]
|
||||
|
||||
# Fetch scores again
|
||||
assert top_10_resp == client.get("/api/v1/scoreboard/top/10").get_json()
|
||||
assert top_1_resp == client.get("/api/v1/scoreboard/top/1").get_json()
|
||||
|
||||
destroy_ctfd(app)
|
||||
1023
tests/api/v1/test_solutions.py
Normal file
1023
tests/api/v1/test_solutions.py
Normal file
File diff suppressed because it is too large
Load Diff
230
tests/api/v1/test_submissions.py
Normal file
230
tests/api/v1/test_submissions.py
Normal file
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Discards, Fails, Solves
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_fail,
|
||||
gen_solve,
|
||||
gen_team,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_api_submissions_get_non_admin():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
gen_solve(app.db, user_id=1)
|
||||
with app.test_client() as client:
|
||||
# test_api_submissions_get_non_admin
|
||||
"""Can a user get /api/v1/submissions if not admin"""
|
||||
r = client.get("/api/v1/submissions", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_submissions_post_non_admin
|
||||
"""Can a user post /api/v1/submissions if not admin"""
|
||||
r = client.post("/api/v1/submissions")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_submission_get_non_admin
|
||||
"""Can a user get /api/v1/submissions/<submission_id> if not admin"""
|
||||
r = client.get("/api/v1/submissions/1", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_submission_delete_non_admin
|
||||
"""Can a user delete /api/v1/submissions/<submission_id> if not admin"""
|
||||
r = client.delete("/api/v1/submissions/1", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_submissions_get_admin():
|
||||
"""Can a user get /api/v1/submissions if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.get("/api/v1/submissions", json="")
|
||||
assert r.status_code == 200
|
||||
r = client.get("/api/v1/submissions?user_id=1", json="")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_submissions_post_admin():
|
||||
"""Can a user post /api/v1/submissions if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.post(
|
||||
"/api/v1/submissions",
|
||||
json={
|
||||
"provided": "MARKED AS SOLVED BY ADMIN",
|
||||
"user_id": 1,
|
||||
"team_id": None,
|
||||
"challenge_id": "1",
|
||||
"type": "correct",
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_submission_get_admin():
|
||||
"""Can a user get /api/v1/submissions/<submission_id> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_solve(app.db, user_id=1)
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.get("/api/v1/submissions/1", json="")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_submission_delete_admin():
|
||||
"""Can a user patch /api/v1/submissions/<submission_id> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_solve(app.db, user_id=1)
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.delete("/api/v1/submissions/1", json="")
|
||||
assert r.status_code == 200
|
||||
assert r.get_json().get("data") is None
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_submission_patch_correct():
|
||||
"""Test that patching a submission to correct creates a solve"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
gen_challenge(app.db)
|
||||
gen_fail(app.db, challenge_id=1, user_id=2)
|
||||
assert Solves.query.count() == 0
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.patch("/api/v1/submissions/1", json={"type": "correct"})
|
||||
assert r.status_code == 200
|
||||
assert Fails.query.count() == 0
|
||||
assert Solves.query.count() == 1
|
||||
assert Discards.query.count() == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_submission_patch_correct_with_existing_solve_fails():
|
||||
"""Test that patching a submission to correct fails if a solve already exists"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
gen_challenge(app.db)
|
||||
gen_fail(app.db, challenge_id=1, user_id=2)
|
||||
gen_solve(app.db, challenge_id=1, user_id=2)
|
||||
assert Solves.query.count() == 1
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.patch("/api/v1/submissions/1", json={"type": "correct"})
|
||||
data = r.get_json()
|
||||
assert r.status_code == 400
|
||||
assert data["success"] is False
|
||||
assert Solves.query.count() == 1
|
||||
assert Fails.query.count() == 1
|
||||
assert Discards.query.count() == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_submission_patch_correct_scoreboard():
|
||||
"If we adjust a submission for someone the scoreboard should be correct accounting for the time of the adjusted submission"
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Create 2 test users
|
||||
register_user(app, name="user1", email="user1@examplectf.com")
|
||||
register_user(app, name="user2", email="user2@examplectf.com")
|
||||
|
||||
# Create 2 test challenges
|
||||
gen_challenge(app.db, name="chal1")
|
||||
gen_challenge(app.db, name="chal2")
|
||||
|
||||
# Give the first test user only fails
|
||||
gen_fail(app.db, challenge_id=1, user_id=2)
|
||||
gen_fail(app.db, challenge_id=2, user_id=2)
|
||||
# Give the second test user only solves
|
||||
gen_solve(app.db, challenge_id=1, user_id=3)
|
||||
gen_solve(app.db, challenge_id=2, user_id=3)
|
||||
with login_as_user(app, "admin") as client:
|
||||
# user2 who has both solves should be considered on top
|
||||
scoreboard = client.get("/api/v1/scoreboard").get_json()["data"]
|
||||
assert len(scoreboard) == 1
|
||||
assert scoreboard[0]["name"] == "user2"
|
||||
|
||||
# We mark user1's first solve as correct
|
||||
# This should give them 100 points
|
||||
# It should not place them above user2 who has 200 points
|
||||
client.patch("/api/v1/submissions/1", json={"type": "correct"})
|
||||
scoreboard = client.get("/api/v1/scoreboard").get_json()["data"]
|
||||
assert len(scoreboard) == 2
|
||||
assert scoreboard[0]["name"] == "user2"
|
||||
assert scoreboard[1]["name"] == "user1"
|
||||
assert scoreboard[1]["score"] == 100
|
||||
|
||||
# We mark user1's second solve as correct
|
||||
# This should give them 200 points
|
||||
# It should place them above user2 who has 200 points but was not the first to solve the challenge
|
||||
# Based on time user1's attempts should be considered correct and first
|
||||
client.patch("/api/v1/submissions/2", json={"type": "correct"})
|
||||
scoreboard = client.get("/api/v1/scoreboard").get_json()["data"]
|
||||
assert len(scoreboard) == 2
|
||||
assert scoreboard[0]["name"] == "user1"
|
||||
assert scoreboard[0]["score"] == 200
|
||||
assert scoreboard[1]["name"] == "user2"
|
||||
assert scoreboard[1]["score"] == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_submission_patch_correct_scoreboard_teams():
|
||||
"If we adjust a submission for a team the scoreboard should be correct after the adjustment"
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
# Create 2 test teams each with only 1 user
|
||||
gen_team(app.db, name="team1", email="team1@examplectf.com", member_count=1)
|
||||
gen_team(app.db, name="team2", email="team2@examplectf.com", member_count=1)
|
||||
|
||||
# Create 2 test challenges
|
||||
gen_challenge(app.db, name="chal1")
|
||||
gen_challenge(app.db, name="chal2")
|
||||
|
||||
# Assign only fails to the user in team 1
|
||||
gen_fail(app.db, challenge_id=1, user_id=2, team_id=1)
|
||||
gen_fail(app.db, challenge_id=2, user_id=2, team_id=1)
|
||||
# Assign only solves to the user in team 2
|
||||
gen_solve(app.db, challenge_id=1, user_id=3, team_id=2)
|
||||
gen_solve(app.db, challenge_id=2, user_id=3, team_id=2)
|
||||
|
||||
with login_as_user(app, "admin") as client:
|
||||
# team2 who has both solves should be considered on top
|
||||
scoreboard = client.get("/api/v1/scoreboard").get_json()["data"]
|
||||
assert len(scoreboard) == 1
|
||||
assert scoreboard[0]["name"] == "team2"
|
||||
|
||||
# We then convert the first submission which should give team1 100 points
|
||||
# team1 should also now appear on the scoreboard in 2nd place
|
||||
client.patch("/api/v1/submissions/1", json={"type": "correct"})
|
||||
scoreboard = client.get("/api/v1/scoreboard").get_json()["data"]
|
||||
assert len(scoreboard) == 2
|
||||
assert scoreboard[0]["name"] == "team2"
|
||||
assert scoreboard[1]["name"] == "team1"
|
||||
assert scoreboard[1]["score"] == 100
|
||||
|
||||
# We mark team1's second solve as correct
|
||||
# This should give them 200 points
|
||||
# It should place them above team2 who has 200 points but was not the first to solve the challenge
|
||||
# Based on time team1's attempts should be considered correct and first
|
||||
client.patch("/api/v1/submissions/2", json={"type": "correct"})
|
||||
scoreboard = client.get("/api/v1/scoreboard").get_json()["data"]
|
||||
assert len(scoreboard) == 2
|
||||
assert scoreboard[0]["name"] == "team1"
|
||||
assert scoreboard[0]["score"] == 200
|
||||
assert scoreboard[1]["name"] == "team2"
|
||||
assert scoreboard[1]["score"] == 200
|
||||
destroy_ctfd(app)
|
||||
103
tests/api/v1/test_tags.py
Normal file
103
tests/api/v1/test_tags.py
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_tag,
|
||||
login_as_user,
|
||||
)
|
||||
|
||||
|
||||
def test_api_tags_get_non_admin():
|
||||
"""Can a user get /api/v1/tags if not admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
gen_tag(app.db, 1)
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/tags", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_tags_post_non_admin
|
||||
"""Can a user post /api/v1/tags if not admin"""
|
||||
r = client.post("/api/v1/tags")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_tag_get_non_admin
|
||||
"""Can a user get /api/v1/tags/<tag_id> if not admin"""
|
||||
r = client.get("/api/v1/tags/1", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_tag_patch_non_admin
|
||||
"""Can a user patch /api/v1/tags/<tag_id> if not admin"""
|
||||
r = client.patch("/api/v1/tags/1", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# test_api_tag_delete_non_admin
|
||||
"""Can a user delete /api/v1/tags/<tag_id> if not admin"""
|
||||
r = client.delete("/api/v1/tags/1", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_tags_get_admin():
|
||||
"""Can a user get /api/v1/tags if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.get("/api/v1/tags", json="")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_tags_post_admin():
|
||||
"""Can a user post /api/v1/tags if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.post("/api/v1/tags", json={"value": "tag", "challenge": 1})
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_tag_get_admin():
|
||||
"""Can a user get /api/v1/tags/<tag_id> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
gen_tag(app.db, 1)
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.get("/api/v1/tags/1", json="")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_tag_patch_admin():
|
||||
"""Can a user patch /api/v1/tags/<tag_id> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
gen_tag(app.db, 1)
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.patch(
|
||||
"/api/v1/tags/1", json={"value": "tag_edit", "challenge_id": 1}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"]["value"] == "tag_edit"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_tag_delete_admin():
|
||||
"""Can a user delete /api/v1/tags/<tag_id> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
gen_tag(app.db, 1)
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.delete("/api/v1/tags/1", json="")
|
||||
assert r.status_code == 200
|
||||
assert r.get_json().get("data") is None
|
||||
destroy_ctfd(app)
|
||||
891
tests/api/v1/test_teams.py
Normal file
891
tests/api/v1/test_teams.py
Normal file
@@ -0,0 +1,891 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from CTFd.models import Awards, Fails, Solves, Teams, Users
|
||||
from CTFd.utils import set_config
|
||||
from CTFd.utils.crypto import verify_password
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_award,
|
||||
gen_challenge,
|
||||
gen_fail,
|
||||
gen_flag,
|
||||
gen_solve,
|
||||
gen_team,
|
||||
gen_user,
|
||||
login_as_user,
|
||||
register_user,
|
||||
simulate_user_activity,
|
||||
)
|
||||
|
||||
|
||||
def test_api_teams_get_public():
|
||||
"""Can a user get /api/v1/teams if teams are public"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
set_config("account_visibility", "public")
|
||||
r = client.get("/api/v1/teams")
|
||||
assert r.status_code == 200
|
||||
set_config("account_visibility", "private")
|
||||
r = client.get("/api/v1/teams")
|
||||
assert r.status_code == 302
|
||||
set_config("account_visibility", "admins")
|
||||
r = client.get("/api/v1/teams")
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_teams_get_private():
|
||||
"""Can a user get /api/v1/teams if teams are private"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
set_config("account_visibility", "public")
|
||||
r = client.get("/api/v1/teams")
|
||||
assert r.status_code == 200
|
||||
set_config("account_visibility", "private")
|
||||
r = client.get("/api/v1/teams")
|
||||
assert r.status_code == 200
|
||||
set_config("account_visibility", "admins")
|
||||
r = client.get("/api/v1/teams")
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_teams_get_admin():
|
||||
"""Can a user get /api/v1/teams if teams are viewed by admins only"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as client:
|
||||
set_config("account_visibility", "public")
|
||||
r = client.get("/api/v1/teams")
|
||||
assert r.status_code == 200
|
||||
set_config("account_visibility", "private")
|
||||
r = client.get("/api/v1/teams")
|
||||
assert r.status_code == 200
|
||||
set_config("account_visibility", "admins")
|
||||
r = client.get("/api/v1/teams")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_teams_post_non_admin():
|
||||
"""Can a user post /api/v1/teams if not admin"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.post("/api/v1/teams", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_teams_post_admin():
|
||||
"""Can a user post /api/v1/teams if admin"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as client:
|
||||
# Create team
|
||||
r = client.post(
|
||||
"/api/v1/teams",
|
||||
json={
|
||||
"website": "http://www.team.com",
|
||||
"name": "team",
|
||||
"country": "TW",
|
||||
"email": "team@team.com",
|
||||
"affiliation": "team",
|
||||
"password": "password",
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
# Make sure password was hashed properly
|
||||
team = Teams.query.filter_by(email="team@team.com").first()
|
||||
assert team
|
||||
assert verify_password("password", team.password)
|
||||
|
||||
# Make sure team can actually be joined
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "team",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/teams/join", data=data)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.team_id == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_teams_post_admin_duplicate():
|
||||
"""Test that admins can only create teams with unique information"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
gen_team(app.db, name="team1")
|
||||
with login_as_user(app, "admin") as client:
|
||||
# Duplicate name
|
||||
r = client.post(
|
||||
"/api/v1/teams",
|
||||
json={
|
||||
"website": "https://examplectf.com",
|
||||
"name": "team1",
|
||||
"country": "TW",
|
||||
"email": "team1@examplectf.com",
|
||||
"affiliation": "team",
|
||||
"password": "password",
|
||||
},
|
||||
)
|
||||
resp = r.get_json()
|
||||
assert r.status_code == 400
|
||||
assert resp["errors"]["name"]
|
||||
assert resp["success"] is False
|
||||
assert Teams.query.count() == 1
|
||||
|
||||
# Duplicate email
|
||||
r = client.post(
|
||||
"/api/v1/teams",
|
||||
json={
|
||||
"website": "https://examplectf.com",
|
||||
"name": "new_team",
|
||||
"country": "TW",
|
||||
"email": "team@examplectf.com",
|
||||
"affiliation": "team",
|
||||
"password": "password",
|
||||
},
|
||||
)
|
||||
resp = r.get_json()
|
||||
assert r.status_code == 400
|
||||
assert resp["errors"]["email"]
|
||||
assert resp["success"] is False
|
||||
assert Teams.query.count() == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_public():
|
||||
"""Can a user get /api/v1/team/<team_id> if teams are public"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
set_config("account_visibility", "public")
|
||||
gen_team(app.db)
|
||||
r = client.get("/api/v1/teams/1")
|
||||
assert r.status_code == 200
|
||||
set_config("account_visibility", "private")
|
||||
r = client.get("/api/v1/teams/1")
|
||||
assert r.status_code == 302
|
||||
set_config("account_visibility", "admins")
|
||||
r = client.get("/api/v1/teams/1")
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_private():
|
||||
"""Can a user get /api/v1/teams/<team_id> if teams are private"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
set_config("account_visibility", "public")
|
||||
gen_team(app.db)
|
||||
r = client.get("/api/v1/teams/1")
|
||||
assert r.status_code == 200
|
||||
set_config("account_visibility", "private")
|
||||
r = client.get("/api/v1/teams/1")
|
||||
assert r.status_code == 200
|
||||
set_config("account_visibility", "admins")
|
||||
r = client.get("/api/v1/teams/1")
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_admin():
|
||||
"""Can a user get /api/v1/teams/<team_id> if teams are viewed by admins only"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as client:
|
||||
gen_team(app.db)
|
||||
set_config("account_visibility", "public")
|
||||
r = client.get("/api/v1/teams/1")
|
||||
assert r.status_code == 200
|
||||
set_config("account_visibility", "private")
|
||||
r = client.get("/api/v1/teams/1")
|
||||
assert r.status_code == 200
|
||||
set_config("account_visibility", "admins")
|
||||
r = client.get("/api/v1/teams/1")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_patch_non_admin():
|
||||
"""Can a user patch /api/v1/teams/<team_id> if not admin"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
gen_team(app.db)
|
||||
with app.test_client() as client:
|
||||
r = client.patch("/api/v1/teams/1", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_patch_admin():
|
||||
"""Can a user patch /api/v1/teams/<team_id> if admin"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
gen_team(app.db)
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.patch(
|
||||
"/api/v1/teams/1",
|
||||
json={
|
||||
"name": "team_name",
|
||||
"email": "team@examplectf.com",
|
||||
"password": "password",
|
||||
"affiliation": "changed",
|
||||
},
|
||||
)
|
||||
team = Teams.query.filter_by(id=1).first()
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"]["affiliation"] == "changed"
|
||||
assert verify_password("password", team.password)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_delete_non_admin():
|
||||
"""Can a user delete /api/v1/teams/<team_id> if not admin"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
gen_team(app.db)
|
||||
with app.test_client() as client:
|
||||
r = client.delete("/api/v1/teams/1", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_delete_admin():
|
||||
"""Can a user patch /api/v1/teams/<team_id> if admin"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
team = gen_team(app.db)
|
||||
|
||||
assert len(team.members) == 4
|
||||
|
||||
members = team.members
|
||||
for user in members:
|
||||
simulate_user_activity(app.db, user=user)
|
||||
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.delete("/api/v1/teams/1", json="")
|
||||
assert r.status_code == 200
|
||||
assert r.get_json().get("data") is None
|
||||
|
||||
for user in Users.query.all():
|
||||
assert user.team_id is None
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_me_not_logged_in():
|
||||
"""Can a user get /api/v1/teams/me if not logged in"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/teams/me")
|
||||
assert r.status_code == 302
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_me_logged_in():
|
||||
"""Can a user get /api/v1/teams/me if logged in"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user = gen_user(app.db)
|
||||
team = gen_team(app.db)
|
||||
team.members.append(user)
|
||||
user.team_id = team.id
|
||||
app.db.session.commit()
|
||||
with login_as_user(app, name="user_name") as client:
|
||||
r = client.get("/api/v1/teams/me")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_patch_me_not_logged_in():
|
||||
"""Can a user patch /api/v1/teams/me if not logged in"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.patch("/api/v1/teams/me", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_patch_me_logged_in_user():
|
||||
"""Can a user patch /api/v1/teams/me if logged in as a regular user"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user1 = gen_user(app.db, name="user1", email="user1@examplectf.com")
|
||||
user2 = gen_user(app.db, name="user2", email="user2@examplectf.com")
|
||||
team = gen_team(app.db)
|
||||
team.members.append(user1)
|
||||
team.members.append(user2)
|
||||
user1.team_id = team.id
|
||||
user2.team_id = team.id
|
||||
app.db.session.commit()
|
||||
with login_as_user(app, name="user2") as client:
|
||||
r = client.patch(
|
||||
"/api/v1/teams/me", json={"name": "team_name", "affiliation": "changed"}
|
||||
)
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_patch_me_logged_in_captain():
|
||||
"""Can a user patch /api/v1/teams/me if logged in as the captain"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user = gen_user(app.db)
|
||||
team = gen_team(app.db)
|
||||
team.members.append(user)
|
||||
team.captain_id = 2
|
||||
user.team_id = team.id
|
||||
app.db.session.commit()
|
||||
with login_as_user(app, name="user_name") as client:
|
||||
r = client.patch(
|
||||
"/api/v1/teams/me", json={"name": "team_name", "affiliation": "changed"}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_patch_me_logged_in_admin_captain():
|
||||
"""Can an admin patch /api/v1/teams/me if logged in as a team captain"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
admin = Users.query.filter_by(id=1).first()
|
||||
user = gen_user(app.db)
|
||||
team = gen_team(app.db)
|
||||
team.members.append(user)
|
||||
team.members.append(admin)
|
||||
|
||||
user.team_id = team.id
|
||||
admin.team_id = team.id
|
||||
|
||||
# We want the admin to be the captain
|
||||
team.captain_id = 1
|
||||
|
||||
app.db.session.commit()
|
||||
with login_as_user(app, name="admin") as client:
|
||||
# Users can't null out their team name
|
||||
r = client.patch("/api/v1/teams/me", json={"name": None})
|
||||
resp = r.get_json()
|
||||
assert r.status_code == 400
|
||||
assert resp["errors"]["name"] == ["Field may not be null."]
|
||||
|
||||
r = client.patch(
|
||||
"/api/v1/teams/me", json={"name": "team_name", "affiliation": "changed"}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
team = Teams.query.filter_by(id=1).first()
|
||||
assert team.name == "team_name"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_me_solves_not_logged_in():
|
||||
"""Can a user get /api/v1/teams/me/solves if not logged in"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/teams/me/solves", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_me_solves_logged_in():
|
||||
"""Can a user get /api/v1/teams/me/solves if logged in"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user = gen_user(app.db)
|
||||
team = gen_team(app.db)
|
||||
team.members.append(user)
|
||||
user.team_id = team.id
|
||||
app.db.session.commit()
|
||||
with login_as_user(app, name="user_name") as client:
|
||||
r = client.get("/api/v1/teams/me/solves")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_solves():
|
||||
"""Can a user get /api/v1/teams/<team_id>/solves if logged in"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user = gen_user(app.db)
|
||||
team = gen_team(app.db)
|
||||
team.members.append(user)
|
||||
user.team_id = team.id
|
||||
app.db.session.commit()
|
||||
with login_as_user(app, name="user_name") as client:
|
||||
r = client.get("/api/v1/teams/1/solves")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_solves_after_freze_time():
|
||||
"""Can a user get /api/v1/teams/<team_id>/solves after freeze time"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
team = gen_team(
|
||||
app.db, name="team1", email="team1@examplectf.com", member_count=1
|
||||
)
|
||||
|
||||
team_member = team.members[0]
|
||||
tm_name = team_member.name
|
||||
|
||||
set_config("freeze", "1507262400")
|
||||
with freeze_time("2017-10-4"):
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
gen_solve(app.db, user_id=3, team_id=1, challenge_id=chal_id)
|
||||
chal2 = gen_challenge(app.db)
|
||||
chal2_id = chal2.id
|
||||
|
||||
with freeze_time("2017-10-8"):
|
||||
gen_solve(app.db, user_id=3, team_id=1, challenge_id=chal2_id)
|
||||
|
||||
assert Solves.query.count() == 2
|
||||
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/api/v1/teams/1/solves")
|
||||
data = r.get_json()["data"]
|
||||
assert len(data) == 1
|
||||
|
||||
with login_as_user(app, name=tm_name) as client:
|
||||
r = client.get("/api/v1/teams/me/solves")
|
||||
data = r.get_json()["data"]
|
||||
assert len(data) == 2
|
||||
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.get("/api/v1/teams/1/solves")
|
||||
data = r.get_json()["data"]
|
||||
assert len(data) == 2
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_me_fails_not_logged_in():
|
||||
"""Can a user get /api/v1/teams/me/fails if not logged in"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/teams/me/fails", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_me_fails_logged_in():
|
||||
"""Can a user get /api/v1/teams/me/fails if logged in"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user = gen_user(app.db)
|
||||
team = gen_team(app.db)
|
||||
team.members.append(user)
|
||||
user.team_id = team.id
|
||||
app.db.session.commit()
|
||||
with login_as_user(app, name="user_name") as client:
|
||||
r = client.get("/api/v1/teams/me/fails")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_fails():
|
||||
"""Can a user get /api/v1/teams/<team_id>/fails if logged in"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user = gen_user(app.db)
|
||||
team = gen_team(app.db)
|
||||
team.members.append(user)
|
||||
user.team_id = team.id
|
||||
app.db.session.commit()
|
||||
with login_as_user(app, name="user_name") as client:
|
||||
r = client.get("/api/v1/teams/1/fails")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_fails_after_freze_time():
|
||||
"""Can a user get /api/v1/teams/<team_id>/fails after freeze time"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
team = gen_team(
|
||||
app.db, name="team1", email="team1@examplectf.com", member_count=1
|
||||
)
|
||||
|
||||
team_member = team.members[0]
|
||||
tm_name = team_member.name
|
||||
|
||||
set_config("freeze", "1507262400")
|
||||
with freeze_time("2017-10-4"):
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
chal2 = gen_challenge(app.db)
|
||||
chal2_id = chal2.id
|
||||
gen_fail(app.db, user_id=3, team_id=1, challenge_id=chal_id)
|
||||
|
||||
with freeze_time("2017-10-8"):
|
||||
gen_fail(app.db, user_id=3, team_id=1, challenge_id=chal2_id)
|
||||
|
||||
assert Fails.query.count() == 2
|
||||
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/api/v1/teams/1/fails")
|
||||
assert r.get_json()["meta"]["count"] == 1
|
||||
|
||||
with login_as_user(app, name=tm_name) as client:
|
||||
r = client.get("/api/v1/teams/me/fails")
|
||||
assert r.get_json()["meta"]["count"] == 2
|
||||
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.get("/api/v1/teams/1/fails")
|
||||
assert r.get_json()["meta"]["count"] == 2
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_me_awards_not_logged_in():
|
||||
"""Can a user get /api/v1/teams/me/awards if not logged in"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/teams/me/awards", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_me_awards_logged_in():
|
||||
"""Can a user get /api/v1/teams/me/awards if logged in"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user = gen_user(app.db)
|
||||
team = gen_team(app.db)
|
||||
team.members.append(user)
|
||||
user.team_id = team.id
|
||||
app.db.session.commit()
|
||||
with login_as_user(app, name="user_name") as client:
|
||||
r = client.get("/api/v1/teams/me/awards")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_awards():
|
||||
"""Can a user get /api/v1/teams/<team_id>/awards if logged in"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user = gen_user(app.db)
|
||||
team = gen_team(app.db)
|
||||
team.members.append(user)
|
||||
user.team_id = team.id
|
||||
app.db.session.commit()
|
||||
with login_as_user(app, name="user_name") as client:
|
||||
r = client.get("/api/v1/teams/1/awards")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_awards_after_freze_time():
|
||||
"""Can a user get /api/v1/teams/<team_id>/awards after freeze time"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
team = gen_team(
|
||||
app.db, name="team1", email="team1@examplectf.com", member_count=1
|
||||
)
|
||||
|
||||
team_member = team.members[0]
|
||||
tm_name = team_member.name
|
||||
|
||||
set_config("freeze", "1507262400")
|
||||
with freeze_time("2017-10-4"):
|
||||
gen_award(app.db, user_id=3)
|
||||
|
||||
with freeze_time("2017-10-8"):
|
||||
gen_award(app.db, user_id=3)
|
||||
|
||||
assert Awards.query.count() == 2
|
||||
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/api/v1/teams/1/awards")
|
||||
data = r.get_json()["data"]
|
||||
assert len(data) == 1
|
||||
|
||||
with login_as_user(app, name=tm_name) as client:
|
||||
r = client.get("/api/v1/teams/me/awards")
|
||||
data = r.get_json()["data"]
|
||||
assert len(data) == 2
|
||||
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.get("/api/v1/teams/1/awards")
|
||||
data = r.get_json()["data"]
|
||||
assert len(data) == 2
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_patch_password():
|
||||
"""Can a user change their team password /api/v1/teams/me if logged in as the captain"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user1 = gen_user(
|
||||
app.db, name="user1", email="user1@examplectf.com", password="captain"
|
||||
) # ID 2
|
||||
user2 = gen_user(app.db, name="user2", email="user2@examplectf.com") # ID 3
|
||||
team = gen_team(app.db)
|
||||
team.members.append(user1)
|
||||
team.members.append(user2)
|
||||
team.captain_id = 2
|
||||
user1.team_id = team.id
|
||||
user2.team_id = team.id
|
||||
app.db.session.commit()
|
||||
with login_as_user(app, name="user2") as client:
|
||||
r = client.patch(
|
||||
"/api/v1/teams/me",
|
||||
json={"confirm": "password", "password": "new_password"},
|
||||
)
|
||||
assert r.status_code == 403
|
||||
|
||||
assert r.get_json() == {
|
||||
"errors": {"": ["Only team captains can edit team information"]},
|
||||
"success": False,
|
||||
}
|
||||
|
||||
team = Teams.query.filter_by(id=1).first()
|
||||
assert (
|
||||
verify_password(plaintext="new_password", ciphertext=team.password)
|
||||
is False
|
||||
)
|
||||
|
||||
with login_as_user(app, name="user1", password="captain") as client:
|
||||
# Test that invalid passwords aren't accepted
|
||||
r = client.patch(
|
||||
"/api/v1/teams/me",
|
||||
json={"confirm": "incorrect_password", "password": "new_password"},
|
||||
)
|
||||
assert r.status_code == 400
|
||||
assert (
|
||||
verify_password(plaintext="new_password", ciphertext=team.password)
|
||||
is False
|
||||
)
|
||||
|
||||
# Test that the team's password is accepted
|
||||
r = client.patch(
|
||||
"/api/v1/teams/me",
|
||||
json={"confirm": "password", "password": "new_password"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
team = Teams.query.filter_by(id=1).first()
|
||||
assert verify_password(plaintext="new_password", ciphertext=team.password)
|
||||
|
||||
# Test that the captain's password is also accepted
|
||||
r = client.patch(
|
||||
"/api/v1/teams/me",
|
||||
json={"confirm": "captain", "password": "captain_password"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
team = Teams.query.filter_by(id=1).first()
|
||||
assert verify_password(
|
||||
plaintext="captain_password", ciphertext=team.password
|
||||
)
|
||||
|
||||
|
||||
def test_api_team_captain_disbanding():
|
||||
"""Test that only team captains can disband teams"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user = gen_user(app.db, name="user")
|
||||
team = gen_team(app.db)
|
||||
team.members.append(user)
|
||||
user.team_id = team.id
|
||||
team.captain_id = 2
|
||||
user2 = gen_user(app.db, name="user2", email="user2@examplectf.com")
|
||||
team.members.append(user2)
|
||||
app.db.session.commit()
|
||||
with login_as_user(app, name="user2") as client:
|
||||
r = client.delete("/api/v1/teams/me", json="")
|
||||
assert r.status_code == 403
|
||||
assert r.get_json() == {
|
||||
"success": False,
|
||||
"errors": {"": ["Only team captains can disband their team"]},
|
||||
}
|
||||
with login_as_user(app) as client:
|
||||
r = client.delete("/api/v1/teams/me", json="")
|
||||
assert r.status_code == 200
|
||||
assert r.get_json() == {
|
||||
"success": True,
|
||||
}
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_disbanding_disabled():
|
||||
"""Test that team disbanding can be disabled"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
set_config("team_disbanding", "disabled")
|
||||
team = gen_team(app.db)
|
||||
captain = Users.query.filter_by(id=team.captain_id).first()
|
||||
app.db.session.commit()
|
||||
with login_as_user(app, name=captain.name) as client:
|
||||
r = client.delete("/api/v1/teams/me", json="")
|
||||
assert r.status_code == 403
|
||||
assert r.get_json() == {
|
||||
"success": False,
|
||||
"errors": {"": ["Team disbanding is currently disabled"]},
|
||||
}
|
||||
set_config("team_disbanding", "inactive_only")
|
||||
with login_as_user(app, name=captain.name) as client:
|
||||
r = client.delete("/api/v1/teams/me", json="")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_captain_disbanding_only_inactive_teams():
|
||||
"""Test that only teams that haven't conducted any actions can be disbanded"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user = gen_user(app.db, name="user")
|
||||
team = gen_team(app.db)
|
||||
team.members.append(user)
|
||||
user.team_id = team.id
|
||||
team.captain_id = 2
|
||||
user2 = gen_user(app.db, name="user2", email="user2@examplectf.com")
|
||||
team.members.append(user2)
|
||||
app.db.session.commit()
|
||||
|
||||
gen_challenge(app.db)
|
||||
gen_flag(app.db, 1)
|
||||
gen_solve(app.db, user_id=3, team_id=1, challenge_id=1)
|
||||
|
||||
with login_as_user(app) as client:
|
||||
r = client.delete("/api/v1/teams/me", json="")
|
||||
assert r.status_code == 403
|
||||
assert r.get_json() == {
|
||||
"success": False,
|
||||
"errors": {
|
||||
"": [
|
||||
"You cannot disband your team as it has participated in the event. "
|
||||
"Please contact an admin to disband your team or remove a member."
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
user = gen_user(app.db, name="user3", email="user3@examplectf.com")
|
||||
team = gen_team(app.db, name="team2", email="team2@examplectf.com")
|
||||
print(user.id)
|
||||
team.members.append(user)
|
||||
user.team_id = team.id
|
||||
team.captain_id = user.id
|
||||
app.db.session.commit()
|
||||
with login_as_user(app, name="user3") as client:
|
||||
r = client.delete("/api/v1/teams/me", json="")
|
||||
print(r.get_json())
|
||||
assert r.status_code == 200
|
||||
assert r.get_json() == {"success": True}
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_accessing_hidden_banned_users():
|
||||
"""Hidden/Banned users should not be visible to normal users, only to admins"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
register_user(app, name="user2", email="user2@examplectf.com")
|
||||
register_user(app, name="visible_user", email="visible_user@examplectf.com")
|
||||
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
team = gen_team(
|
||||
app.db, name="hidden_team", email="hidden_team@examplectf.com", hidden=True
|
||||
)
|
||||
team.members.append(user)
|
||||
user.team_id = team.id
|
||||
app.db.session.commit()
|
||||
|
||||
user = Users.query.filter_by(id=3).first()
|
||||
team = gen_team(
|
||||
app.db, name="banned_team", email="banned_team@examplectf.com", banned=True
|
||||
)
|
||||
team.members.append(user)
|
||||
user.team_id = team.id
|
||||
app.db.session.commit()
|
||||
|
||||
with login_as_user(app, name="visible_user") as client:
|
||||
list_teams = client.get("/api/v1/teams").get_json()["data"]
|
||||
assert len(list_teams) == 0
|
||||
|
||||
assert client.get("/api/v1/teams/1").status_code == 404
|
||||
assert client.get("/api/v1/teams/1/solves").status_code == 404
|
||||
assert client.get("/api/v1/teams/1/fails").status_code == 404
|
||||
assert client.get("/api/v1/teams/1/awards").status_code == 404
|
||||
|
||||
assert client.get("/api/v1/teams/2").status_code == 404
|
||||
assert client.get("/api/v1/teams/2/solves").status_code == 404
|
||||
assert client.get("/api/v1/teams/2/fails").status_code == 404
|
||||
assert client.get("/api/v1/teams/2/awards").status_code == 404
|
||||
|
||||
with login_as_user(app, name="admin") as client:
|
||||
# Admins see hidden teams in lists
|
||||
list_users = client.get("/api/v1/teams?view=admin").get_json()["data"]
|
||||
assert len(list_users) == 2
|
||||
|
||||
assert client.get("/api/v1/teams/1").status_code == 200
|
||||
assert client.get("/api/v1/teams/1/solves").status_code == 200
|
||||
assert client.get("/api/v1/teams/1/fails").status_code == 200
|
||||
assert client.get("/api/v1/teams/1/awards").status_code == 200
|
||||
|
||||
assert client.get("/api/v1/teams/2").status_code == 200
|
||||
assert client.get("/api/v1/teams/2/solves").status_code == 200
|
||||
assert client.get("/api/v1/teams/2/fails").status_code == 200
|
||||
assert client.get("/api/v1/teams/2/awards").status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_without_team_challenge_interaction():
|
||||
"""Can a user interact with challenges without having joined a team?"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
gen_challenge(app.db)
|
||||
gen_flag(app.db, 1)
|
||||
|
||||
with login_as_user(app) as client:
|
||||
assert client.get("/api/v1/challenges").status_code == 403
|
||||
assert client.get("/api/v1/challenges/1").status_code == 403
|
||||
assert (
|
||||
client.post(
|
||||
"/api/v1/challenges/attempt",
|
||||
json={"challenge_id": 1, "submission": "wrong_flag"},
|
||||
).status_code
|
||||
== 403
|
||||
)
|
||||
|
||||
# Create a user with a team
|
||||
user = gen_user(app.db, email="user_name@examplectf.com")
|
||||
team = gen_team(app.db)
|
||||
team.members.append(user)
|
||||
user.team_id = team.id
|
||||
app.db.session.commit()
|
||||
|
||||
# Test if user with team can interact with challenges
|
||||
with login_as_user(app, name="user_name") as client:
|
||||
assert client.get("/api/v1/challenges").status_code == 200
|
||||
assert client.get("/api/v1/challenges/1").status_code == 200
|
||||
assert (
|
||||
client.post(
|
||||
"/api/v1/challenges/attempt",
|
||||
json={"challenge_id": 1, "submission": "flag"},
|
||||
).status_code
|
||||
== 200
|
||||
)
|
||||
destroy_ctfd(app)
|
||||
123
tests/api/v1/test_tokens.py
Normal file
123
tests/api/v1/test_tokens.py
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
|
||||
from CTFd.models import Tokens, Users
|
||||
from CTFd.schemas.tokens import TokenSchema
|
||||
from CTFd.utils.security.auth import generate_user_token
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, gen_user, login_as_user
|
||||
|
||||
|
||||
def test_api_tag_list_post():
|
||||
"""Can a user create a token"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
user = gen_user(app.db, name="user")
|
||||
user_id = user.id
|
||||
with login_as_user(app) as client:
|
||||
r = client.post("/api/v1/tokens", json={})
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
value = resp["data"]["value"]
|
||||
token = Tokens.query.filter_by(value=value).first()
|
||||
assert token.user_id == user_id
|
||||
assert token.expiration > datetime.datetime.utcnow()
|
||||
|
||||
data = {"expiration": "9999-12-30"}
|
||||
r = client.post("/api/v1/tokens", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
value = resp["data"]["value"]
|
||||
token = Tokens.query.filter_by(value=value).first()
|
||||
assert token.user_id == user_id
|
||||
assert token.expiration.year == 9999
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_tag_list_get():
|
||||
"""Can a user get /api/v1/tokens"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
user = gen_user(app.db, name="user")
|
||||
generate_user_token(user)
|
||||
|
||||
user2 = gen_user(app.db, name="user2", email="user2@examplectf.com")
|
||||
generate_user_token(user2)
|
||||
generate_user_token(user2)
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/api/v1/tokens", json="")
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
assert len(resp["data"]) == 1
|
||||
|
||||
with login_as_user(app, name="user2") as client:
|
||||
r = client.get("/api/v1/tokens", json="")
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
assert len(resp["data"]) == 2
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_tag_detail_get():
|
||||
"""Can a user get /api/v1/tokens/<token_id>"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
user = gen_user(app.db, name="user")
|
||||
generate_user_token(user)
|
||||
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/api/v1/tokens/1", json="")
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
assert sorted(resp["data"].keys()) == sorted(TokenSchema().views["user"])
|
||||
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.get("/api/v1/tokens/1", json="")
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
assert sorted(resp["data"].keys()) == sorted(TokenSchema().views["admin"])
|
||||
|
||||
gen_user(app.db, name="user2", email="user2@examplectf.com")
|
||||
with login_as_user(app, "user2") as client:
|
||||
r = client.get("/api/v1/tokens/1", json="")
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_token_delete():
|
||||
"""Can tokens be deleted by owners, and admins"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Can be deleted by the user
|
||||
user = gen_user(app.db)
|
||||
user_id = user.id
|
||||
username = user.name
|
||||
token = generate_user_token(user)
|
||||
token_id = token.id
|
||||
with login_as_user(app, username) as client:
|
||||
r = client.delete("/api/v1/tokens/" + str(token_id), json="")
|
||||
assert r.status_code == 200
|
||||
assert Tokens.query.count() == 0
|
||||
|
||||
# Can be deleted by admins
|
||||
user = Users.query.filter_by(id=user_id).first()
|
||||
token = generate_user_token(user)
|
||||
token_id = token.id
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.delete("/api/v1/tokens/" + str(token_id), json="")
|
||||
assert r.status_code == 200
|
||||
assert Tokens.query.count() == 0
|
||||
|
||||
# First user
|
||||
first_user = Users.query.filter_by(id=user_id).first()
|
||||
token = generate_user_token(first_user)
|
||||
token_id = token.id
|
||||
# Second user
|
||||
second_user = gen_user(app.db, name="user2", email="user2@examplectf.com")
|
||||
username2 = second_user.name
|
||||
with login_as_user(app, username2) as client:
|
||||
r = client.delete("/api/v1/tokens/" + str(token_id), json="")
|
||||
assert r.status_code == 404
|
||||
assert Tokens.query.count() == 1
|
||||
destroy_ctfd(app)
|
||||
132
tests/api/v1/test_topics.py
Normal file
132
tests/api/v1/test_topics.py
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_topic,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_api_topics_non_admin():
|
||||
"""Can a user interact with /api/v1/topics if not admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
gen_topic(app.db, challenge_id=1)
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/topics", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
"""Can a user post /api/v1/topics if not admin"""
|
||||
r = client.post("/api/v1/topics")
|
||||
assert r.status_code == 403
|
||||
|
||||
"""Can a user delete /api/v1/topics if not admin"""
|
||||
r = client.delete("/api/v1/topics")
|
||||
assert r.status_code == 403
|
||||
|
||||
"""Can a user get /api/v1/topics/<topic_id> if not admin"""
|
||||
r = client.get("/api/v1/topics/1", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
"""Can a user delete /api/v1/topics/<topic_id> if not admin"""
|
||||
r = client.delete("/api/v1/topics/1", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/api/v1/topics", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
"""Can a user post /api/v1/topics if not admin"""
|
||||
r = client.post("/api/v1/topics")
|
||||
assert r.status_code == 403
|
||||
|
||||
"""Can a user delete /api/v1/topics if not admin"""
|
||||
r = client.delete("/api/v1/topics")
|
||||
assert r.status_code == 403
|
||||
|
||||
"""Can a user get /api/v1/topics/<topic_id> if not admin"""
|
||||
r = client.get("/api/v1/topics/1", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
"""Can a user delete /api/v1/topics/<topic_id> if not admin"""
|
||||
r = client.delete("/api/v1/topics/1", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_topics_get_admin():
|
||||
"""Can a user get /api/v1/topics if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
gen_topic(app.db, challenge_id=1)
|
||||
gen_topic(app.db, challenge_id=1, value="topic2")
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.get("/api/v1/topics")
|
||||
assert r.status_code == 200
|
||||
assert r.get_json() == {
|
||||
"success": True,
|
||||
"data": [{"id": 1, "value": "topic"}, {"id": 2, "value": "topic2"}],
|
||||
}
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_topics_post_admin():
|
||||
"""Can a user post /api/v1/topics if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.post(
|
||||
"/api/v1/topics",
|
||||
json={"value": "topic", "type": "challenge", "challenge_id": 1},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
print(r.get_json())
|
||||
assert r.get_json() == {
|
||||
"success": True,
|
||||
"data": {
|
||||
"challenge_id": 1,
|
||||
"challenge": 1,
|
||||
"topic": 1,
|
||||
"id": 1,
|
||||
"topic_id": 1,
|
||||
},
|
||||
}
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_topics_delete_admin():
|
||||
"""Can a user delete /api/v1/topics/<topic_id> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
gen_topic(app.db, challenge_id=1)
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.delete("/api/v1/topics/1", json="")
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
assert resp.get("data") is None
|
||||
assert resp.get("success") is True
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_topics_delete_target_admin():
|
||||
"""Can a user delete /api/v1/topics if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
gen_topic(app.db, challenge_id=1)
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.delete("/api/v1/topics?type=challenge&target_id=1", json="")
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
assert resp.get("data") is None
|
||||
assert resp.get("success") is True
|
||||
destroy_ctfd(app)
|
||||
970
tests/api/v1/test_users.py
Normal file
970
tests/api/v1/test_users.py
Normal file
@@ -0,0 +1,970 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from CTFd.models import Awards, Fails, Solves, Users
|
||||
from CTFd.schemas.users import UserSchema
|
||||
from CTFd.utils import set_config
|
||||
from CTFd.utils.crypto import verify_password
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_award,
|
||||
gen_challenge,
|
||||
gen_fail,
|
||||
gen_solve,
|
||||
gen_team,
|
||||
gen_user,
|
||||
login_as_user,
|
||||
register_user,
|
||||
simulate_user_activity,
|
||||
)
|
||||
|
||||
|
||||
def test_api_users_get_public():
|
||||
"""Can a user get /api/v1/users if users are public"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
set_config("account_visibility", "public")
|
||||
r = client.get("/api/v1/users")
|
||||
assert r.status_code == 200
|
||||
set_config("account_visibility", "private")
|
||||
r = client.get("/api/v1/users")
|
||||
assert r.status_code == 302
|
||||
set_config("account_visibility", "admins")
|
||||
r = client.get("/api/v1/users")
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_users_get_private():
|
||||
"""Can a user get /api/v1/users if users are public"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
set_config("account_visibility", "public")
|
||||
r = client.get("/api/v1/users")
|
||||
assert r.status_code == 200
|
||||
set_config("account_visibility", "private")
|
||||
r = client.get("/api/v1/users")
|
||||
assert r.status_code == 302
|
||||
set_config("account_visibility", "admins")
|
||||
r = client.get("/api/v1/users")
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_users_get_admins():
|
||||
"""Can a user get /api/v1/users if users are public"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
set_config("account_visibility", "public")
|
||||
r = client.get("/api/v1/users")
|
||||
assert r.status_code == 200
|
||||
set_config("account_visibility", "private")
|
||||
r = client.get("/api/v1/users")
|
||||
assert r.status_code == 302
|
||||
set_config("account_visibility", "admins")
|
||||
r = client.get("/api/v1/users")
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_users_post_non_admin():
|
||||
"""Can a user post /api/v1/users if not admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.post("/api/v1/users", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_users_post_admin():
|
||||
"""Can a user post /api/v1/users if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as client:
|
||||
# Create user
|
||||
r = client.post(
|
||||
"/api/v1/users",
|
||||
json={"name": "user", "email": "user@user.com", "password": "password"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
# Make sure password was hashed properly
|
||||
user = Users.query.filter_by(email="user@user.com").first()
|
||||
assert user
|
||||
assert verify_password("password", user.password)
|
||||
|
||||
# Make sure user can login with the creds
|
||||
client = login_as_user(app)
|
||||
r = client.get("/profile")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_users_post_admin_with_attributes():
|
||||
"""Can a user post /api/v1/users with user settings"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as client:
|
||||
# Create user
|
||||
r = client.post(
|
||||
"/api/v1/users",
|
||||
json={
|
||||
"name": "user",
|
||||
"email": "user@user.com",
|
||||
"password": "password",
|
||||
"banned": True,
|
||||
"hidden": True,
|
||||
"verified": True,
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
# Make sure password was hashed properly
|
||||
user = Users.query.filter_by(email="user@user.com").first()
|
||||
assert user
|
||||
assert verify_password("password", user.password)
|
||||
assert user.banned
|
||||
assert user.hidden
|
||||
assert user.verified
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_users_post_admin_duplicate_information():
|
||||
"""Can an admin create a user with duplicate information"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app, "admin") as client:
|
||||
# Duplicate email
|
||||
r = client.post(
|
||||
"/api/v1/users",
|
||||
json={
|
||||
"name": "user2",
|
||||
"email": "user@examplectf.com",
|
||||
"password": "password",
|
||||
},
|
||||
)
|
||||
resp = r.get_json()
|
||||
assert r.status_code == 400
|
||||
assert resp["errors"]["email"]
|
||||
assert resp["success"] is False
|
||||
assert Users.query.count() == 2
|
||||
|
||||
# Duplicate user
|
||||
r = client.post(
|
||||
"/api/v1/users",
|
||||
json={
|
||||
"name": "user",
|
||||
"email": "user2@examplectf.com",
|
||||
"password": "password",
|
||||
},
|
||||
)
|
||||
resp = r.get_json()
|
||||
assert r.status_code == 400
|
||||
assert resp["errors"]["name"]
|
||||
assert resp["success"] is False
|
||||
assert Users.query.count() == 2
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_users_patch_admin_duplicate_information():
|
||||
"""Can an admin modify a user with duplicate information"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(
|
||||
app, name="user1", email="user1@examplectf.com", password="password"
|
||||
)
|
||||
register_user(
|
||||
app, name="user2", email="user2@examplectf.com", password="password"
|
||||
)
|
||||
with login_as_user(app, "admin") as client:
|
||||
# Duplicate name
|
||||
r = client.patch(
|
||||
"/api/v1/users/1",
|
||||
json={
|
||||
"name": "user2",
|
||||
"email": "user@examplectf.com",
|
||||
"password": "password",
|
||||
},
|
||||
)
|
||||
resp = r.get_json()
|
||||
assert r.status_code == 400
|
||||
assert resp["errors"]["name"]
|
||||
assert resp["success"] is False
|
||||
|
||||
# Duplicate email
|
||||
r = client.patch(
|
||||
"/api/v1/users/1",
|
||||
json={
|
||||
"name": "user",
|
||||
"email": "user2@examplectf.com",
|
||||
"password": "password",
|
||||
},
|
||||
)
|
||||
resp = r.get_json()
|
||||
assert r.status_code == 400
|
||||
assert resp["errors"]["email"]
|
||||
assert resp["success"] is False
|
||||
assert Users.query.count() == 3
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_users_patch_duplicate_information():
|
||||
"""Can a user modify their information to another user's"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(
|
||||
app, name="user1", email="user1@examplectf.com", password="password"
|
||||
)
|
||||
register_user(
|
||||
app, name="user2", email="user2@examplectf.com", password="password"
|
||||
)
|
||||
with login_as_user(app, "user1") as client:
|
||||
# Duplicate email
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={
|
||||
"name": "user1",
|
||||
"email": "user2@examplectf.com",
|
||||
"confirm": "password",
|
||||
},
|
||||
)
|
||||
resp = r.get_json()
|
||||
assert r.status_code == 400
|
||||
assert resp["errors"]["email"]
|
||||
assert resp["success"] is False
|
||||
|
||||
# Duplicate user
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={
|
||||
"name": "user2",
|
||||
"email": "user1@examplectf.com",
|
||||
"confirm": "password",
|
||||
},
|
||||
)
|
||||
resp = r.get_json()
|
||||
assert r.status_code == 400
|
||||
assert resp["errors"]["name"]
|
||||
assert resp["success"] is False
|
||||
assert Users.query.count() == 3
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_public():
|
||||
"""Can a user get /api/v1/team/<user_id> if users are public"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
set_config("account_visibility", "public")
|
||||
gen_user(app.db)
|
||||
r = client.get("/api/v1/users/2")
|
||||
assert r.status_code == 200
|
||||
set_config("account_visibility", "private")
|
||||
r = client.get("/api/v1/users/2")
|
||||
assert r.status_code == 302
|
||||
set_config("account_visibility", "admins")
|
||||
r = client.get("/api/v1/users/2")
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_private():
|
||||
"""Can a user get /api/v1/users/<user_id> if users are private"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
set_config("account_visibility", "public")
|
||||
r = client.get("/api/v1/users/2")
|
||||
print(r.__dict__)
|
||||
assert r.status_code == 200
|
||||
set_config("account_visibility", "private")
|
||||
r = client.get("/api/v1/users/2")
|
||||
assert r.status_code == 200
|
||||
set_config("account_visibility", "admins")
|
||||
r = client.get("/api/v1/users/2")
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_team_get_admin():
|
||||
"""Can a user get /api/v1/users/<user_id> if users are viewed by admins only"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, "admin") as client:
|
||||
gen_user(app.db)
|
||||
set_config("account_visibility", "public")
|
||||
r = client.get("/api/v1/users/2")
|
||||
assert r.status_code == 200
|
||||
set_config("account_visibility", "private")
|
||||
r = client.get("/api/v1/users/2")
|
||||
assert r.status_code == 200
|
||||
set_config("account_visibility", "admins")
|
||||
r = client.get("/api/v1/users/2")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_patch_non_admin():
|
||||
"""Can a user patch /api/v1/users/<user_id> if not admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with app.test_client() as client:
|
||||
r = client.patch("/api/v1/users/2", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_patch_admin():
|
||||
"""Can a user patch /api/v1/users/<user_id> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.patch(
|
||||
"/api/v1/users/2",
|
||||
json={
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"password": "password",
|
||||
"country": "US",
|
||||
"verified": True,
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
user_data = r.get_json()["data"]
|
||||
assert user_data["country"] == "US"
|
||||
assert user_data["verified"] is True
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_delete_non_admin():
|
||||
"""Can a user delete /api/v1/users/<user_id> if not admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with app.test_client() as client:
|
||||
r = client.delete("/api/v1/teams/2", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_delete_admin():
|
||||
"""Can a user patch /api/v1/users/<user_id> if admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
simulate_user_activity(app.db, user=user)
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.delete("/api/v1/users/2", json="")
|
||||
assert r.status_code == 200
|
||||
assert r.get_json().get("data") is None
|
||||
assert Users.query.filter_by(id=2).first() is None
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_get_me_not_logged_in():
|
||||
"""Can a user get /api/v1/users/me if not logged in"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/users/me")
|
||||
assert r.status_code == 302
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_get_me_logged_in():
|
||||
"""Can a user get /api/v1/users/me if logged in"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/api/v1/users/me")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_patch_me_not_logged_in():
|
||||
"""Can a user patch /api/v1/users/me if not logged in"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.patch("/api/v1/users/me", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_patch_me_logged_in():
|
||||
"""Can a user patch /api/v1/users/me if logged in"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"password": "password",
|
||||
"confirm": "password",
|
||||
"country": "US",
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"]["country"] == "US"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_admin_user_patch_me_logged_in():
|
||||
"""Can an admin patch /api/v1/users/me"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"password": "password",
|
||||
"confirm": "password",
|
||||
"country": "US",
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"]["country"] == "US"
|
||||
|
||||
user = Users.query.filter_by(id=1).first()
|
||||
assert user.name == "user"
|
||||
assert user.email == "user@examplectf.com"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_change_name():
|
||||
"""Can a user change their name via the API"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
r = client.patch("/api/v1/users/me", json={"name": "user2"})
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["name"] == "user2"
|
||||
assert resp["success"] is True
|
||||
|
||||
r = client.patch("/api/v1/users/me", json={"name": None})
|
||||
resp = r.get_json()
|
||||
print(resp)
|
||||
assert r.status_code == 400
|
||||
assert resp["errors"]["name"] == ["Field may not be null."]
|
||||
assert resp["success"] is False
|
||||
|
||||
set_config("name_changes", False)
|
||||
|
||||
r = client.patch("/api/v1/users/me", json={"name": "new_name"})
|
||||
assert r.status_code == 400
|
||||
resp = r.get_json()
|
||||
assert "name" in resp["errors"]
|
||||
assert resp["success"] is False
|
||||
|
||||
set_config("name_changes", True)
|
||||
r = client.patch("/api/v1/users/me", json={"name": "new_name"})
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["name"] == "new_name"
|
||||
assert resp["success"] is True
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_change_email():
|
||||
"""Test that users can change their email via the API"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
app.db.session.commit()
|
||||
with login_as_user(app) as client:
|
||||
# Test users can't submit null
|
||||
r = client.patch(
|
||||
"/api/v1/users/me", json={"email": None, "confirm": "password"}
|
||||
)
|
||||
resp = r.get_json()
|
||||
print(resp)
|
||||
assert r.status_code == 400
|
||||
assert resp["errors"]["email"] == ["Field may not be null."]
|
||||
|
||||
# Test users can exercise the API
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={"email": "new_email@email.com", "confirm": "password"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["email"] == "new_email@email.com"
|
||||
assert resp["success"] is True
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.email == "new_email@email.com"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_change_verify_email():
|
||||
"""Test that users are marked unconfirmed if they change their email and verify_emails is turned on"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("verify_emails", True)
|
||||
register_user(app)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
user.verified = True
|
||||
app.db.session.commit()
|
||||
with login_as_user(app) as client:
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={"email": "new_email@email.com", "confirm": "password"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["email"] == "new_email@email.com"
|
||||
assert resp["success"] is True
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.verified is False
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_change_email_under_whitelist():
|
||||
"""Test that users can only change emails to ones in the whitelist"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
set_config(
|
||||
"domain_whitelist", "whitelisted.com, whitelisted.org, whitelisted.net"
|
||||
)
|
||||
with login_as_user(app) as client:
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={"email": "new_email@email.com", "confirm": "password"},
|
||||
)
|
||||
assert r.status_code == 400
|
||||
resp = r.get_json()
|
||||
assert resp["errors"]["email"]
|
||||
assert resp["success"] is False
|
||||
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={"email": "new_email@whitelisted.com", "confirm": "password"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["email"] == "new_email@whitelisted.com"
|
||||
assert resp["success"] is True
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_change_email_under_blacklist():
|
||||
"""Test that users can not change emails to ones in the blacklist"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
set_config(
|
||||
"domain_blacklist", "blacklisted.com, blacklisted.org, blacklisted.net"
|
||||
)
|
||||
with login_as_user(app) as client:
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={"email": "new_email@blacklisted.com", "confirm": "password"},
|
||||
)
|
||||
assert r.status_code == 400
|
||||
resp = r.get_json()
|
||||
assert resp["errors"]["email"]
|
||||
assert resp["success"] is False
|
||||
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={"email": "new_email@test.com", "confirm": "password"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["email"] == "new_email@test.com"
|
||||
assert resp["success"] is True
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_get_me_solves_not_logged_in():
|
||||
"""Can a user get /api/v1/users/me/solves if not logged in"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/users/me/solves", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_get_me_solves_logged_in():
|
||||
"""Can a user get /api/v1/users/me/solves if logged in"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/api/v1/users/me/solves")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_get_solves():
|
||||
"""Can a user get /api/v1/users/<user_id>/solves if logged in"""
|
||||
app = create_ctfd(user_mode="users")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/api/v1/users/2/solves")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_get_solves_after_freze_time():
|
||||
"""Can a user get /api/v1/users/<user_id>/solves after freeze time"""
|
||||
app = create_ctfd(user_mode="users")
|
||||
with app.app_context():
|
||||
register_user(app, name="user1", email="user1@examplectf.com")
|
||||
register_user(app, name="user2", email="user2@examplectf.com")
|
||||
|
||||
# Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("freeze", "1507262400")
|
||||
with freeze_time("2017-10-4"):
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
gen_solve(app.db, user_id=2, challenge_id=chal_id)
|
||||
chal2 = gen_challenge(app.db)
|
||||
chal2_id = chal2.id
|
||||
|
||||
with freeze_time("2017-10-8"):
|
||||
chal2 = gen_solve(app.db, user_id=2, challenge_id=chal2_id)
|
||||
|
||||
# There should now be two solves assigned to the same user.
|
||||
assert Solves.query.count() == 2
|
||||
|
||||
# User 2 should have 2 solves when seen by themselves
|
||||
client = login_as_user(app, name="user1")
|
||||
r = client.get("/api/v1/users/me/solves")
|
||||
data = r.get_json()["data"]
|
||||
assert len(data) == 2
|
||||
|
||||
# User 2 should have 1 solve when seen by another user
|
||||
client = login_as_user(app, name="user2")
|
||||
r = client.get("/api/v1/users/2/solves")
|
||||
data = r.get_json()["data"]
|
||||
assert len(data) == 1
|
||||
|
||||
# Admins should see all solves for the user
|
||||
admin = login_as_user(app, name="admin")
|
||||
|
||||
r = admin.get("/api/v1/users/2/solves")
|
||||
data = r.get_json()["data"]
|
||||
assert len(data) == 2
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_get_me_fails_not_logged_in():
|
||||
"""Can a user get /api/v1/users/me/fails if not logged in"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/users/me/fails", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_get_me_fails_logged_in():
|
||||
"""Can a user get /api/v1/users/me/fails if logged in"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/api/v1/users/me/fails")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_get_fails():
|
||||
"""Can a user get /api/v1/users/<user_id>/fails if logged in"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/api/v1/users/2/fails")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_get_fails_after_freze_time():
|
||||
"""Can a user get /api/v1/users/<user_id>/fails after freeze time"""
|
||||
app = create_ctfd(user_mode="users")
|
||||
with app.app_context():
|
||||
register_user(app, name="user1", email="user1@examplectf.com")
|
||||
register_user(app, name="user2", email="user2@examplectf.com")
|
||||
|
||||
# Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("freeze", "1507262400")
|
||||
with freeze_time("2017-10-4"):
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
chal2 = gen_challenge(app.db)
|
||||
chal2_id = chal2.id
|
||||
gen_fail(app.db, user_id=2, challenge_id=chal_id)
|
||||
|
||||
with freeze_time("2017-10-8"):
|
||||
chal2 = gen_fail(app.db, user_id=2, challenge_id=chal2_id)
|
||||
|
||||
# There should now be two fails assigned to the same user.
|
||||
assert Fails.query.count() == 2
|
||||
|
||||
# User 2 should have 2 fail when seen by themselves
|
||||
client = login_as_user(app, name="user1")
|
||||
r = client.get("/api/v1/users/me/fails")
|
||||
assert r.get_json()["meta"]["count"] == 2
|
||||
|
||||
# User 2 should have 1 fail when seen by another user
|
||||
client = login_as_user(app, name="user2")
|
||||
r = client.get("/api/v1/users/2/fails")
|
||||
assert r.get_json()["meta"]["count"] == 1
|
||||
|
||||
# Admins should see all fails for the user
|
||||
admin = login_as_user(app, name="admin")
|
||||
|
||||
r = admin.get("/api/v1/users/2/fails")
|
||||
assert r.get_json()["meta"]["count"] == 2
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_get_me_awards_not_logged_in():
|
||||
"""Can a user get /api/v1/users/me/awards if not logged in"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/users/me/awards", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_get_me_awards_logged_in():
|
||||
"""Can a user get /api/v1/users/me/awards if logged in"""
|
||||
app = create_ctfd(user_mode="users")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/api/v1/users/me/awards")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_get_awards():
|
||||
"""Can a user get /api/v1/users/<user_id>/awards if logged in"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/api/v1/users/2/awards")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_get_awards_after_freze_time():
|
||||
"""Can a user get /api/v1/users/<user_id>/awards after freeze time"""
|
||||
app = create_ctfd(user_mode="users")
|
||||
with app.app_context():
|
||||
register_user(app, name="user1", email="user1@examplectf.com")
|
||||
register_user(app, name="user2", email="user2@examplectf.com")
|
||||
|
||||
# Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("freeze", "1507262400")
|
||||
with freeze_time("2017-10-4"):
|
||||
gen_award(app.db, user_id=2)
|
||||
|
||||
with freeze_time("2017-10-8"):
|
||||
gen_award(app.db, user_id=2)
|
||||
|
||||
# There should now be two awards assigned to the same user.
|
||||
assert Awards.query.count() == 2
|
||||
|
||||
# User 2 should have 2 awards when seen by themselves
|
||||
client = login_as_user(app, name="user1")
|
||||
r = client.get("/api/v1/users/me/awards")
|
||||
data = r.get_json()["data"]
|
||||
assert len(data) == 2
|
||||
|
||||
# User 2 should have 1 award when seen by another user
|
||||
client = login_as_user(app, name="user2")
|
||||
r = client.get("/api/v1/users/2/awards")
|
||||
data = r.get_json()["data"]
|
||||
assert len(data) == 1
|
||||
|
||||
# Admins should see all awards for the user
|
||||
admin = login_as_user(app, name="admin")
|
||||
|
||||
r = admin.get("/api/v1/users/2/awards")
|
||||
data = r.get_json()["data"]
|
||||
assert len(data) == 2
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_accessing_hidden_users():
|
||||
"""Hidden users should not be visible to normal users, only to admins"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="visible_user", email="visible_user@examplectf.com")
|
||||
register_user(
|
||||
app, name="hidden_user", email="hidden_user@examplectf.com"
|
||||
) # ID 3
|
||||
user = Users.query.filter_by(name="hidden_user").first()
|
||||
user.hidden = True
|
||||
app.db.session.commit()
|
||||
|
||||
with login_as_user(app, name="visible_user") as client:
|
||||
list_users = client.get("/api/v1/users").get_json()["data"]
|
||||
assert len(list_users) == 1
|
||||
|
||||
assert client.get("/api/v1/users/3").status_code == 404
|
||||
assert client.get("/api/v1/users/3/solves").status_code == 404
|
||||
assert client.get("/api/v1/users/3/fails").status_code == 404
|
||||
assert client.get("/api/v1/users/3/awards").status_code == 404
|
||||
|
||||
with login_as_user(app, name="admin") as client:
|
||||
# Admins see the user in lists
|
||||
list_users = client.get("/api/v1/users?view=admin").get_json()["data"]
|
||||
assert len(list_users) == 3
|
||||
|
||||
assert client.get("/api/v1/users/3").status_code == 200
|
||||
assert client.get("/api/v1/users/3/solves").status_code == 200
|
||||
assert client.get("/api/v1/users/3/fails").status_code == 200
|
||||
assert client.get("/api/v1/users/3/awards").status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_accessing_banned_users():
|
||||
"""Banned users should not be visible to normal users, only to admins"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="visible_user", email="visible_user@examplectf.com")
|
||||
register_user(
|
||||
app, name="banned_user", email="banned_user@examplectf.com"
|
||||
) # ID 3
|
||||
user = Users.query.filter_by(name="banned_user").first()
|
||||
user.banned = True
|
||||
app.db.session.commit()
|
||||
|
||||
with login_as_user(app, name="visible_user") as client:
|
||||
list_users = client.get("/api/v1/users").get_json()["data"]
|
||||
assert len(list_users) == 1
|
||||
|
||||
assert client.get("/api/v1/users/3").status_code == 404
|
||||
assert client.get("/api/v1/users/3/solves").status_code == 404
|
||||
assert client.get("/api/v1/users/3/fails").status_code == 404
|
||||
assert client.get("/api/v1/users/3/awards").status_code == 404
|
||||
|
||||
with login_as_user(app, name="admin") as client:
|
||||
# Admins see the user in lists
|
||||
list_users = client.get("/api/v1/users?view=admin").get_json()["data"]
|
||||
assert len(list_users) == 3
|
||||
|
||||
assert client.get("/api/v1/users/3").status_code == 200
|
||||
assert client.get("/api/v1/users/3/solves").status_code == 200
|
||||
assert client.get("/api/v1/users/3/fails").status_code == 200
|
||||
assert client.get("/api/v1/users/3/awards").status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_send_email():
|
||||
"""Can an admin post /api/v1/users/<user_id>/email"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
|
||||
register_user(app)
|
||||
|
||||
with login_as_user(app) as client:
|
||||
r = client.post(
|
||||
"/api/v1/users/2/email", json={"text": "email should get rejected"}
|
||||
)
|
||||
assert r.status_code == 403
|
||||
|
||||
with login_as_user(app, "admin") as admin:
|
||||
r = admin.post(
|
||||
"/api/v1/users/2/email", json={"text": "email should be accepted"}
|
||||
)
|
||||
assert r.get_json() == {
|
||||
"success": False,
|
||||
"errors": {"": ["Email settings not configured"]},
|
||||
}
|
||||
assert r.status_code == 400
|
||||
|
||||
set_config("verify_emails", True)
|
||||
set_config("mail_server", "localhost")
|
||||
set_config("mail_port", 25)
|
||||
set_config("mail_useauth", True)
|
||||
set_config("mail_username", "username")
|
||||
set_config("mail_password", "password")
|
||||
|
||||
with login_as_user(app, "admin") as admin:
|
||||
r = admin.post("/api/v1/users/2/email", json={"text": ""})
|
||||
assert r.get_json() == {
|
||||
"success": False,
|
||||
"errors": {"text": ["Email text cannot be empty"]},
|
||||
}
|
||||
assert r.status_code == 400
|
||||
|
||||
with login_as_user(app, "admin") as admin:
|
||||
r = admin.post(
|
||||
"/api/v1/users/2/email", json={"text": "email should be accepted"}
|
||||
)
|
||||
# Email should go through but since we aren't mocking
|
||||
# the server we get a Connection refused error
|
||||
assert r.status_code == 400
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_get_schema():
|
||||
"""Can a user get /api/v1/users/<user_id> doesn't return unnecessary data"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="user1", email="user1@examplectf.com") # ID 2
|
||||
register_user(app, name="user2", email="user2@examplectf.com") # ID 3
|
||||
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/users/3")
|
||||
data = r.get_json()["data"]
|
||||
assert sorted(data.keys()) == sorted(
|
||||
UserSchema.views["user"] + ["score", "place"]
|
||||
)
|
||||
|
||||
with login_as_user(app, name="user1") as client:
|
||||
r = client.get("/api/v1/users/3")
|
||||
data = r.get_json()["data"]
|
||||
assert sorted(data.keys()) == sorted(
|
||||
UserSchema.views["user"] + ["score", "place"]
|
||||
)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_patch_team_id():
|
||||
"""Users can't patch their team_id directly"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
gen_team(app.db)
|
||||
|
||||
with login_as_user(app) as client:
|
||||
data = {
|
||||
"team_id": 1,
|
||||
}
|
||||
r = client.patch("/api/v1/users/me", json=data)
|
||||
data = r.get_json()
|
||||
assert data["data"]["team_id"] is None
|
||||
destroy_ctfd(app)
|
||||
0
tests/api/v1/user/__init__.py
Normal file
0
tests/api/v1/user/__init__.py
Normal file
48
tests/api/v1/user/test_admin_access.py
Normal file
48
tests/api/v1/user/test_admin_access.py
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user
|
||||
|
||||
|
||||
def test_api_hint_404():
|
||||
"""Are admin protected resources accessible by admins/non-admins"""
|
||||
app = create_ctfd()
|
||||
endpoints = [
|
||||
"/api/v1/configs/{}",
|
||||
"/api/v1/challenges/types",
|
||||
"/api/v1/statistics/teams",
|
||||
"/api/v1/flags/{}",
|
||||
"/api/v1/statistics/users/{}",
|
||||
"/api/v1/configs",
|
||||
"/api/v1/statistics/challenges/solves/percentages",
|
||||
"/api/v1/statistics/scores/distribution",
|
||||
"/api/v1/tags/{}",
|
||||
"/api/v1/pages",
|
||||
"/api/v1/files/{}",
|
||||
"/api/v1/challenges/{}/tags",
|
||||
"/api/v1/hints",
|
||||
"/api/v1/challenges/{}/files",
|
||||
"/api/v1/flags",
|
||||
"/api/v1/submissions/{}",
|
||||
"/api/v1/challenges/{}/flags",
|
||||
"/api/v1/awards/{}",
|
||||
"/api/v1/unlocks",
|
||||
"/api/v1/challenges/{}/hints",
|
||||
"/api/v1/statistics/submissions/{}",
|
||||
"/api/v1/flags/types/{}",
|
||||
"/api/v1/tags",
|
||||
"/api/v1/statistics/challenges/{}",
|
||||
"/api/v1/files",
|
||||
"/api/v1/flags/types",
|
||||
"/api/v1/submissions",
|
||||
"/api/v1/pages/{}",
|
||||
]
|
||||
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
for endpoint in endpoints:
|
||||
r = client.get(endpoint.format(1))
|
||||
assert r.status_code == 302
|
||||
assert r.location.startswith("/login")
|
||||
destroy_ctfd(app)
|
||||
393
tests/api/v1/user/test_challenges.py
Normal file
393
tests/api/v1/user/test_challenges.py
Normal file
@@ -0,0 +1,393 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from CTFd.utils import set_config
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_user,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_api_challenge_list_visibility():
|
||||
"""Can the api load /api/v1/challenges if challenge_visibility is private/public"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-5"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("challenge_visibility", "public")
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 200
|
||||
set_config("challenge_visibility", "private")
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 302
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenge_list_ctftime():
|
||||
"""Can the api load /api/v1/challenges if ctftime is over"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-7"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("challenge_visibility", "public")
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenge_list_user_visibility():
|
||||
"""Can the user load /api/v1/challenges if challenge_visibility is private/public"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-5"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 200
|
||||
set_config("challenge_visibility", "public")
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenge_list_user_ctftime():
|
||||
"""Can the user load /api/v1/challenges if ctftime is over"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-7"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenge_list_verified_emails():
|
||||
"""Can a verified email load /api/v1/challenges"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-5"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("verify_emails", True)
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 302
|
||||
gen_user(
|
||||
app.db,
|
||||
name="user_name",
|
||||
email="verified_user@examplectf.com",
|
||||
password="password",
|
||||
verified=True,
|
||||
)
|
||||
registered_client = login_as_user(app, "user_name", "password")
|
||||
r = registered_client.get("/api/v1/challenges")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenge_visibility():
|
||||
"""Can the api load /api/v1/challenges/<challenge_id> if challenge_visibility is private/public"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-5"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("challenge_visibility", "public")
|
||||
with app.test_client() as client:
|
||||
gen_challenge(app.db)
|
||||
r = client.get("/api/v1/challenges/1")
|
||||
assert r.status_code == 200
|
||||
set_config("challenge_visibility", "private")
|
||||
r = client.get("/api/v1/challenges/1")
|
||||
assert r.status_code == 302
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenge_ctftime():
|
||||
"""Can the api load /api/v1/challenges/<challenge_id> if ctftime is over"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-7"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("challenge_visibility", "public")
|
||||
gen_challenge(app.db)
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/challenges/1")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenge_user_visibility():
|
||||
"""Can the user load /api/v1/challenges/<challenge_id> if challenge_visibility is private/public"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-5"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
gen_challenge(app.db)
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/challenges/1")
|
||||
assert r.status_code == 200
|
||||
set_config("challenge_visibility", "public")
|
||||
r = client.get("/api/v1/challenges/1")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenge_user_ctftime():
|
||||
"""Can the user load /api/v1/challenges/<challenge_id> if ctftime is over"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-7"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
gen_challenge(app.db)
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/challenges/1")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenge_verified_emails():
|
||||
"""Can a verified email load /api/v1/challenges/<challenge_id>"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-5"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("verify_emails", True)
|
||||
gen_challenge(app.db)
|
||||
gen_user(
|
||||
app.db,
|
||||
name="user_name",
|
||||
email="verified_user@examplectf.com",
|
||||
password="password",
|
||||
verified=True,
|
||||
)
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
registered_client = login_as_user(app, "user_name", "password")
|
||||
r = client.get("/api/v1/challenges/1")
|
||||
assert r.status_code == 302
|
||||
r = registered_client.get("/api/v1/challenges/1")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenge_404():
|
||||
"""Will a bad <challenge_id> at /api/v1/challenges/<challenge_id> 404"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-5"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/challenges/1")
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenge_solves_visibility():
|
||||
"""Can the api load /api/v1/challenges/<challenge_id>/solves if challenge_visibility is private/public"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-5"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("challenge_visibility", "public")
|
||||
gen_challenge(app.db)
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/challenges/1/solves")
|
||||
assert r.status_code == 200
|
||||
set_config("challenge_visibility", "private")
|
||||
r = client.get("/api/v1/challenges/1/solves")
|
||||
assert r.status_code == 302
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenge_solves_ctftime():
|
||||
"""Can the api load /api/v1/challenges/<challenge_id>/solves if ctftime is over"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-7"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("challenge_visibility", "public")
|
||||
gen_challenge(app.db)
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/challenges/1/solves")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenge_solves_user_visibility():
|
||||
"""Can the user load /api/v1/challenges/<challenge_id>/solves if challenge_visibility is private/public"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-5"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
gen_challenge(app.db)
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/challenges/1/solves")
|
||||
assert r.status_code == 200
|
||||
set_config("challenge_visibility", "public")
|
||||
r = client.get("/api/v1/challenges/1/solves")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenge_solves_user_ctftime():
|
||||
"""Can the user load /api/v1/challenges/<challenge_id>/solves if ctftime is over"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-7"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
gen_challenge(app.db)
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/challenges/1/solves")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenge_solves_verified_emails():
|
||||
"""Can a verified email load /api/v1/challenges/<challenge_id>/solves"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-5"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("verify_emails", True)
|
||||
gen_challenge(app.db)
|
||||
gen_user(
|
||||
app.db,
|
||||
name="user_name",
|
||||
email="verified_user@examplectf.com",
|
||||
password="password",
|
||||
verified=True,
|
||||
)
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
registered_client = login_as_user(app, "user_name", "password")
|
||||
r = client.get("/api/v1/challenges/1/solves")
|
||||
assert r.status_code == 302
|
||||
r = registered_client.get("/api/v1/challenges/1/solves")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenges_solves_score_visibility():
|
||||
"""Can a user load /api/v1/challenges/<challenge_id>/solves if score_visibility is public/private/admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-5"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("challenge_visibility", "public")
|
||||
set_config("score_visibility", "public")
|
||||
gen_challenge(app.db)
|
||||
with app.test_client() as client:
|
||||
r = client.get("/api/v1/challenges/1/solves")
|
||||
assert r.status_code == 200
|
||||
set_config("challenge_visibility", "private")
|
||||
set_config("score_visibility", "private")
|
||||
register_user(app)
|
||||
private_client = login_as_user(app)
|
||||
r = private_client.get("/api/v1/challenges/1/solves")
|
||||
assert r.status_code == 200
|
||||
set_config("score_visibility", "admins")
|
||||
admin = login_as_user(app, "admin", "password")
|
||||
r = admin.get("/api/v1/challenges/1/solves")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_challenge_solves_404():
|
||||
"""Will a bad <challenge_id> at /api/v1/challenges/<challenge_id>/solves 404"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-5"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/challenges/1/solves")
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
285
tests/api/v1/user/test_hints.py
Normal file
285
tests/api/v1/user/test_hints.py
Normal file
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from CTFd.models import Hints
|
||||
from CTFd.utils import set_config
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_award,
|
||||
gen_challenge,
|
||||
gen_hint,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_api_hint_404():
|
||||
"""Can the users 404 /api/v1/hints/<hint_id> if logged in/out"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_hint_visibility():
|
||||
"""Can the users load /api/v1/hints/<hint_id> if logged in/out"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
chal = gen_challenge(app.db)
|
||||
gen_hint(app.db, chal.id)
|
||||
with app.test_client() as non_logged_in_user:
|
||||
r = non_logged_in_user.get("/api/v1/hints/1")
|
||||
assert r.status_code == 302
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_hint_visibility_ctftime():
|
||||
"""Can the users load /api/v1/hints/<hint_id> if not ctftime"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-7"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
chal = gen_challenge(app.db)
|
||||
gen_hint(app.db, chal.id)
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_hint_locked():
|
||||
"""Can the users unlock /api/v1/hints/<hint_id> if they don't have enough points"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
chal = gen_challenge(app.db)
|
||||
gen_hint(app.db, chal.id, content="This is a hint", cost=1, type="standard")
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 200
|
||||
r = client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
assert r.status_code == 400
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_hint_unlocked():
|
||||
"""Can the users unlock /api/v1/hints/<hint_id> if they have enough points"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
chal = gen_challenge(app.db)
|
||||
gen_hint(app.db, chal.id, content="This is a hint", cost=1, type="standard")
|
||||
register_user(app)
|
||||
# Give user points with an award
|
||||
gen_award(app.db, 2)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 200
|
||||
r = client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
assert r.status_code == 200
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_hint_double_unlock():
|
||||
"""Can a target hint be unlocked twice"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
chal = gen_challenge(app.db)
|
||||
gen_hint(app.db, chal.id, content="This is a hint", cost=1, type="standard")
|
||||
register_user(app)
|
||||
# Give user points with an award
|
||||
gen_award(app.db, 2)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 200
|
||||
r = client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
assert r.status_code == 200
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 200
|
||||
r = client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
assert r.status_code == 400
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_users_dont_prevent_other_users_from_unlocking_hints():
|
||||
"""Unlocks from one user don't affect other users"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
chal = gen_challenge(app.db)
|
||||
gen_hint(app.db, chal.id, content="This is a hint", cost=1, type="standard")
|
||||
register_user(app)
|
||||
register_user(app, name="user2", email="user2@examplectf.com")
|
||||
|
||||
# Give users points with an award
|
||||
gen_award(app.db, user_id=2)
|
||||
gen_award(app.db, user_id=3)
|
||||
|
||||
# First user unlocks hints
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 200
|
||||
r = client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
assert r.status_code == 200
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 200
|
||||
|
||||
# Second user unlocks hints
|
||||
with login_as_user(app, name="user2") as client:
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 200
|
||||
r = client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
assert r.status_code == 200
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_hints_admin_access():
|
||||
"""Can the users access /api/v1/hints if not admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/hints")
|
||||
assert r.status_code == 302
|
||||
r = client.post("/api/v1/hints", json="")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_hint_admin_access():
|
||||
"""Can the users patch/delete /api/v1/hint/<hint_id> if not admin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
chal = gen_challenge(app.db)
|
||||
gen_hint(app.db, chal.id, content="This is a hint", cost=1, type="standard")
|
||||
admin = login_as_user(app, "admin")
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.patch("/api/v1/hints/1", json="")
|
||||
assert r.status_code == 403
|
||||
r = client.delete("/api/v1/hints/1", json="")
|
||||
assert r.status_code == 403
|
||||
r_admin = admin.patch("/api/v1/hints/1", json={"cost": 2})
|
||||
assert r_admin.status_code == 200
|
||||
r_admin = admin.delete("/api/v1/hints/1", json="")
|
||||
assert r_admin.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_hints_accessible_public():
|
||||
"""Test that hints with no cost and no prerequsites can be viewed publicy"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Set challenges to be visible publicly
|
||||
set_config("challenge_visibility", "public")
|
||||
|
||||
register_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
gen_hint(
|
||||
app.db, chal.id, content="This is a free hint", cost=0, type="standard"
|
||||
)
|
||||
gen_hint(
|
||||
app.db, chal.id, content="This is a private hint", cost=1, type="standard"
|
||||
)
|
||||
gen_hint(
|
||||
app.db, chal.id, content="This is a private hint", cost=1, type="standard"
|
||||
)
|
||||
hint = Hints.query.filter_by(id=3).first()
|
||||
hint.requirements = {"prerequisites": [2]}
|
||||
app.db.session.commit()
|
||||
|
||||
with app.test_client() as non_logged_in_user:
|
||||
# Hints cannot be seen unless free public access is on
|
||||
r = non_logged_in_user.get("/api/v1/hints/1")
|
||||
assert r.status_code == 403
|
||||
errors = r.get_json()["errors"]
|
||||
assert errors == {"cost": ["You must login to unlock this hint"]}
|
||||
|
||||
# Enable free public access
|
||||
set_config("hints_free_public_access", True)
|
||||
r = non_logged_in_user.get("/api/v1/hints/1")
|
||||
hint = r.get_json()["data"]
|
||||
assert hint["content"] == "This is a free hint"
|
||||
|
||||
r = non_logged_in_user.get("/api/v1/hints/2")
|
||||
assert r.status_code == 403
|
||||
errors = r.get_json()["errors"]
|
||||
assert errors == {"cost": ["You must login to unlock this hint"]}
|
||||
|
||||
r = non_logged_in_user.get("/api/v1/hints/3")
|
||||
assert r.status_code == 403
|
||||
errors = r.get_json()["errors"]
|
||||
assert errors == {"cost": ["You must login to unlock this hint"]}
|
||||
|
||||
r = non_logged_in_user.post(
|
||||
"/api/v1/unlocks", json={"target": 2, "type": "hints"}
|
||||
)
|
||||
assert r.status_code == 403
|
||||
|
||||
# Set challenges to be visible to only authed
|
||||
set_config("challenge_visibility", "private")
|
||||
|
||||
# Free hints no longer visible to unauthed
|
||||
with app.test_client() as non_logged_in_user:
|
||||
r = non_logged_in_user.get("/api/v1/hints/1", json="")
|
||||
assert r.status_code == 403
|
||||
|
||||
# Verify existing hint behavior for authed users
|
||||
with login_as_user(app) as client:
|
||||
# Free hints require an unlock
|
||||
r = client.get("/api/v1/hints/1")
|
||||
hint = r.get_json()["data"]
|
||||
assert hint.get("content") is None
|
||||
|
||||
# Issue the unlock and then request the content
|
||||
r = client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
r = client.get("/api/v1/hints/1")
|
||||
hint = r.get_json()["data"]
|
||||
assert hint["content"] == "This is a free hint"
|
||||
|
||||
r = client.get("/api/v1/hints/2")
|
||||
assert r.status_code == 200
|
||||
assert "content" not in r.get_json()["data"]
|
||||
|
||||
r = client.get("/api/v1/hints/3")
|
||||
assert r.status_code == 403
|
||||
|
||||
gen_award(app.db, 2)
|
||||
|
||||
# Haven't unlocked the prereq hint
|
||||
r = client.get("/api/v1/hints/3")
|
||||
assert r.status_code == 403
|
||||
|
||||
# Unlock the prereq
|
||||
r = client.post("/api/v1/unlocks", json={"target": 2, "type": "hints"})
|
||||
assert r.status_code == 200
|
||||
r = client.get("/api/v1/hints/2")
|
||||
assert r.status_code == 200
|
||||
|
||||
# Attempt to unlock again but dont have the points
|
||||
r = client.get("/api/v1/hints/3")
|
||||
assert r.status_code == 200
|
||||
assert "content" not in r.get_json()["data"]
|
||||
|
||||
r = client.post("/api/v1/unlocks", json={"target": 3, "type": "hints"})
|
||||
assert r.status_code == 200
|
||||
r = client.get("/api/v1/hints/3")
|
||||
assert r.status_code == 200
|
||||
assert "content" in r.get_json()["data"]
|
||||
0
tests/api/v1/users/__init__.py
Normal file
0
tests/api/v1/users/__init__.py
Normal file
89
tests/api/v1/users/test_scoring.py
Normal file
89
tests/api/v1/users/test_scoring.py
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Users
|
||||
from CTFd.utils import set_config
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
login_as_user,
|
||||
register_user,
|
||||
simulate_user_activity,
|
||||
)
|
||||
|
||||
|
||||
def test_api_user_place_score_hidden_if_scores_hidden():
|
||||
"""/api/v1/users/me should not reveal user place if scores aren't visible"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
simulate_user_activity(app.db, user=user)
|
||||
|
||||
with login_as_user(app, name="user") as client:
|
||||
r = client.get("/api/v1/users/me", json="")
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["place"] == "1st"
|
||||
assert resp["data"]["score"] == 200
|
||||
|
||||
set_config("score_visibility", "hidden")
|
||||
with login_as_user(app, name="user") as client:
|
||||
r = client.get("/api/v1/users/me", json="")
|
||||
resp = r.get_json()
|
||||
# Users can see their own score but they cannot see their place
|
||||
# This is because a user can always sum up their own score but
|
||||
# they cannot determine their place without social information
|
||||
assert resp["data"]["place"] is None
|
||||
assert resp["data"]["score"] == 200
|
||||
|
||||
set_config("score_visibility", "admins")
|
||||
with login_as_user(app, name="user") as client:
|
||||
r = client.get("/api/v1/users/me", json="")
|
||||
resp = r.get_json()
|
||||
# The same behavior as above applies even under admins only score mode
|
||||
# The rationale is the same. Users can always sum their own score
|
||||
assert resp["data"]["place"] is None
|
||||
assert resp["data"]["score"] == 200
|
||||
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.get("/api/v1/users/2", json="")
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["place"] == "1st"
|
||||
assert resp["data"]["score"] is not None
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_public_user_place_score_hidden_if_scores_hidden():
|
||||
"""/api/v1/users/<user_id> should not reveal user place if scores aren't visible"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
simulate_user_activity(app.db, user=user)
|
||||
|
||||
with login_as_user(app, name="user") as client:
|
||||
r = client.get("/api/v1/users/2", json="")
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["place"] == "1st"
|
||||
assert resp["data"]["score"] is not None
|
||||
|
||||
set_config("score_visibility", "hidden")
|
||||
with login_as_user(app, name="user") as client:
|
||||
r = client.get("/api/v1/users/2", json="")
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["place"] is None
|
||||
assert resp["data"]["score"] is None
|
||||
|
||||
set_config("score_visibility", "admins")
|
||||
with login_as_user(app, name="user") as client:
|
||||
r = client.get("/api/v1/users/2", json="")
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["place"] is None
|
||||
assert resp["data"]["score"] is None
|
||||
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.get("/api/v1/users/2", json="")
|
||||
resp = r.get_json()
|
||||
assert resp["data"]["place"] == "1st"
|
||||
assert resp["data"]["score"] is not None
|
||||
destroy_ctfd(app)
|
||||
113
tests/api/v1/users/test_users.py
Normal file
113
tests/api/v1/users/test_users.py
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Users, db
|
||||
from CTFd.utils.crypto import verify_password
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user
|
||||
|
||||
|
||||
def test_api_self_ban():
|
||||
"""PATCH /api/v1/users/<user_id> should not allow a user to ban themselves"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.patch("/api/v1/users/1", json={"banned": True})
|
||||
resp = r.get_json()
|
||||
assert r.status_code == 400
|
||||
assert resp["success"] == False
|
||||
assert resp["errors"] == {"id": "You cannot ban yourself"}
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_modify_user_type():
|
||||
"""Can a user patch /api/v1/users/<user_id> to promote a user to admin and demote them to user"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.patch("/api/v1/users/2", json={"type": "admin"})
|
||||
assert r.status_code == 200
|
||||
user_data = r.get_json()["data"]
|
||||
assert user_data["name"] == "user"
|
||||
assert user_data["type"] == "admin"
|
||||
|
||||
r = client.patch("/api/v1/users/2", json={"type": "user"})
|
||||
assert r.status_code == 200
|
||||
user_data = r.get_json()["data"]
|
||||
assert user_data["name"] == "user"
|
||||
assert user_data["type"] == "user"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_can_query_by_user_emails():
|
||||
"""Can an admin user query /api/v1/users using a user's email address"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="testuser", email="user@findme.com")
|
||||
with login_as_user(app, "testuser") as client:
|
||||
r = client.get("/api/v1/users?field=email&q=findme", json=True)
|
||||
assert r.status_code == 400
|
||||
assert r.get_json()["errors"].get("field")
|
||||
with login_as_user(app, "admin") as client:
|
||||
r = client.get("/api/v1/users?field=email&q=findme", json=True)
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"][0]["id"] == 2
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_can_update_password_if_none_not_if_set():
|
||||
"""Can a user set their password if they do not currently have a password"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Create a user with a null password. Use raw SQL to bypass SQLAlchemy validates
|
||||
register_user(app, name="testuser", email="user@examplectf.com")
|
||||
db.session.execute("UPDATE users SET password=NULL WHERE name='testuser'")
|
||||
user = Users.query.filter_by(name="testuser").first()
|
||||
db.session.commit()
|
||||
assert user.password is None
|
||||
|
||||
with app.test_client() as client:
|
||||
# Login as user
|
||||
with client.session_transaction() as sess:
|
||||
sess["id"] = user.id
|
||||
r = client.get("/api/v1/users/me", json=True)
|
||||
assert r.status_code == 200
|
||||
|
||||
# Test that user can change password
|
||||
user = Users.query.filter_by(name="testuser").first()
|
||||
assert user.password is None
|
||||
data = {"password": "12345", "confirm": "password"}
|
||||
r = client.patch("/api/v1/users/me", json=data)
|
||||
assert r.status_code == 200
|
||||
|
||||
# Verify password is now set
|
||||
user = Users.query.filter_by(name="testuser").first()
|
||||
assert verify_password(plaintext="12345", ciphertext=user.password)
|
||||
|
||||
# Verify that password cannot be changed
|
||||
data = {"password": "noset", "confirm": "password"}
|
||||
r = client.patch("/api/v1/users/me", json=data)
|
||||
resp = r.get_json()
|
||||
assert resp["errors"]["confirm"] == ["Your previous password is incorrect"]
|
||||
assert r.status_code == 400
|
||||
|
||||
# Verify a regular user cannot patch another user
|
||||
register_user(
|
||||
app,
|
||||
name="testuser2",
|
||||
email="user2@examplectf.com",
|
||||
password="testinguser",
|
||||
)
|
||||
testuser = Users.query.filter_by(name="testuser2").first()
|
||||
assert verify_password(
|
||||
plaintext="testinguser", ciphertext=testuser.password
|
||||
)
|
||||
data = {"password": "password", "confirm": "password"}
|
||||
r = client.patch("/api/v1/users/3", json=data)
|
||||
assert r.status_code == 403
|
||||
testuser = Users.query.filter_by(name="testuser2").first()
|
||||
assert verify_password(
|
||||
plaintext="testinguser", ciphertext=testuser.password
|
||||
)
|
||||
|
||||
destroy_ctfd(app)
|
||||
59
tests/brackets/test_brackets.py
Normal file
59
tests/brackets/test_brackets.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from CTFd.models import Teams, Users
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_bracket,
|
||||
login_as_user,
|
||||
register_team,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_require_bracket_on_register():
|
||||
"""Require users to submit a bracket if there is a bracket configured"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_bracket(app.db)
|
||||
with app.test_client() as client:
|
||||
client.get("/register")
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
client.post("/register", data=data)
|
||||
login_as_user(app, raise_for_error=False)
|
||||
assert Users.query.filter_by(email="user@examplectf.com").count() == 0
|
||||
data["bracket_id"] = 1
|
||||
client.post("/register", data=data)
|
||||
login_as_user(app, raise_for_error=True)
|
||||
assert Users.query.filter_by(email="user@examplectf.com").count() == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_require_team_bracket_on_register():
|
||||
"""Test that brackets are required on team mode"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
gen_bracket(app.db, type="teams")
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
register_team(app, raise_for_error=False)
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "team",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
# Test that a team is not created
|
||||
client.post("/teams/new", data=data)
|
||||
assert Teams.query.count() == 0
|
||||
|
||||
# Test that the team is now created
|
||||
data["bracket_id"] = 1
|
||||
client.post("/teams/new", data=data)
|
||||
assert Teams.query.filter_by(id=1).first().bracket_id == 1
|
||||
destroy_ctfd(app)
|
||||
0
tests/cache/__init__.py
vendored
Normal file
0
tests/cache/__init__.py
vendored
Normal file
110
tests/cache/test_cache.py
vendored
Normal file
110
tests/cache/test_cache.py
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from redis.exceptions import ConnectionError
|
||||
|
||||
from CTFd.cache import clear_all_user_sessions, clear_user_session
|
||||
from CTFd.config import TestingConfig
|
||||
from CTFd.models import Users
|
||||
from CTFd.utils.security.auth import login_user
|
||||
from CTFd.utils.user import get_current_user, is_admin
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, register_user
|
||||
|
||||
|
||||
def test_clear_user_session():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
|
||||
# Users by default should have a non-admin type
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
with app.test_request_context("/"):
|
||||
login_user(user)
|
||||
user = get_current_user()
|
||||
assert user.id == 2
|
||||
assert user.type == "user"
|
||||
assert is_admin() is False
|
||||
|
||||
# Set the user's updated type
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
user.type = "admin"
|
||||
app.db.session.commit()
|
||||
|
||||
# Should still return False because this is still cached
|
||||
assert is_admin() is False
|
||||
|
||||
clear_user_session(user_id=2)
|
||||
|
||||
# Should now return True after clearing cache
|
||||
assert is_admin() is True
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_clear_all_user_sessions():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
|
||||
# Users by default should have a non-admin type
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
with app.test_request_context("/"):
|
||||
login_user(user)
|
||||
user = get_current_user()
|
||||
assert user.id == 2
|
||||
assert user.type == "user"
|
||||
assert is_admin() is False
|
||||
|
||||
# Set the user's updated type
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
user.type = "admin"
|
||||
app.db.session.commit()
|
||||
|
||||
# Should still return False because this is still cached
|
||||
assert is_admin() is False
|
||||
|
||||
clear_all_user_sessions()
|
||||
|
||||
# Should now return True after clearing cache
|
||||
assert is_admin() is True
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_cache_subclass_commands():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
from CTFd.cache import cache
|
||||
|
||||
cache.inc("testing_inc")
|
||||
resp = cache.inc("testing_inc")
|
||||
assert resp == 2
|
||||
assert cache.get("testing_inc") == 2
|
||||
cache.expire("testing_inc", 0)
|
||||
assert cache.get("testing_inc") is None
|
||||
resp = cache.inc("testing_inc")
|
||||
assert resp == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_redis_cache_subclass_commands():
|
||||
class RedisConfig(TestingConfig):
|
||||
REDIS_URL = "redis://localhost:6379/1"
|
||||
CACHE_REDIS_URL = "redis://localhost:6379/1"
|
||||
CACHE_TYPE = "redis"
|
||||
|
||||
try:
|
||||
app = create_ctfd(config=RedisConfig)
|
||||
except ConnectionError:
|
||||
print("Failed to connect to redis. Skipping test.")
|
||||
else:
|
||||
with app.app_context():
|
||||
from CTFd.cache import cache
|
||||
|
||||
cache.inc("testing_inc")
|
||||
resp = cache.inc("testing_inc")
|
||||
assert resp == 2
|
||||
assert cache.get("testing_inc") == 2
|
||||
cache.expire("testing_inc", 0)
|
||||
assert cache.get("testing_inc") is None
|
||||
resp = cache.inc("testing_inc")
|
||||
assert resp == 1
|
||||
destroy_ctfd(app)
|
||||
128
tests/cache/test_challenges.py
vendored
Normal file
128
tests/cache/test_challenges.py
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Users
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
login_as_user,
|
||||
register_user,
|
||||
simulate_user_activity,
|
||||
)
|
||||
|
||||
|
||||
def test_adding_challenge_clears_cache():
|
||||
"""
|
||||
Test that when we add a challenge, it appears in our challenge list
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
|
||||
with login_as_user(app) as client, login_as_user(
|
||||
app, name="admin", password="password"
|
||||
) as admin:
|
||||
req = client.get("/api/v1/challenges")
|
||||
data = req.get_json()
|
||||
assert data["data"] == []
|
||||
|
||||
challenge_data = {
|
||||
"name": "name",
|
||||
"category": "category",
|
||||
"description": "description",
|
||||
"value": 100,
|
||||
"state": "visible",
|
||||
"type": "standard",
|
||||
}
|
||||
|
||||
r = admin.post("/api/v1/challenges", json=challenge_data)
|
||||
assert r.get_json().get("data")["id"] == 1
|
||||
|
||||
req = client.get("/api/v1/challenges")
|
||||
data = req.get_json()
|
||||
assert data["data"] != []
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_deleting_challenge_clears_cache_solves():
|
||||
"""
|
||||
Test that deleting a challenge clears the cached solves for the challenge
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
simulate_user_activity(app.db, user)
|
||||
with login_as_user(app) as client, login_as_user(
|
||||
app, name="admin", password="password"
|
||||
) as admin:
|
||||
req = client.get("/api/v1/challenges")
|
||||
data = req.get_json()["data"]
|
||||
challenge = data[0]
|
||||
assert challenge["solves"] == 1
|
||||
from CTFd.utils.challenges import ( # noqa: I001
|
||||
get_solve_counts_for_challenges,
|
||||
get_solves_for_challenge_id,
|
||||
)
|
||||
|
||||
solves = get_solves_for_challenge_id(1)
|
||||
solve_counts = get_solve_counts_for_challenges()
|
||||
solves_req = client.get("/api/v1/challenges/1/solves").get_json()["data"]
|
||||
assert len(solves_req) == 1
|
||||
assert len(solves) == 1
|
||||
assert solve_counts[1] == 1
|
||||
|
||||
r = admin.delete("/api/v1/challenges/1", json="")
|
||||
assert r.status_code == 200
|
||||
|
||||
solve_counts = get_solve_counts_for_challenges()
|
||||
solves = get_solves_for_challenge_id(1)
|
||||
r = client.get("/api/v1/challenges/1/solves")
|
||||
assert r.status_code == 404
|
||||
assert len(solves) == 0
|
||||
assert solve_counts.get(1) is None
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_deleting_solve_clears_cache():
|
||||
"""
|
||||
Test that deleting a solve clears out the solve count cache
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
simulate_user_activity(app.db, user)
|
||||
with login_as_user(app) as client, login_as_user(
|
||||
app, name="admin", password="password"
|
||||
) as admin:
|
||||
req = client.get("/api/v1/challenges")
|
||||
data = req.get_json()["data"]
|
||||
challenge = data[0]
|
||||
assert challenge["solves"] == 1
|
||||
from CTFd.utils.challenges import ( # noqa: I001
|
||||
get_solve_counts_for_challenges,
|
||||
get_solves_for_challenge_id,
|
||||
)
|
||||
|
||||
solves = get_solves_for_challenge_id(1)
|
||||
solve_counts = get_solve_counts_for_challenges()
|
||||
solves_req = client.get("/api/v1/challenges/1/solves").get_json()["data"]
|
||||
assert len(solves_req) == 1
|
||||
assert len(solves) == 1
|
||||
assert solve_counts[1] == 1
|
||||
|
||||
r = admin.get("/api/v1/submissions/6", json="")
|
||||
assert r.get_json()["data"]["type"] == "correct"
|
||||
r = admin.delete("/api/v1/submissions/6", json="")
|
||||
assert r.status_code == 200
|
||||
r = admin.get("/api/v1/submissions/6", json="")
|
||||
assert r.status_code == 404
|
||||
|
||||
solve_counts = get_solve_counts_for_challenges()
|
||||
solves = get_solves_for_challenge_id(1)
|
||||
solves_req = client.get("/api/v1/challenges/1/solves").get_json()["data"]
|
||||
assert len(solves_req) == 0
|
||||
assert len(solves) == 0
|
||||
assert solve_counts.get(1) is None
|
||||
destroy_ctfd(app)
|
||||
0
tests/challenges/__init__.py
Normal file
0
tests/challenges/__init__.py
Normal file
61
tests/challenges/test_base_challenge.py
Normal file
61
tests/challenges/test_base_challenge.py
Normal file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import pytest
|
||||
from flask import request
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from CTFd.exceptions.challenges import ChallengeSolveException
|
||||
from CTFd.models import Solves
|
||||
from CTFd.plugins.challenges import BaseChallenge
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_solve,
|
||||
gen_team,
|
||||
gen_user,
|
||||
)
|
||||
|
||||
|
||||
def test_base_challenge_solve_creates_solve_when_missing():
|
||||
"""Test that BaseChallenge.solve() creates a solve if it doesn't exist"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
challenge = gen_challenge(app.db)
|
||||
user = gen_user(app.db, name="solver", email="solver@example.com")
|
||||
|
||||
assert Solves.query.count() == 0
|
||||
|
||||
with app.test_request_context(
|
||||
json={"submission": "flag"},
|
||||
environ_base={"REMOTE_ADDR": "127.0.0.1"},
|
||||
):
|
||||
BaseChallenge.solve(user, None, challenge, request)
|
||||
|
||||
assert Solves.query.count() == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_base_challenge_solve_raises_on_duplicate_solve():
|
||||
"""Test that BaseChallenge.solve() raises ChallengeSolveException on duplicate solve"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
challenge = gen_challenge(app.db)
|
||||
team = gen_team(app.db, member_count=1)
|
||||
user = team.members[0]
|
||||
|
||||
gen_solve(app.db, user_id=user.id, team_id=team.id, challenge_id=challenge.id)
|
||||
assert Solves.query.count() == 1
|
||||
|
||||
with app.test_request_context(
|
||||
json={"submission": "flag"},
|
||||
environ_base={"REMOTE_ADDR": "127.0.0.1"},
|
||||
):
|
||||
with pytest.raises(ChallengeSolveException) as exc:
|
||||
BaseChallenge.solve(user, team, challenge, request)
|
||||
|
||||
assert isinstance(exc.value.__cause__, IntegrityError)
|
||||
|
||||
assert Solves.query.count() == 1
|
||||
destroy_ctfd(app)
|
||||
275
tests/challenges/test_challenge_logic.py
Normal file
275
tests/challenges/test_challenge_logic.py
Normal file
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Challenges
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_flag,
|
||||
gen_team,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_all_flags_challenge_logic():
|
||||
"""Test a challenge that requires all flags to be submitted for solving"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="user1")
|
||||
admin = login_as_user(app, name="admin", password="password")
|
||||
|
||||
# Create a challenge
|
||||
challenge_data = {
|
||||
"name": "All Flags Challenge",
|
||||
"category": "Logic",
|
||||
"description": "Submit all flags to solve.",
|
||||
"value": 100,
|
||||
"state": "visible",
|
||||
"type": "standard",
|
||||
"logic": "all",
|
||||
}
|
||||
r = admin.post("/api/v1/challenges", json=challenge_data)
|
||||
challenge_id = r.get_json()["data"]["id"]
|
||||
|
||||
client = login_as_user(app, name="user1", password="password")
|
||||
|
||||
# Add multiple flags to the challenge
|
||||
flags = ["flag{one}", "flag{two}", "flag{three}"]
|
||||
for content in flags:
|
||||
gen_flag(app.db, challenge_id=challenge_id, content=content)
|
||||
|
||||
# Simulate "all flags" logic: user must submit all flags to solve
|
||||
# Submit only one flag
|
||||
submission = {"challenge_id": challenge_id, "submission": "flag{one}"}
|
||||
r = client.post("/api/v1/challenges/attempt", json=submission)
|
||||
assert r.get_json()["data"]["status"] == "partial"
|
||||
|
||||
# Submit two flags (simulate by submitting them one after another)
|
||||
submission = {"challenge_id": challenge_id, "submission": "flag{two}"}
|
||||
r = client.post("/api/v1/challenges/attempt", json=submission)
|
||||
assert r.get_json()["data"]["status"] == "partial"
|
||||
|
||||
# Submit all three flags
|
||||
submission = {"challenge_id": challenge_id, "submission": "flag{three}"}
|
||||
r = client.post("/api/v1/challenges/attempt", json=submission)
|
||||
assert r.get_json()["data"]["status"] == "correct"
|
||||
|
||||
# After all flags are submitted, the challenge should be marked as solved
|
||||
r = client.get(f"/api/v1/challenges/{challenge_id}/solves")
|
||||
solves = r.get_json()["data"]
|
||||
assert len(solves) == 1
|
||||
assert solves[0]["account_id"] == 2 # user1's ID (admin is 1)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_all_flags_challenge_logic_teams_mode():
|
||||
"""Test a challenge that requires all flags to be submitted for solving in teams mode"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
# Create admin for challenge creation
|
||||
admin = login_as_user(app, name="admin", password="password")
|
||||
|
||||
# Create a challenge with "all" logic
|
||||
challenge_data = {
|
||||
"name": "All Flags Team Challenge",
|
||||
"category": "Logic",
|
||||
"description": "Submit all flags to solve as a team.",
|
||||
"value": 100,
|
||||
"state": "visible",
|
||||
"type": "standard",
|
||||
"logic": "all",
|
||||
}
|
||||
r = admin.post("/api/v1/challenges", json=challenge_data)
|
||||
challenge_id = r.get_json()["data"]["id"]
|
||||
|
||||
c = Challenges.query.filter_by(id=challenge_id).first()
|
||||
assert c.logic == "all"
|
||||
|
||||
# Create a team with members using gen_team helper
|
||||
team = gen_team(
|
||||
app.db, name="test_team", email="team@examplectf.com", member_count=3
|
||||
)
|
||||
team_id = team.id
|
||||
|
||||
# Add multiple flags to the challenge
|
||||
flags = ["flag{team_one}", "flag{team_two}", "flag{team_three}"]
|
||||
for content in flags:
|
||||
gen_flag(app.db, challenge_id=challenge_id, content=content)
|
||||
|
||||
# Get team members for testing
|
||||
members = team.members
|
||||
user1_name = members[0].name
|
||||
user2_name = members[1].name
|
||||
user3_name = members[2].name
|
||||
|
||||
# Test that different team members can submit flags and it counts towards team progress
|
||||
|
||||
# User1 submits first flag
|
||||
client1 = login_as_user(app, name=user1_name, password="password")
|
||||
submission = {"challenge_id": challenge_id, "submission": "flag{team_one}"}
|
||||
r = client1.post("/api/v1/challenges/attempt", json=submission)
|
||||
assert r.get_json()["data"]["status"] == "partial"
|
||||
|
||||
# User2 submits second flag
|
||||
client2 = login_as_user(app, name=user2_name, password="password")
|
||||
submission = {"challenge_id": challenge_id, "submission": "flag{team_two}"}
|
||||
r = client2.post("/api/v1/challenges/attempt", json=submission)
|
||||
assert r.get_json()["data"]["status"] == "partial"
|
||||
|
||||
# User3 submits third flag - should complete the challenge for the team
|
||||
client3 = login_as_user(app, name=user3_name, password="password")
|
||||
submission = {"challenge_id": challenge_id, "submission": "flag{team_three}"}
|
||||
r = client3.post("/api/v1/challenges/attempt", json=submission)
|
||||
assert r.get_json()["data"]["status"] == "correct"
|
||||
|
||||
# Check that the team is marked as having solved the challenge
|
||||
r = admin.get(f"/api/v1/challenges/{challenge_id}/solves")
|
||||
solves = r.get_json()["data"]
|
||||
assert len(solves) == 1
|
||||
assert solves[0]["account_id"] == team_id
|
||||
|
||||
# Verify team score reflects the solve
|
||||
team_response = admin.get(f"/api/v1/teams/{team_id}")
|
||||
team_data = team_response.get_json()["data"]
|
||||
assert team_data["score"] == 100
|
||||
|
||||
# Test that another team member trying to submit the same flag gets "already_solved"
|
||||
submission = {"challenge_id": challenge_id, "submission": "flag{team_one}"}
|
||||
r = client1.post("/api/v1/challenges/attempt", json=submission)
|
||||
assert r.get_json()["data"]["status"] == "already_solved"
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_group_flags_challenge_logic_teams_mode():
|
||||
"""Test a challenge that requires each team member to submit any flag for solving in teams mode"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
# Create admin for challenge creation
|
||||
admin = login_as_user(app, name="admin", password="password")
|
||||
|
||||
# Create a challenge with "group" logic
|
||||
challenge_data = {
|
||||
"name": "Group Flags Team Challenge",
|
||||
"category": "Logic",
|
||||
"description": "Each team member must submit any flag to solve as a team.",
|
||||
"value": 100,
|
||||
"state": "visible",
|
||||
"type": "standard",
|
||||
"logic": "team",
|
||||
}
|
||||
r = admin.post("/api/v1/challenges", json=challenge_data)
|
||||
challenge_id = r.get_json()["data"]["id"]
|
||||
|
||||
c = Challenges.query.filter_by(id=challenge_id).first()
|
||||
assert c.logic == "team"
|
||||
|
||||
# Create a team with members using gen_team helper
|
||||
team = gen_team(
|
||||
app.db, name="test_team", email="team@examplectf.com", member_count=3
|
||||
)
|
||||
team_id = team.id
|
||||
|
||||
# Add multiple flags to the challenge
|
||||
flags = ["flag{group_one}", "flag{group_two}", "flag{group_three}"]
|
||||
for content in flags:
|
||||
gen_flag(app.db, challenge_id=challenge_id, content=content)
|
||||
|
||||
# Get team members for testing
|
||||
members = team.members
|
||||
user1_name = members[0].name
|
||||
user2_name = members[1].name
|
||||
user3_name = members[2].name
|
||||
|
||||
# Test that each team member must submit any flag for the team to solve
|
||||
|
||||
# User1 submits first flag - should be partial since not all members have submitted
|
||||
client1 = login_as_user(app, name=user1_name, password="password")
|
||||
submission = {"challenge_id": challenge_id, "submission": "flag{group_one}"}
|
||||
r = client1.post("/api/v1/challenges/attempt", json=submission)
|
||||
assert r.get_json()["data"]["status"] == "partial"
|
||||
|
||||
# User2 submits any flag (can be different) - still partial since user3 hasn't submitted
|
||||
client2 = login_as_user(app, name=user2_name, password="password")
|
||||
submission = {"challenge_id": challenge_id, "submission": "flag{group_two}"}
|
||||
r = client2.post("/api/v1/challenges/attempt", json=submission)
|
||||
assert r.get_json()["data"]["status"] == "partial"
|
||||
|
||||
# User3 submits any flag - should complete the challenge since all members have now submitted
|
||||
client3 = login_as_user(app, name=user3_name, password="password")
|
||||
submission = {
|
||||
"challenge_id": challenge_id,
|
||||
"submission": "flag{group_one}",
|
||||
} # Can reuse same flag
|
||||
r = client3.post("/api/v1/challenges/attempt", json=submission)
|
||||
assert r.get_json()["data"]["status"] == "correct"
|
||||
|
||||
# Check that the team is marked as having solved the challenge
|
||||
r = admin.get(f"/api/v1/challenges/{challenge_id}/solves")
|
||||
solves = r.get_json()["data"]
|
||||
assert len(solves) == 1
|
||||
assert solves[0]["account_id"] == team_id
|
||||
|
||||
# Verify team score reflects the solve
|
||||
team_response = admin.get(f"/api/v1/teams/{team_id}")
|
||||
team_data = team_response.get_json()["data"]
|
||||
assert team_data["score"] == 100
|
||||
|
||||
# Test that team members trying to submit again get "already_solved"
|
||||
submission = {"challenge_id": challenge_id, "submission": "flag{group_three}"}
|
||||
r = client1.post("/api/v1/challenges/attempt", json=submission)
|
||||
assert r.get_json()["data"]["status"] == "already_solved"
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenge_default_logic_is_any():
|
||||
"""Test that the default logic for a challenge is 'any'"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="user1")
|
||||
admin = login_as_user(app, name="admin", password="password")
|
||||
|
||||
# Create a challenge without specifying logic (should default to "any")
|
||||
challenge_data = {
|
||||
"name": "Default Logic Challenge",
|
||||
"category": "Test",
|
||||
"description": "Test challenge with default logic.",
|
||||
"value": 100,
|
||||
"state": "visible",
|
||||
"type": "standard",
|
||||
# Note: no "logic" field specified - should default to "any"
|
||||
}
|
||||
r = admin.post("/api/v1/challenges", json=challenge_data)
|
||||
challenge_id = r.get_json()["data"]["id"]
|
||||
|
||||
# Verify the challenge logic is set to "any" by default
|
||||
challenge = Challenges.query.filter_by(id=challenge_id).first()
|
||||
assert challenge.logic == "any"
|
||||
|
||||
# Test that "any" logic works correctly
|
||||
client = login_as_user(app, name="user1", password="password")
|
||||
|
||||
# Add multiple flags to the challenge
|
||||
flags = ["flag{first}", "flag{second}", "flag{third}"]
|
||||
for content in flags:
|
||||
gen_flag(app.db, challenge_id=challenge_id, content=content)
|
||||
|
||||
# With "any" logic, submitting any single correct flag should solve the challenge
|
||||
submission = {"challenge_id": challenge_id, "submission": "flag{second}"}
|
||||
r = client.post("/api/v1/challenges/attempt", json=submission)
|
||||
assert r.get_json()["data"]["status"] == "correct"
|
||||
|
||||
# Verify the challenge is marked as solved
|
||||
r = admin.get(f"/api/v1/challenges/{challenge_id}/solves")
|
||||
solves = r.get_json()["data"]
|
||||
assert len(solves) == 1
|
||||
assert solves[0]["account_id"] == 2 # user1's ID (admin is 1)
|
||||
|
||||
# Test that submitting another flag returns "already_solved"
|
||||
submission = {"challenge_id": challenge_id, "submission": "flag{first}"}
|
||||
r = client.post("/api/v1/challenges/attempt", json=submission)
|
||||
assert r.get_json()["data"]["status"] == "already_solved"
|
||||
|
||||
destroy_ctfd(app)
|
||||
76
tests/challenges/test_challenge_types.py
Normal file
76
tests/challenges/test_challenge_types.py
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Challenges
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user
|
||||
|
||||
|
||||
def test_missing_challenge_type():
|
||||
"""Test that missing challenge types don't cause total challenge rendering failure"""
|
||||
app = create_ctfd(enable_plugins=True)
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
challenge_data = {
|
||||
"name": "name",
|
||||
"category": "category",
|
||||
"description": "description",
|
||||
"initial": 100,
|
||||
"decay": 20,
|
||||
"minimum": 1,
|
||||
"state": "visible",
|
||||
"type": "dynamic",
|
||||
}
|
||||
|
||||
r = client.post("/api/v1/challenges", json=challenge_data)
|
||||
assert r.get_json().get("data")["id"] == 1
|
||||
assert r.get_json().get("data")["type"] == "dynamic"
|
||||
|
||||
chal_count = Challenges.query.count()
|
||||
assert chal_count == 1
|
||||
|
||||
# Delete the dynamic challenge type
|
||||
from CTFd.plugins.challenges import CHALLENGE_CLASSES
|
||||
|
||||
del CHALLENGE_CLASSES["dynamic"]
|
||||
|
||||
r = client.get("/admin/challenges")
|
||||
assert r.status_code == 200
|
||||
assert b"dynamic" in r.data
|
||||
|
||||
r = client.get("/admin/challenges/1")
|
||||
assert r.status_code == 500
|
||||
assert b"The underlying challenge type (dynamic) is not installed" in r.data
|
||||
|
||||
challenge_data = {
|
||||
"name": "name",
|
||||
"category": "category",
|
||||
"description": "description",
|
||||
"value": 100,
|
||||
"state": "visible",
|
||||
"type": "standard",
|
||||
}
|
||||
r = client.post("/api/v1/challenges", json=challenge_data)
|
||||
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
|
||||
# We should still see the one visible standard challenge
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 200
|
||||
assert len(r.json["data"]) == 1
|
||||
assert r.json["data"][0]["type"] == "standard"
|
||||
|
||||
# We cannot load the broken challenge
|
||||
r = client.get("/api/v1/challenges/1")
|
||||
assert r.status_code == 500
|
||||
assert (
|
||||
"The underlying challenge type (dynamic) is not installed"
|
||||
in r.json["message"]
|
||||
)
|
||||
|
||||
# We can load other challenges
|
||||
r = client.get("/api/v1/challenges/2")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
402
tests/challenges/test_dynamic.py
Normal file
402
tests/challenges/test_dynamic.py
Normal file
@@ -0,0 +1,402 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Challenges
|
||||
from CTFd.plugins.dynamic_challenges import DynamicChallenge, DynamicValueChallenge
|
||||
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_dynamic_challenge():
|
||||
"""Test that dynamic challenges can be made from the API/admin panel"""
|
||||
app = create_ctfd(enable_plugins=True)
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
challenge_data = {
|
||||
"name": "name",
|
||||
"category": "category",
|
||||
"description": "description",
|
||||
"initial": 100,
|
||||
"decay": 20,
|
||||
"minimum": 1,
|
||||
"state": "hidden",
|
||||
"type": "dynamic",
|
||||
}
|
||||
|
||||
r = client.post("/api/v1/challenges", json=challenge_data)
|
||||
assert r.get_json().get("data")["id"] == 1
|
||||
|
||||
challenges = DynamicChallenge.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
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_can_update_dynamic_challenge():
|
||||
app = create_ctfd(enable_plugins=True)
|
||||
with app.app_context():
|
||||
challenge_data = {
|
||||
"name": "name",
|
||||
"category": "category",
|
||||
"description": "description",
|
||||
"initial": 100,
|
||||
"decay": 20,
|
||||
"minimum": 1,
|
||||
"state": "hidden",
|
||||
"type": "dynamic",
|
||||
}
|
||||
req = FakeRequest(form=challenge_data)
|
||||
challenge = DynamicValueChallenge.create(req)
|
||||
|
||||
assert challenge.value == 100
|
||||
assert challenge.initial == 100
|
||||
assert challenge.decay == 20
|
||||
assert challenge.minimum == 1
|
||||
|
||||
challenge_data = {
|
||||
"name": "new_name",
|
||||
"category": "category",
|
||||
"description": "new_description",
|
||||
"value": "200",
|
||||
"initial": "200",
|
||||
"decay": "40",
|
||||
"minimum": "5",
|
||||
"max_attempts": "0",
|
||||
"state": "visible",
|
||||
}
|
||||
|
||||
req = FakeRequest(form=challenge_data)
|
||||
challenge = DynamicValueChallenge.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.state == "visible"
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_can_add_requirement_dynamic_challenge():
|
||||
"""Test that requirements can be added to dynamic challenges"""
|
||||
app = create_ctfd(enable_plugins=True)
|
||||
with app.app_context():
|
||||
challenge_data = {
|
||||
"name": "name",
|
||||
"category": "category",
|
||||
"description": "description",
|
||||
"initial": 100,
|
||||
"decay": 20,
|
||||
"minimum": 1,
|
||||
"state": "hidden",
|
||||
"type": "dynamic",
|
||||
}
|
||||
req = FakeRequest(form=challenge_data)
|
||||
challenge = DynamicValueChallenge.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",
|
||||
"max_attempts": "0",
|
||||
"state": "visible",
|
||||
}
|
||||
|
||||
req = FakeRequest(form=challenge_data)
|
||||
challenge = DynamicValueChallenge.create(req)
|
||||
|
||||
assert challenge.name == "second_name"
|
||||
assert challenge.description == "new_description"
|
||||
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 = DynamicValueChallenge.update(challenge, req)
|
||||
|
||||
assert challenge.requirements == [1]
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_can_delete_dynamic_challenge():
|
||||
"""Test that dynamic challenges can be deleted"""
|
||||
app = create_ctfd(enable_plugins=True)
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
challenge_data = {
|
||||
"name": "name",
|
||||
"category": "category",
|
||||
"description": "description",
|
||||
"initial": 100,
|
||||
"decay": 20,
|
||||
"minimum": 1,
|
||||
"state": "hidden",
|
||||
"type": "dynamic",
|
||||
}
|
||||
|
||||
r = client.post("/api/v1/challenges", json=challenge_data)
|
||||
assert r.get_json().get("data")["id"] == 1
|
||||
|
||||
challenges = DynamicChallenge.query.all()
|
||||
assert len(challenges) == 1
|
||||
|
||||
challenge = challenges[0]
|
||||
DynamicValueChallenge.delete(challenge)
|
||||
|
||||
challenges = DynamicChallenge.query.all()
|
||||
assert len(challenges) == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_dynamic_challenge_loses_value_properly():
|
||||
app = create_ctfd(enable_plugins=True)
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
challenge_data = {
|
||||
"name": "name",
|
||||
"category": "category",
|
||||
"description": "description",
|
||||
"initial": 100,
|
||||
"decay": 20,
|
||||
"minimum": 1,
|
||||
"state": "visible",
|
||||
"type": "dynamic",
|
||||
}
|
||||
|
||||
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 = DynamicChallenge.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_dynamic_challenge_doesnt_lose_value_on_update():
|
||||
"""Dynamic challenge updates without changing any values or solves shouldn't change the current value. See #1043"""
|
||||
app = create_ctfd(enable_plugins=True)
|
||||
with app.app_context():
|
||||
challenge_data = {
|
||||
"name": "name",
|
||||
"category": "category",
|
||||
"description": "description",
|
||||
"initial": 10000,
|
||||
"decay": 4,
|
||||
"minimum": 10,
|
||||
"state": "visible",
|
||||
"type": "dynamic",
|
||||
}
|
||||
req = FakeRequest(form=challenge_data)
|
||||
challenge = DynamicValueChallenge.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 = DynamicValueChallenge.update(chal, req)
|
||||
assert prev_chal_value == chal.value
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_dynamic_challenge_value_isnt_affected_by_hidden_users():
|
||||
app = create_ctfd(enable_plugins=True)
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
challenge_data = {
|
||||
"name": "name",
|
||||
"category": "category",
|
||||
"description": "description",
|
||||
"initial": 100,
|
||||
"decay": 20,
|
||||
"minimum": 1,
|
||||
"state": "visible",
|
||||
"type": "dynamic",
|
||||
}
|
||||
|
||||
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 = DynamicChallenge.query.filter_by(id=1).first()
|
||||
assert chal.value == chal.initial
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_dynamic_challenges_reset():
|
||||
app = create_ctfd(enable_plugins=True)
|
||||
with app.app_context():
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
challenge_data = {
|
||||
"name": "name",
|
||||
"category": "category",
|
||||
"description": "description",
|
||||
"initial": 100,
|
||||
"decay": 20,
|
||||
"minimum": 1,
|
||||
"state": "hidden",
|
||||
"type": "dynamic",
|
||||
}
|
||||
|
||||
r = client.post("/api/v1/challenges", json=challenge_data)
|
||||
assert Challenges.query.count() == 1
|
||||
assert DynamicChallenge.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
|
||||
assert DynamicChallenge.query.count() == 0
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_dynamic_challenge_linear_loses_value_properly():
|
||||
app = create_ctfd(enable_plugins=True)
|
||||
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",
|
||||
"initial": 100,
|
||||
"decay": 5,
|
||||
"minimum": 1,
|
||||
"state": "visible",
|
||||
"type": "dynamic",
|
||||
}
|
||||
|
||||
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 = DynamicChallenge.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)
|
||||
598
tests/challenges/test_ratings.py
Normal file
598
tests/challenges/test_ratings.py
Normal 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)
|
||||
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)
|
||||
52
tests/constants/test_constants.py
Normal file
52
tests/constants/test_constants.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from CTFd.constants import JinjaEnum, JSEnum, RawEnum
|
||||
from tests.helpers import create_ctfd, destroy_ctfd
|
||||
|
||||
|
||||
def test_RawEnum():
|
||||
class Colors(str, RawEnum):
|
||||
RED = "red"
|
||||
GREEN = "green"
|
||||
BLUE = "blue"
|
||||
|
||||
class Numbers(str, RawEnum):
|
||||
ONE = 1
|
||||
TWO = 2
|
||||
THREE = 3
|
||||
|
||||
assert Colors.RED == "red"
|
||||
assert Colors.GREEN == "green"
|
||||
assert Colors.BLUE == "blue"
|
||||
assert Colors.test("red") is True
|
||||
assert Colors.test("purple") is False
|
||||
assert str(Numbers.ONE) == "1"
|
||||
assert sorted(Colors.keys()) == sorted(["RED", "GREEN", "BLUE"])
|
||||
assert sorted(Colors.values()) == sorted(["red", "green", "blue"])
|
||||
|
||||
|
||||
def test_JSEnum():
|
||||
import json
|
||||
|
||||
from CTFd.constants import JS_ENUMS # noqa: I001
|
||||
|
||||
@JSEnum
|
||||
class Colors(str, RawEnum):
|
||||
RED = "red"
|
||||
GREEN = "green"
|
||||
BLUE = "blue"
|
||||
|
||||
assert JS_ENUMS["Colors"] == {"RED": "red", "GREEN": "green", "BLUE": "blue"}
|
||||
assert json.dumps(JS_ENUMS)
|
||||
|
||||
|
||||
def test_JinjaEnum():
|
||||
@JinjaEnum
|
||||
class Colors(str, RawEnum):
|
||||
RED = "red"
|
||||
GREEN = "green"
|
||||
BLUE = "blue"
|
||||
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
assert app.jinja_env.globals["Colors"] is Colors
|
||||
assert app.jinja_env.globals["Colors"].RED == "red"
|
||||
destroy_ctfd(app)
|
||||
9
tests/constants/time.py
Normal file
9
tests/constants/time.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from CTFd.constants import RawEnum
|
||||
|
||||
|
||||
class FreezeTimes(str, RawEnum):
|
||||
NOT_STARTED = "2017-10-3" # Tuesday, October 3, 2017
|
||||
STARTED = "2017-10-5" # Thursday, October 5, 2017
|
||||
ENDED = "2017-10-7" # Saturday, October 7, 2017
|
||||
START = "1507089600" # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
END = "1507262400" # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
624
tests/helpers.py
Normal file
624
tests/helpers.py
Normal file
@@ -0,0 +1,624 @@
|
||||
import datetime
|
||||
import gc
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
from collections import namedtuple
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import requests
|
||||
from flask.testing import FlaskClient
|
||||
from freezegun import freeze_time
|
||||
from sqlalchemy.engine.url import make_url
|
||||
from sqlalchemy_utils import drop_database
|
||||
from werkzeug.datastructures import Headers
|
||||
|
||||
from CTFd import create_app
|
||||
from CTFd.cache import cache, clear_challenges, clear_ratings, clear_standings
|
||||
from CTFd.config import TestingConfig
|
||||
from CTFd.constants.themes import DEFAULT_THEME
|
||||
from CTFd.models import (
|
||||
Awards,
|
||||
Brackets,
|
||||
ChallengeComments,
|
||||
ChallengeFiles,
|
||||
Challenges,
|
||||
ChallengeTopics,
|
||||
Comments,
|
||||
Fails,
|
||||
Fields,
|
||||
Files,
|
||||
Flags,
|
||||
Hints,
|
||||
Notifications,
|
||||
PageComments,
|
||||
PageFiles,
|
||||
Pages,
|
||||
Ratings,
|
||||
Solutions,
|
||||
Solves,
|
||||
Tags,
|
||||
TeamComments,
|
||||
Teams,
|
||||
Tokens,
|
||||
Topics,
|
||||
Tracking,
|
||||
Unlocks,
|
||||
UserComments,
|
||||
Users,
|
||||
)
|
||||
from CTFd.utils import set_config
|
||||
from tests.constants.time import FreezeTimes
|
||||
|
||||
text_type = str
|
||||
binary_type = bytes
|
||||
|
||||
|
||||
FakeRequest = namedtuple("FakeRequest", ["form"])
|
||||
|
||||
|
||||
class CTFdTestClient(FlaskClient):
|
||||
def open(self, *args, **kwargs):
|
||||
if kwargs.get("json") is not None:
|
||||
with self.session_transaction() as sess:
|
||||
api_key_headers = Headers({"CSRF-Token": sess.get("nonce")})
|
||||
headers = kwargs.pop("headers", Headers())
|
||||
if isinstance(headers, dict):
|
||||
headers = Headers(headers)
|
||||
headers.extend(api_key_headers)
|
||||
kwargs["headers"] = headers
|
||||
return super(CTFdTestClient, self).open(*args, **kwargs)
|
||||
|
||||
|
||||
class ctftime:
|
||||
@contextmanager
|
||||
def init():
|
||||
"""
|
||||
This context manager can be used to setup start and end dates for a test CTFd
|
||||
"""
|
||||
try:
|
||||
set_config("start", FreezeTimes.START)
|
||||
set_config("end", FreezeTimes.END)
|
||||
yield
|
||||
finally:
|
||||
set_config("start", None)
|
||||
set_config("end", None)
|
||||
|
||||
@contextmanager
|
||||
def not_started():
|
||||
"""
|
||||
This context manager sets the current time to before the start date of the test CTFd
|
||||
"""
|
||||
try:
|
||||
freezer = freeze_time(FreezeTimes.NOT_STARTED)
|
||||
frozen_time = freezer.start()
|
||||
yield frozen_time
|
||||
finally:
|
||||
freezer.stop()
|
||||
|
||||
@contextmanager
|
||||
def started():
|
||||
"""
|
||||
This context manager sets the current time to the start date of the test CTFd
|
||||
"""
|
||||
try:
|
||||
freezer = freeze_time(FreezeTimes.STARTED)
|
||||
frozen_time = freezer.start()
|
||||
yield frozen_time
|
||||
finally:
|
||||
freezer.stop()
|
||||
|
||||
@contextmanager
|
||||
def ended():
|
||||
"""
|
||||
This context manager sets the current time to after the end date of the test CTFd
|
||||
"""
|
||||
try:
|
||||
freezer = freeze_time(FreezeTimes.ENDED)
|
||||
frozen_time = freezer.start()
|
||||
yield frozen_time
|
||||
finally:
|
||||
freezer.stop()
|
||||
|
||||
|
||||
def create_ctfd(
|
||||
ctf_name="CTFd",
|
||||
ctf_description="CTF description",
|
||||
name="admin",
|
||||
email="admin@examplectf.com",
|
||||
password="password",
|
||||
user_mode="users",
|
||||
setup=True,
|
||||
enable_plugins=False,
|
||||
application_root="/",
|
||||
config=TestingConfig,
|
||||
ctf_theme=None,
|
||||
):
|
||||
if enable_plugins:
|
||||
config.SAFE_MODE = False
|
||||
else:
|
||||
config.SAFE_MODE = True
|
||||
|
||||
if ctf_theme is None:
|
||||
ctf_theme = DEFAULT_THEME
|
||||
|
||||
config.APPLICATION_ROOT = application_root
|
||||
url = make_url(config.SQLALCHEMY_DATABASE_URI)
|
||||
if url.database:
|
||||
url = url.set(database=str(uuid.uuid4()))
|
||||
config.SQLALCHEMY_DATABASE_URI = str(url)
|
||||
|
||||
app = create_app(config)
|
||||
app.test_client_class = CTFdTestClient
|
||||
|
||||
if setup:
|
||||
app = setup_ctfd(
|
||||
app,
|
||||
ctf_name=ctf_name,
|
||||
ctf_description=ctf_description,
|
||||
name=name,
|
||||
email=email,
|
||||
password=password,
|
||||
user_mode=user_mode,
|
||||
ctf_theme=ctf_theme,
|
||||
)
|
||||
return app
|
||||
|
||||
|
||||
def setup_ctfd(
|
||||
app,
|
||||
ctf_name="CTFd",
|
||||
ctf_description="CTF description",
|
||||
name="admin",
|
||||
email="admin@examplectf.com",
|
||||
password="password",
|
||||
user_mode="users",
|
||||
ctf_theme=None,
|
||||
):
|
||||
if ctf_theme is None:
|
||||
ctf_theme = DEFAULT_THEME
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
client.get("/setup") # Populate session with nonce
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"ctf_name": ctf_name,
|
||||
"ctf_description": ctf_description,
|
||||
"name": name,
|
||||
"email": email,
|
||||
"password": password,
|
||||
"user_mode": user_mode,
|
||||
"nonce": sess.get("nonce"),
|
||||
"ctf_theme": ctf_theme,
|
||||
}
|
||||
client.post("/setup", data=data)
|
||||
return app
|
||||
|
||||
|
||||
def destroy_ctfd(app):
|
||||
with app.app_context():
|
||||
gc.collect() # Garbage collect (necessary in the case of dataset freezes to clean database connections)
|
||||
cache.clear()
|
||||
drop_database(app.config["SQLALCHEMY_DATABASE_URI"])
|
||||
|
||||
|
||||
def register_user(
|
||||
app,
|
||||
name="user",
|
||||
email="user@examplectf.com",
|
||||
password="password",
|
||||
bracket_id=None,
|
||||
raise_for_error=True,
|
||||
):
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
client.get("/register")
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": name,
|
||||
"email": email,
|
||||
"password": password,
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
if bracket_id:
|
||||
data["bracket_id"] = bracket_id
|
||||
client.post("/register", data=data)
|
||||
if raise_for_error:
|
||||
with client.session_transaction() as sess:
|
||||
assert sess["id"]
|
||||
assert sess["nonce"]
|
||||
assert sess["hash"]
|
||||
|
||||
|
||||
def register_team(app, name="team", password="password", raise_for_error=True):
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
client.get("/team")
|
||||
with client.session_transaction() as sess:
|
||||
data = {"name": name, "password": password, "nonce": sess.get("nonce")}
|
||||
r = client.post("/teams/new", data=data)
|
||||
if raise_for_error:
|
||||
assert r.status_code == 302
|
||||
return client
|
||||
|
||||
|
||||
def login_as_user(app, name="user", password="password", raise_for_error=True):
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
data = {"name": name, "password": password, "nonce": sess.get("nonce")}
|
||||
client.post("/login", data=data)
|
||||
if raise_for_error:
|
||||
with client.session_transaction() as sess:
|
||||
assert sess["id"]
|
||||
assert sess["nonce"]
|
||||
assert sess["hash"]
|
||||
return client
|
||||
|
||||
|
||||
def login_with_mlc(
|
||||
app,
|
||||
name="user",
|
||||
scope="profile%20team",
|
||||
email="user@examplectf.com",
|
||||
oauth_id=1337,
|
||||
team_name="TestTeam",
|
||||
team_oauth_id=1234,
|
||||
raise_for_error=True,
|
||||
):
|
||||
with app.test_client() as client, patch.object(
|
||||
requests, "get"
|
||||
) as fake_get_request, patch.object(requests, "post") as fake_post_request:
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
nonce = sess["nonce"]
|
||||
|
||||
redirect_url = "{endpoint}?response_type=code&client_id={client_id}&scope={scope}&state={state}".format(
|
||||
endpoint=app.config["OAUTH_AUTHORIZATION_ENDPOINT"],
|
||||
client_id=app.config["OAUTH_CLIENT_ID"],
|
||||
scope=scope,
|
||||
state=nonce,
|
||||
)
|
||||
|
||||
r = client.get("/oauth", follow_redirects=False)
|
||||
assert r.location == redirect_url
|
||||
|
||||
fake_post_response = Mock()
|
||||
fake_post_request.return_value = fake_post_response
|
||||
fake_post_response.status_code = 200
|
||||
fake_post_response.json = lambda: {"access_token": "fake_mlc_access_token"}
|
||||
|
||||
fake_get_response = Mock()
|
||||
fake_get_request.return_value = fake_get_response
|
||||
fake_get_response.status_code = 200
|
||||
fake_get_response.json = lambda: {
|
||||
"id": oauth_id,
|
||||
"name": name,
|
||||
"email": email,
|
||||
"team": {"id": team_oauth_id, "name": team_name},
|
||||
}
|
||||
|
||||
client.get(
|
||||
"/redirect?code={code}&state={state}".format(
|
||||
code="mlc_test_code", state=nonce
|
||||
),
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
if raise_for_error:
|
||||
with client.session_transaction() as sess:
|
||||
assert sess["id"]
|
||||
assert sess["nonce"]
|
||||
assert sess["hash"]
|
||||
return client
|
||||
|
||||
|
||||
def get_scores(user):
|
||||
r = user.get("/api/v1/scoreboard")
|
||||
scores = r.get_json()
|
||||
return scores["data"]
|
||||
|
||||
|
||||
def random_string(n=5):
|
||||
return "".join(
|
||||
random.choice(string.ascii_letters + string.digits) for _ in range(n)
|
||||
)
|
||||
|
||||
|
||||
def random_int(start=2147483647, stop=None, step=1):
|
||||
return random.randrange(start, stop, step)
|
||||
|
||||
|
||||
def gen_challenge(
|
||||
db,
|
||||
name="chal_name",
|
||||
description="chal_description",
|
||||
value=100,
|
||||
category="chal_category",
|
||||
type="standard",
|
||||
state="visible",
|
||||
**kwargs
|
||||
):
|
||||
chal = Challenges(
|
||||
name=name,
|
||||
description=description,
|
||||
value=value,
|
||||
category=category,
|
||||
type=type,
|
||||
state=state,
|
||||
**kwargs
|
||||
)
|
||||
db.session.add(chal)
|
||||
db.session.commit()
|
||||
clear_challenges()
|
||||
return chal
|
||||
|
||||
|
||||
def gen_award(db, user_id, team_id=None, name="award_name", value=100):
|
||||
award = Awards(user_id=user_id, team_id=team_id, name=name, value=value)
|
||||
award.date = datetime.datetime.utcnow()
|
||||
db.session.add(award)
|
||||
db.session.commit()
|
||||
clear_standings()
|
||||
return award
|
||||
|
||||
|
||||
def gen_tag(db, challenge_id, value="tag_tag", **kwargs):
|
||||
tag = Tags(challenge_id=challenge_id, value=value, **kwargs)
|
||||
db.session.add(tag)
|
||||
db.session.commit()
|
||||
return tag
|
||||
|
||||
|
||||
def gen_topic(db, challenge_id, value="topic", **kwargs):
|
||||
topic = Topics(value=value, **kwargs)
|
||||
db.session.add(topic)
|
||||
db.session.commit()
|
||||
|
||||
challenge_topic = ChallengeTopics(challenge_id=challenge_id, topic_id=topic.id)
|
||||
db.session.add(challenge_topic)
|
||||
db.session.commit()
|
||||
return challenge_topic
|
||||
|
||||
|
||||
def gen_file(db, location, challenge_id=None, page_id=None):
|
||||
if challenge_id:
|
||||
f = ChallengeFiles(challenge_id=challenge_id, location=location)
|
||||
elif page_id:
|
||||
f = PageFiles(page_id=page_id, location=location)
|
||||
else:
|
||||
f = Files(location=location)
|
||||
db.session.add(f)
|
||||
db.session.commit()
|
||||
return f
|
||||
|
||||
|
||||
def gen_flag(db, challenge_id, content="flag", type="static", data=None, **kwargs):
|
||||
flag = Flags(challenge_id=challenge_id, content=content, type=type, **kwargs)
|
||||
if data:
|
||||
flag.data = data
|
||||
db.session.add(flag)
|
||||
db.session.commit()
|
||||
return flag
|
||||
|
||||
|
||||
def gen_user(
|
||||
db, name="user_name", email="user@examplectf.com", password="password", **kwargs
|
||||
):
|
||||
user = Users(name=name, email=email, password=password, **kwargs)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user
|
||||
|
||||
|
||||
def gen_team(
|
||||
db,
|
||||
name="team_name",
|
||||
email="team@examplectf.com",
|
||||
password="password",
|
||||
member_count=4,
|
||||
**kwargs
|
||||
):
|
||||
team = Teams(name=name, email=email, password=password, **kwargs)
|
||||
for i in range(member_count):
|
||||
name = "user-{}-{}".format(random_string(), str(i))
|
||||
user = gen_user(db, name=name, email=name + "@examplectf.com", team_id=team.id)
|
||||
if i == 0:
|
||||
team.captain_id = user.id
|
||||
team.members.append(user)
|
||||
db.session.add(team)
|
||||
db.session.commit()
|
||||
return team
|
||||
|
||||
|
||||
def gen_hint(
|
||||
db, challenge_id, content="This is a hint", cost=0, type="standard", **kwargs
|
||||
):
|
||||
hint = Hints(
|
||||
challenge_id=challenge_id, content=content, cost=cost, type=type, **kwargs
|
||||
)
|
||||
db.session.add(hint)
|
||||
db.session.commit()
|
||||
return hint
|
||||
|
||||
|
||||
def gen_unlock(db, user_id, team_id=None, target=None, type="hints"):
|
||||
unlock = Unlocks(user_id=user_id, team_id=team_id, target=target, type=type)
|
||||
db.session.add(unlock)
|
||||
db.session.commit()
|
||||
return unlock
|
||||
|
||||
|
||||
def gen_solve(
|
||||
db,
|
||||
user_id,
|
||||
team_id=None,
|
||||
challenge_id=None,
|
||||
ip="127.0.0.1",
|
||||
provided="rightkey",
|
||||
**kwargs
|
||||
):
|
||||
solve = Solves(
|
||||
user_id=user_id,
|
||||
team_id=team_id,
|
||||
challenge_id=challenge_id,
|
||||
ip=ip,
|
||||
provided=provided,
|
||||
**kwargs
|
||||
)
|
||||
solve.date = datetime.datetime.utcnow()
|
||||
db.session.add(solve)
|
||||
db.session.commit()
|
||||
clear_standings()
|
||||
clear_challenges()
|
||||
return solve
|
||||
|
||||
|
||||
def gen_rating(db, user_id, challenge_id, value=1, review="Great challenge!", **kwargs):
|
||||
rating = Ratings(
|
||||
user_id=user_id, challenge_id=challenge_id, value=value, review=review, **kwargs
|
||||
)
|
||||
db.session.add(rating)
|
||||
db.session.commit()
|
||||
clear_ratings()
|
||||
return rating
|
||||
|
||||
|
||||
def gen_fail(
|
||||
db,
|
||||
user_id,
|
||||
team_id=None,
|
||||
challenge_id=None,
|
||||
ip="127.0.0.1",
|
||||
provided="wrongkey",
|
||||
**kwargs
|
||||
):
|
||||
fail = Fails(
|
||||
user_id=user_id,
|
||||
team_id=team_id,
|
||||
challenge_id=challenge_id,
|
||||
ip=ip,
|
||||
provided=provided,
|
||||
**kwargs
|
||||
)
|
||||
fail.date = datetime.datetime.utcnow()
|
||||
db.session.add(fail)
|
||||
db.session.commit()
|
||||
return fail
|
||||
|
||||
|
||||
def gen_tracking(db, user_id=None, ip="127.0.0.1", **kwargs):
|
||||
tracking = Tracking(ip=ip, user_id=user_id, **kwargs)
|
||||
db.session.add(tracking)
|
||||
db.session.commit()
|
||||
return tracking
|
||||
|
||||
|
||||
def gen_page(db, title, route, content, draft=False, auth_required=False, **kwargs):
|
||||
page = Pages(
|
||||
title=title,
|
||||
route=route,
|
||||
content=content,
|
||||
draft=draft,
|
||||
auth_required=auth_required,
|
||||
**kwargs
|
||||
)
|
||||
db.session.add(page)
|
||||
db.session.commit()
|
||||
return page
|
||||
|
||||
|
||||
def gen_notification(db, title="title", content="content"):
|
||||
notif = Notifications(title=title, content=content)
|
||||
db.session.add(notif)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def gen_token(db, type="user", user_id=None, expiration=None):
|
||||
token = Tokens(type=type, user_id=user_id, expiration=expiration)
|
||||
db.session.add(token)
|
||||
db.session.commit()
|
||||
return token
|
||||
|
||||
|
||||
def gen_comment(db, content="comment", author_id=None, type="challenge", **kwargs):
|
||||
if type == "challenge":
|
||||
model = ChallengeComments
|
||||
elif type == "user":
|
||||
model = UserComments
|
||||
elif type == "team":
|
||||
model = TeamComments
|
||||
elif type == "page":
|
||||
model = PageComments
|
||||
else:
|
||||
model = Comments
|
||||
|
||||
comment = model(content=content, author_id=author_id, type=type, **kwargs)
|
||||
db.session.add(comment)
|
||||
db.session.commit()
|
||||
return comment
|
||||
|
||||
|
||||
def gen_solution(db, challenge_id, content="test solution", state="hidden", **kwargs):
|
||||
"""Helper function to generate a solution"""
|
||||
solution = Solutions(
|
||||
challenge_id=challenge_id, content=content, state=state, **kwargs
|
||||
)
|
||||
db.session.add(solution)
|
||||
db.session.commit()
|
||||
return solution
|
||||
|
||||
|
||||
def gen_field(
|
||||
db,
|
||||
name="CustomField",
|
||||
type="user",
|
||||
field_type="text",
|
||||
description="CustomFieldDescription",
|
||||
required=True,
|
||||
public=True,
|
||||
editable=True,
|
||||
):
|
||||
field = Fields(
|
||||
name=name,
|
||||
type=type,
|
||||
field_type=field_type,
|
||||
description=description,
|
||||
required=required,
|
||||
public=public,
|
||||
editable=editable,
|
||||
)
|
||||
db.session.add(field)
|
||||
db.session.commit()
|
||||
return field
|
||||
|
||||
|
||||
def gen_bracket(
|
||||
db,
|
||||
name="players",
|
||||
description="players who are part of the test",
|
||||
type="users",
|
||||
):
|
||||
bracket = Brackets(
|
||||
name=name,
|
||||
description=description,
|
||||
type=type,
|
||||
)
|
||||
db.session.add(bracket)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def simulate_user_activity(db, user):
|
||||
gen_tracking(db, user_id=user.id)
|
||||
gen_award(db, user_id=user.id)
|
||||
challenge = gen_challenge(db)
|
||||
flag = gen_flag(db, challenge_id=challenge.id)
|
||||
hint = gen_hint(db, challenge_id=challenge.id)
|
||||
|
||||
for _ in range(5):
|
||||
gen_fail(db, user_id=user.id, challenge_id=challenge.id)
|
||||
|
||||
gen_unlock(db, user_id=user.id, target=hint.id, type="hints")
|
||||
gen_solve(db, user_id=user.id, challenge_id=challenge.id, provided=flag.content)
|
||||
23
tests/models/test_model_utils.py
Normal file
23
tests/models/test_model_utils.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from CTFd.models import (
|
||||
Challenges,
|
||||
Comments,
|
||||
Files,
|
||||
Solves,
|
||||
Submissions,
|
||||
get_class_by_tablename,
|
||||
)
|
||||
from tests.helpers import create_ctfd, destroy_ctfd
|
||||
|
||||
|
||||
def test_get_class_by_tablename():
|
||||
"""
|
||||
Test that get_class_by_tablename() returns the correct table
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
assert get_class_by_tablename("solves") == Solves
|
||||
assert get_class_by_tablename("comments") == Comments
|
||||
assert get_class_by_tablename("files") == Files
|
||||
assert get_class_by_tablename("submissions") == Submissions
|
||||
assert get_class_by_tablename("challenges") == Challenges
|
||||
destroy_ctfd(app)
|
||||
0
tests/oauth/__init__.py
Normal file
0
tests/oauth/__init__.py
Normal file
113
tests/oauth/test_redirect.py
Normal file
113
tests/oauth/test_redirect.py
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Teams, Users
|
||||
from CTFd.utils import set_config
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
login_as_user,
|
||||
login_with_mlc,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_oauth_not_configured():
|
||||
"""Test that OAuth redirection fails if OAuth settings aren't configured"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/oauth", follow_redirects=False)
|
||||
assert r.location == "/login"
|
||||
r = client.get(r.location)
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "OAuth Settings not configured" in resp
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_oauth_configured_flow():
|
||||
"""Test that MLC integration works properly but does not allow registration (account creation) if disabled"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
app.config.update(
|
||||
{
|
||||
"OAUTH_CLIENT_ID": "ctfd_testing_client_id",
|
||||
"OAUTH_CLIENT_SECRET": "ctfd_testing_client_secret",
|
||||
"OAUTH_AUTHORIZATION_ENDPOINT": "http://auth.localhost/oauth/authorize",
|
||||
"OAUTH_TOKEN_ENDPOINT": "http://auth.localhost/oauth/token",
|
||||
"OAUTH_API_ENDPOINT": "http://api.localhost/user",
|
||||
}
|
||||
)
|
||||
with app.app_context():
|
||||
set_config("registration_visibility", "private")
|
||||
assert Users.query.count() == 1
|
||||
assert Teams.query.count() == 0
|
||||
|
||||
client = login_with_mlc(app, raise_for_error=False)
|
||||
|
||||
assert Users.query.count() == 1
|
||||
|
||||
# Users shouldn't be able to register because registration is disabled
|
||||
resp = client.get("http://localhost/login").get_data(as_text=True)
|
||||
assert "Public registration is disabled" in resp
|
||||
|
||||
set_config("registration_visibility", "public")
|
||||
client = login_with_mlc(app)
|
||||
|
||||
# Users should be able to register now
|
||||
assert Users.query.count() == 2
|
||||
user = Users.query.filter_by(email="user@examplectf.com").first()
|
||||
assert user.oauth_id == 1337
|
||||
assert user.team_id == 1
|
||||
|
||||
# Teams should be created
|
||||
assert Teams.query.count() == 1
|
||||
team = Teams.query.filter_by(id=1).first()
|
||||
assert team.oauth_id == 1234
|
||||
|
||||
client.get("/logout")
|
||||
|
||||
# Users should still be able to login if registration is disabled
|
||||
set_config("registration_visibility", "private")
|
||||
client = login_with_mlc(app)
|
||||
with client.session_transaction() as sess:
|
||||
assert sess["id"]
|
||||
assert sess["nonce"]
|
||||
assert sess["hash"]
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_oauth_login_upgrade():
|
||||
"""Test that users who use MLC after having registered will be associated with their MLC account"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
app.config.update(
|
||||
{
|
||||
"OAUTH_CLIENT_ID": "ctfd_testing_client_id",
|
||||
"OAUTH_CLIENT_SECRET": "ctfd_testing_client_secret",
|
||||
"OAUTH_AUTHORIZATION_ENDPOINT": "http://auth.localhost/oauth/authorize",
|
||||
"OAUTH_TOKEN_ENDPOINT": "http://auth.localhost/oauth/token",
|
||||
"OAUTH_API_ENDPOINT": "http://api.localhost/user",
|
||||
}
|
||||
)
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
assert Users.query.count() == 2
|
||||
set_config("registration_visibility", "private")
|
||||
|
||||
# Users should still be able to login
|
||||
client = login_as_user(app)
|
||||
client.get("/logout")
|
||||
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.oauth_id is None
|
||||
assert user.team_id is None
|
||||
|
||||
login_with_mlc(app)
|
||||
|
||||
assert Users.query.count() == 2
|
||||
|
||||
# Logging in with MLC should insert an OAuth ID and team ID
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.oauth_id
|
||||
assert user.verified
|
||||
assert user.team_id
|
||||
destroy_ctfd(app)
|
||||
71
tests/oauth/test_teams.py
Normal file
71
tests/oauth/test_teams.py
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from CTFd.models import Teams
|
||||
from CTFd.utils import set_config
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, gen_team, login_with_mlc
|
||||
|
||||
|
||||
def test_team_size_limit():
|
||||
"""Only team_size amount of members can join a team even via MLC"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
app.config.update(
|
||||
{
|
||||
"OAUTH_CLIENT_ID": "ctfd_testing_client_id",
|
||||
"OAUTH_CLIENT_SECRET": "ctfd_testing_client_secret",
|
||||
"OAUTH_AUTHORIZATION_ENDPOINT": "http://auth.localhost/oauth/authorize",
|
||||
"OAUTH_TOKEN_ENDPOINT": "http://auth.localhost/oauth/token",
|
||||
"OAUTH_API_ENDPOINT": "http://api.localhost/user",
|
||||
}
|
||||
)
|
||||
with app.app_context():
|
||||
set_config("team_size", 1)
|
||||
team = gen_team(app.db, member_count=1, oauth_id=1234)
|
||||
team_id = team.id
|
||||
login_with_mlc(
|
||||
app, team_name="team_name", team_oauth_id=1234, raise_for_error=False
|
||||
)
|
||||
assert len(Teams.query.filter_by(id=team_id).first().members) == 1
|
||||
|
||||
set_config("team_size", 2)
|
||||
login_with_mlc(app, team_name="team_name", team_oauth_id=1234)
|
||||
assert len(Teams.query.filter_by(id=team_id).first().members) == 2
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_num_teams_limit():
|
||||
"""Only num_teams teams can be created even via MLC"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
app.config.update(
|
||||
{
|
||||
"OAUTH_CLIENT_ID": "ctfd_testing_client_id",
|
||||
"OAUTH_CLIENT_SECRET": "ctfd_testing_client_secret",
|
||||
"OAUTH_AUTHORIZATION_ENDPOINT": "http://auth.localhost/oauth/authorize",
|
||||
"OAUTH_TOKEN_ENDPOINT": "http://auth.localhost/oauth/token",
|
||||
"OAUTH_API_ENDPOINT": "http://api.localhost/user",
|
||||
}
|
||||
)
|
||||
with app.app_context():
|
||||
set_config("num_teams", 1)
|
||||
gen_team(app.db, member_count=1, oauth_id=1234)
|
||||
login_with_mlc(
|
||||
app,
|
||||
name="foobar",
|
||||
email="foobar@a.com",
|
||||
oauth_id=111,
|
||||
team_name="foobar",
|
||||
team_oauth_id=1111,
|
||||
raise_for_error=False,
|
||||
)
|
||||
assert Teams.query.count() == 1
|
||||
|
||||
set_config("num_teams", 2)
|
||||
login_with_mlc(
|
||||
app,
|
||||
name="foobarbaz",
|
||||
email="foobarbaz@a.com",
|
||||
oauth_id=222,
|
||||
team_name="foobarbaz",
|
||||
team_oauth_id=2222,
|
||||
)
|
||||
assert Teams.query.count() == 2
|
||||
destroy_ctfd(app)
|
||||
48
tests/oauth/test_users.py
Normal file
48
tests/oauth/test_users.py
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from CTFd.models import Users
|
||||
from CTFd.utils import set_config
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, login_with_mlc, register_user
|
||||
|
||||
|
||||
def test_num_users_oauth_limit():
|
||||
"""Only num_users users can be created even via MLC"""
|
||||
app = create_ctfd()
|
||||
app.config.update(
|
||||
{
|
||||
"OAUTH_CLIENT_ID": "ctfd_testing_client_id",
|
||||
"OAUTH_CLIENT_SECRET": "ctfd_testing_client_secret",
|
||||
"OAUTH_AUTHORIZATION_ENDPOINT": "http://auth.localhost/oauth/authorize",
|
||||
"OAUTH_TOKEN_ENDPOINT": "http://auth.localhost/oauth/token",
|
||||
"OAUTH_API_ENDPOINT": "http://api.localhost/user",
|
||||
}
|
||||
)
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
# There should be the admin and our registered user
|
||||
assert Users.query.count() == 2
|
||||
set_config("num_users", 1)
|
||||
|
||||
# This registration should fail and we should still have 2 users
|
||||
login_with_mlc(
|
||||
app,
|
||||
name="foobarbaz",
|
||||
email="foobarbaz@a.com",
|
||||
oauth_id=111,
|
||||
scope="profile",
|
||||
raise_for_error=False,
|
||||
)
|
||||
assert Users.query.count() == 2
|
||||
|
||||
# We increment num_users to 2 and then login again
|
||||
set_config("num_users", 2)
|
||||
login_with_mlc(
|
||||
app,
|
||||
name="foobarbaz",
|
||||
email="foobarbaz@a.com",
|
||||
oauth_id=111,
|
||||
scope="profile",
|
||||
)
|
||||
# The above login should have succeeded
|
||||
assert Users.query.count() == 3
|
||||
destroy_ctfd(app)
|
||||
0
tests/teams/__init__.py
Normal file
0
tests/teams/__init__.py
Normal file
245
tests/teams/test_auth.py
Normal file
245
tests/teams/test_auth.py
Normal file
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Teams, Users, db
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_team,
|
||||
gen_user,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_banned_team():
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
team = gen_team(app.db, banned=True)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
user.team_id = team.id
|
||||
db.session.commit()
|
||||
|
||||
client = login_as_user(app)
|
||||
|
||||
routes = ["/", "/challenges", "/api/v1/challenges"]
|
||||
for route in routes:
|
||||
r = client.get(route)
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_teams_join_get():
|
||||
"""Can a user get /teams/join"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/teams/join")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_teams_join_post():
|
||||
"""Can a user post /teams/join"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
gen_user(app.db, name="user")
|
||||
gen_team(app.db, name="team")
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/teams/join")
|
||||
assert r.status_code == 200
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "team",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/teams/join", data=data)
|
||||
assert r.status_code == 302
|
||||
|
||||
# Cannot join a team with an incorrect password
|
||||
incorrect_data = data
|
||||
incorrect_data["password"] = ""
|
||||
r = client.post("/teams/join", data=incorrect_data)
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_teams_join_when_already_on_team():
|
||||
"""Test that a user cannot join another team"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
gen_user(app.db, name="user")
|
||||
gen_team(app.db, email="team1@examplectf.com", name="team1")
|
||||
gen_team(app.db, email="team2@examplectf.com", name="team2")
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/teams/join")
|
||||
assert r.status_code == 200
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "team1",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/teams/join", data=data)
|
||||
assert r.status_code == 302
|
||||
|
||||
# Try to join another team while on a team
|
||||
r = client.get("/teams/join")
|
||||
assert r.status_code == 200
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "team2",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/teams/join", data=data)
|
||||
assert r.status_code == 403
|
||||
user = Users.query.filter_by(name="user").first()
|
||||
assert user.team.name == "team1"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_team_login():
|
||||
"""Can a user login as a team"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user = gen_user(app.db, name="user")
|
||||
team = gen_team(app.db)
|
||||
user.team_id = team.id
|
||||
team.members.append(user)
|
||||
app.db.session.commit()
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/team")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_team_join_ratelimited():
|
||||
"""Test that team joins are ratelimited"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
gen_user(app.db, name="user")
|
||||
gen_team(app.db, name="team")
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/teams/join")
|
||||
assert r.status_code == 200
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "team",
|
||||
"password": "wrong_password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
for _ in range(10):
|
||||
r = client.post("/teams/join", data=data)
|
||||
|
||||
data["password"] = "password"
|
||||
for _ in range(10):
|
||||
r = client.post("/teams/join", data=data)
|
||||
assert r.status_code == 429
|
||||
assert Users.query.filter_by(id=2).first().team_id is None
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_teams_new_get():
|
||||
"""Can a user get /teams/new"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/teams/new")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_teams_new_post():
|
||||
"""Can a user post /teams/new"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
gen_user(app.db, name="user")
|
||||
with login_as_user(app) as client:
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "team",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/teams/new", data=data)
|
||||
assert r.status_code == 302
|
||||
|
||||
# You can't create a team with a duplicate name
|
||||
r = client.post("/teams/new", data=data)
|
||||
assert r.status_code == 403
|
||||
|
||||
# You can't create a team with an empty name
|
||||
incorrect_data = data
|
||||
incorrect_data["name"] = ""
|
||||
r = client.post("/teams/new", data=incorrect_data)
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_teams_new_post_when_already_on_team():
|
||||
"""Test that a user cannot create a new team while on a team"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
gen_user(app.db, name="user")
|
||||
with login_as_user(app) as client:
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "team1",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/teams/new", data=data)
|
||||
assert r.status_code == 302
|
||||
|
||||
# Try to create another team while on a team
|
||||
r = client.get("/teams/new")
|
||||
assert r.status_code == 200
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "team2",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/teams/join", data=data)
|
||||
assert r.status_code == 403
|
||||
user = Users.query.filter_by(name="user").first()
|
||||
assert user.team.name == "team1"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_teams_from_admin_hidden():
|
||||
"""Test that teams created by admins in /teams/new are hidden by default"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
gen_user(app.db, name="user")
|
||||
with login_as_user(app) as client:
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "team_user",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/teams/new", data=data)
|
||||
assert r.status_code == 302
|
||||
|
||||
team = Teams.query.filter_by(name="team_user").first()
|
||||
assert team.hidden == False
|
||||
|
||||
with login_as_user(app, "admin") as client:
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "team_admin",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/teams/new", data=data)
|
||||
assert r.status_code == 302
|
||||
|
||||
team = Teams.query.filter_by(name="team_admin").first()
|
||||
assert team.hidden == True
|
||||
destroy_ctfd(app)
|
||||
65
tests/teams/test_challenges.py
Normal file
65
tests/teams/test_challenges.py
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.utils import set_config
|
||||
from CTFd.utils.scores import get_standings
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_flag,
|
||||
gen_team,
|
||||
gen_user,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_challenge_team_submit():
|
||||
"""Is a user's solved challenge reflected by other team members"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user = gen_user(app.db)
|
||||
second_user = gen_user(app.db, name="user", email="second@examplectf.com")
|
||||
team = gen_team(app.db)
|
||||
user.team_id = team.id
|
||||
second_user.team_id = team.id
|
||||
team.members.append(user)
|
||||
team.members.append(second_user)
|
||||
gen_challenge(app.db)
|
||||
gen_flag(app.db, 1)
|
||||
app.db.session.commit()
|
||||
with login_as_user(app, name="user_name") as client:
|
||||
flag = {"challenge_id": 1, "submission": "flag"}
|
||||
client.post("/api/v1/challenges/attempt", json=flag)
|
||||
with login_as_user(app) as second_client:
|
||||
flag = {"challenge_id": 1, "submission": "flag"}
|
||||
r = second_client.post("/api/v1/challenges/attempt", json=flag)
|
||||
assert r.json["data"]["status"] == "already_solved"
|
||||
standings = get_standings()
|
||||
assert standings[0].name == "team_name"
|
||||
assert standings[0].score == 100
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_anonymous_users_view_public_challenges_without_team():
|
||||
"""Test that if challenges are public, users without team can still view them"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
gen_challenge(app.db)
|
||||
with app.test_client() as client:
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 302
|
||||
assert r.location.startswith("/login")
|
||||
|
||||
set_config("challenge_visibility", "public")
|
||||
with app.test_client() as client:
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 302
|
||||
assert r.location.startswith("/team")
|
||||
destroy_ctfd(app)
|
||||
396
tests/teams/test_fields.py
Normal file
396
tests/teams/test_fields.py
Normal file
@@ -0,0 +1,396 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import TeamFieldEntries, Teams, Users
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_field,
|
||||
gen_team,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_new_fields_show_on_pages():
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
team = gen_team(app.db)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
user.team_id = team.id
|
||||
team = Teams.query.filter_by(id=1).first()
|
||||
team.captain_id = 2
|
||||
app.db.session.commit()
|
||||
|
||||
gen_field(app.db, name="CustomField1", type="team")
|
||||
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/teams/new")
|
||||
assert "CustomField1" in r.get_data(as_text=True)
|
||||
assert "CustomFieldDescription" in r.get_data(as_text=True)
|
||||
|
||||
r = client.get("/team")
|
||||
assert "CustomField1" in r.get_data(as_text=True)
|
||||
assert "CustomFieldDescription" in r.get_data(as_text=True)
|
||||
|
||||
r = client.patch(
|
||||
"/api/v1/teams/me",
|
||||
json={"fields": [{"field_id": 1, "value": "CustomFieldEntry"}]},
|
||||
)
|
||||
resp = r.get_json()
|
||||
assert resp["success"] is True
|
||||
assert resp["data"]["fields"][0]["value"] == "CustomFieldEntry"
|
||||
assert resp["data"]["fields"][0]["description"] == "CustomFieldDescription"
|
||||
assert resp["data"]["fields"][0]["name"] == "CustomField1"
|
||||
assert resp["data"]["fields"][0]["field_id"] == 1
|
||||
|
||||
r = client.get("/team")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomFieldEntry" in resp
|
||||
|
||||
r = client.get("/teams/1")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomFieldEntry" in resp
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_team_fields_required_on_creation():
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
gen_field(app.db, type="team")
|
||||
|
||||
with app.app_context():
|
||||
with login_as_user(app) as client:
|
||||
assert Teams.query.count() == 0
|
||||
r = client.get("/teams/new")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField" in resp
|
||||
assert "CustomFieldDescription" in resp
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "team",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/teams/new", data=data)
|
||||
assert "Please provide all required fields" in r.get_data(as_text=True)
|
||||
assert Teams.query.count() == 0
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "team",
|
||||
"password": "password",
|
||||
"fields[1]": "CustomFieldEntry",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/teams/new", data=data)
|
||||
assert r.status_code == 302
|
||||
assert Teams.query.count() == 1
|
||||
|
||||
entry = TeamFieldEntries.query.filter_by(id=1).first()
|
||||
assert entry.team_id == 1
|
||||
assert entry.value == "CustomFieldEntry"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_team_fields_properties():
|
||||
"""Test that custom fields for team can be set and editted"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
gen_field(
|
||||
app.db,
|
||||
name="CustomField1",
|
||||
type="team",
|
||||
required=True,
|
||||
public=True,
|
||||
editable=True,
|
||||
)
|
||||
gen_field(
|
||||
app.db,
|
||||
name="CustomField2",
|
||||
type="team",
|
||||
required=False,
|
||||
public=True,
|
||||
editable=True,
|
||||
)
|
||||
gen_field(
|
||||
app.db,
|
||||
name="CustomField3",
|
||||
type="team",
|
||||
required=False,
|
||||
public=False,
|
||||
editable=True,
|
||||
)
|
||||
gen_field(
|
||||
app.db,
|
||||
name="CustomField4",
|
||||
type="team",
|
||||
required=False,
|
||||
public=False,
|
||||
editable=False,
|
||||
)
|
||||
|
||||
register_user(app)
|
||||
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/teams/new")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" in resp
|
||||
assert "CustomField4" in resp
|
||||
|
||||
# Manually create team so that we can set the required profile field
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "team",
|
||||
"password": "password",
|
||||
"fields[1]": "custom_field_value",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/teams/new", data=data)
|
||||
assert r.status_code == 302
|
||||
|
||||
r = client.get("/team")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" in resp
|
||||
assert "CustomField4" not in resp
|
||||
|
||||
r = client.patch(
|
||||
"/api/v1/teams/me",
|
||||
json={
|
||||
"fields": [
|
||||
{"field_id": 1, "value": "CustomFieldEntry1"},
|
||||
{"field_id": 2, "value": "CustomFieldEntry2"},
|
||||
{"field_id": 3, "value": "CustomFieldEntry3"},
|
||||
{"field_id": 4, "value": "CustomFieldEntry4"},
|
||||
]
|
||||
},
|
||||
)
|
||||
resp = r.get_json()
|
||||
assert resp == {
|
||||
"success": False,
|
||||
"errors": {"fields": ["Field 'CustomField4' cannot be editted"]},
|
||||
}
|
||||
|
||||
r = client.patch(
|
||||
"/api/v1/teams/me",
|
||||
json={
|
||||
"fields": [
|
||||
{"field_id": 1, "value": "CustomFieldEntry1"},
|
||||
{"field_id": 2, "value": "CustomFieldEntry2"},
|
||||
{"field_id": 3, "value": "CustomFieldEntry3"},
|
||||
]
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
r = client.get("/team")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert (
|
||||
"CustomField3" in resp
|
||||
) # This is here because /team contains team settings
|
||||
assert "CustomField4" not in resp
|
||||
|
||||
r = client.get("/teams/1")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" not in resp
|
||||
assert "CustomField4" not in resp
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_teams_boolean_checkbox_field():
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
gen_field(
|
||||
app.db,
|
||||
name="CustomField1",
|
||||
type="team",
|
||||
field_type="boolean",
|
||||
required=False,
|
||||
)
|
||||
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/teams/new")
|
||||
resp = r.get_data(as_text=True)
|
||||
|
||||
# We should have rendered a checkbox input
|
||||
assert "checkbox" in resp
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "team",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
"fields[1]": "y",
|
||||
}
|
||||
client.post("/teams/new", data=data)
|
||||
assert Teams.query.count() == 1
|
||||
|
||||
assert TeamFieldEntries.query.count() == 1
|
||||
assert TeamFieldEntries.query.filter_by(id=1).first().value is True
|
||||
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/team")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "checkbox" in resp
|
||||
|
||||
r = client.patch(
|
||||
"/api/v1/teams/me", json={"fields": [{"field_id": 1, "value": False}]}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert TeamFieldEntries.query.count() == 1
|
||||
assert TeamFieldEntries.query.filter_by(id=1).first().value is False
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_team_needs_all_required_fields():
|
||||
"""Test that teams need to complete profiles before seeing challenges"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
# Create a user and team who haven't filled any of their fields
|
||||
register_user(app)
|
||||
team = gen_team(app.db)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
user.team_id = team.id
|
||||
team = Teams.query.filter_by(id=1).first()
|
||||
team.captain_id = 2
|
||||
app.db.session.commit()
|
||||
|
||||
gen_field(
|
||||
app.db,
|
||||
name="CustomField1",
|
||||
type="team",
|
||||
required=True,
|
||||
public=True,
|
||||
editable=True,
|
||||
)
|
||||
gen_field(
|
||||
app.db,
|
||||
name="CustomField2",
|
||||
type="team",
|
||||
required=False,
|
||||
public=True,
|
||||
editable=True,
|
||||
)
|
||||
gen_field(
|
||||
app.db,
|
||||
name="CustomField3",
|
||||
type="team",
|
||||
required=False,
|
||||
public=False,
|
||||
editable=True,
|
||||
)
|
||||
gen_field(
|
||||
app.db,
|
||||
name="CustomField4",
|
||||
type="team",
|
||||
required=False,
|
||||
public=False,
|
||||
editable=False,
|
||||
)
|
||||
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/teams/new")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" in resp
|
||||
assert "CustomField4" in resp
|
||||
|
||||
# We can't view challenges because we have an incomplete team profile
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 403
|
||||
|
||||
# When we go to our profile we should see all fields
|
||||
r = client.get("/team")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" in resp
|
||||
assert "CustomField4" in resp
|
||||
|
||||
# Set all non-required fields
|
||||
r = client.patch(
|
||||
"/api/v1/teams/me",
|
||||
json={
|
||||
"fields": [
|
||||
# {"field_id": 1, "value": "CustomFieldEntry1"},
|
||||
{"field_id": 2, "value": "CustomFieldEntry2"},
|
||||
{"field_id": 3, "value": "CustomFieldEntry3"},
|
||||
{"field_id": 4, "value": "CustomFieldEntry4"},
|
||||
]
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
# We can't view challenges because we have an incomplete team profile
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 403
|
||||
|
||||
# Set required fields
|
||||
r = client.patch(
|
||||
"/api/v1/teams/me",
|
||||
json={"fields": [{"field_id": 1, "value": "CustomFieldEntry1"}]},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
# We can view challenges now
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
|
||||
# Attempts to edit a non-edittable field to field after completing profile
|
||||
r = client.patch(
|
||||
"/api/v1/teams/me",
|
||||
json={"fields": [{"field_id": 4, "value": "CustomFieldEntry4"}]},
|
||||
)
|
||||
resp = r.get_json()
|
||||
assert resp == {
|
||||
"success": False,
|
||||
"errors": {"fields": ["Field 'CustomField4' cannot be editted"]},
|
||||
}
|
||||
|
||||
# I can edit edittable fields
|
||||
r = client.patch(
|
||||
"/api/v1/teams/me",
|
||||
json={
|
||||
"fields": [
|
||||
{"field_id": 1, "value": "CustomFieldEntry1"},
|
||||
{"field_id": 2, "value": "CustomFieldEntry2"},
|
||||
{"field_id": 3, "value": "CustomFieldEntry3"},
|
||||
]
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
# I should see the correct fields in the private team profile
|
||||
r = client.get("/team")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert (
|
||||
"CustomField3" in resp
|
||||
) # This is here because /team contains team settings
|
||||
assert "CustomField4" not in resp
|
||||
|
||||
# I should see the correct fields in the public team profile
|
||||
r = client.get("/teams/1")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" not in resp
|
||||
assert "CustomField4" not in resp
|
||||
destroy_ctfd(app)
|
||||
91
tests/teams/test_hidden_team_scores.py
Normal file
91
tests/teams/test_hidden_team_scores.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from CTFd.models import Teams
|
||||
from CTFd.utils.scores import get_standings, get_team_standings
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_flag,
|
||||
gen_team,
|
||||
gen_user,
|
||||
login_as_user,
|
||||
)
|
||||
|
||||
|
||||
def setup_app(app):
|
||||
user1 = gen_user(app.db, name="user1", email="user1@examplectf.com")
|
||||
team1 = gen_team(app.db, name="team1", email="team1@examplectf.com")
|
||||
user1.team_id = team1.id
|
||||
team1.members.append(user1)
|
||||
team1.hidden = True
|
||||
|
||||
user2 = gen_user(app.db, name="user2", email="user2@examplectf.com")
|
||||
team2 = gen_team(app.db, name="team2", email="team2@examplectf.com")
|
||||
user2.team_id = team2.id
|
||||
team2.members.append(user2)
|
||||
|
||||
gen_challenge(app.db)
|
||||
gen_flag(app.db, 1)
|
||||
|
||||
app.db.session.commit()
|
||||
|
||||
with login_as_user(app, name="user1") as client:
|
||||
flag = {"challenge_id": 1, "submission": "flag"}
|
||||
client.post("/api/v1/challenges/attempt", json=flag)
|
||||
|
||||
with login_as_user(app, name="user2") as client:
|
||||
flag = {"challenge_id": 1, "submission": "flag"}
|
||||
client.post("/api/v1/challenges/attempt", json=flag)
|
||||
|
||||
|
||||
def test_standings():
|
||||
app = create_ctfd(user_mode="teams")
|
||||
|
||||
with app.app_context():
|
||||
setup_app(app)
|
||||
|
||||
standings = get_standings()
|
||||
|
||||
assert standings[0].name == "team2"
|
||||
assert standings[0].score == 100
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_team_standings():
|
||||
app = create_ctfd(user_mode="teams")
|
||||
|
||||
with app.app_context():
|
||||
setup_app(app)
|
||||
|
||||
team_standings = get_team_standings()
|
||||
|
||||
first_team = Teams.query.filter_by(id=team_standings[0].team_id).first_or_404()
|
||||
|
||||
assert first_team.name == "team2"
|
||||
assert first_team.score == 100
|
||||
|
||||
|
||||
def test_admin_standings():
|
||||
app = create_ctfd(user_mode="teams")
|
||||
|
||||
with app.app_context():
|
||||
setup_app(app)
|
||||
|
||||
standings = get_standings(admin=True)
|
||||
|
||||
assert standings[0].name == "team1"
|
||||
assert standings[0].score == 100
|
||||
|
||||
|
||||
def test_admin_team_standings():
|
||||
app = create_ctfd(user_mode="teams")
|
||||
|
||||
with app.app_context():
|
||||
setup_app(app)
|
||||
|
||||
team_standings = get_team_standings(admin=True)
|
||||
|
||||
first_team = Teams.query.filter_by(id=team_standings[0].team_id).first_or_404()
|
||||
|
||||
assert first_team.name == "team1"
|
||||
assert first_team.score == 100
|
||||
138
tests/teams/test_hints.py
Normal file
138
tests/teams/test_hints.py
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.utils.scores import get_standings
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_award,
|
||||
gen_challenge,
|
||||
gen_hint,
|
||||
gen_team,
|
||||
gen_user,
|
||||
login_as_user,
|
||||
)
|
||||
|
||||
|
||||
def test_hint_team_unlock():
|
||||
"""Is a user's unlocked hint reflected on other team members"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user = gen_user(app.db)
|
||||
second_user = gen_user(app.db, name="user", email="second@examplectf.com")
|
||||
team = gen_team(app.db)
|
||||
user.team_id = team.id
|
||||
second_user.team_id = team.id
|
||||
team.members.append(user)
|
||||
team.members.append(second_user)
|
||||
chal = gen_challenge(app.db)
|
||||
gen_hint(app.db, chal.id, content="hint", cost=1, type="standard")
|
||||
# Give the points to the user that doesn't unlock
|
||||
# Users that unlock hints should be able to unlock but cost their team points
|
||||
gen_award(app.db, user_id=3, team_id=team.id)
|
||||
app.db.session.commit()
|
||||
with login_as_user(app, name="user_name") as client:
|
||||
# Assert that we don't see a hint
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.get_json()["data"].get("content") is None
|
||||
|
||||
# Unlock the hint
|
||||
client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
|
||||
# Assert that we see a hint
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.get_json()["data"].get("content")
|
||||
with login_as_user(app) as second_client:
|
||||
# Assert that we see a hint
|
||||
r = second_client.get("/api/v1/hints/1")
|
||||
assert r.get_json()["data"].get("content")
|
||||
|
||||
# Assert that we can't double unlock
|
||||
r = second_client.post(
|
||||
"/api/v1/unlocks", json={"target": 1, "type": "hints"}
|
||||
)
|
||||
assert r.status_code == 400
|
||||
assert (
|
||||
r.get_json()["errors"]["target"]
|
||||
== "You've already unlocked this target"
|
||||
)
|
||||
|
||||
# Assert that we see a hint
|
||||
r = second_client.get("/api/v1/hints/1")
|
||||
assert r.json["data"]["content"] == "hint"
|
||||
|
||||
# Verify standings
|
||||
# We start with 100 points from the award.
|
||||
# We lose a point because we unlock successfully once
|
||||
standings = get_standings()
|
||||
assert standings[0].name == "team_name"
|
||||
assert standings[0].score == 99
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_hint_team_unlocking_without_points():
|
||||
"""Test that teams cannot enter negative point valuations from unlocking hints"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user = gen_user(app.db)
|
||||
second_user = gen_user(app.db, name="user", email="second@examplectf.com")
|
||||
team = gen_team(app.db)
|
||||
user.team_id = team.id
|
||||
second_user.team_id = team.id
|
||||
team.members.append(user)
|
||||
team.members.append(second_user)
|
||||
chal = gen_challenge(app.db)
|
||||
gen_hint(app.db, chal.id, content="hint", cost=1, type="standard")
|
||||
app.db.session.commit()
|
||||
with login_as_user(app, name="user_name") as client:
|
||||
# Assert that we don't see a hint
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.get_json()["data"].get("content") is None
|
||||
|
||||
# Attempt to unlock the hint
|
||||
r = client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
assert r.status_code == 400
|
||||
assert (
|
||||
r.get_json()["errors"]["score"]
|
||||
== "You do not have enough points to unlock this hint"
|
||||
)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_teams_dont_prevent_other_teams_from_unlocking_hints():
|
||||
"""Unlocks from one user don't affect other users"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
chal = gen_challenge(app.db)
|
||||
gen_hint(app.db, chal.id, content="This is a hint", cost=1, type="standard")
|
||||
|
||||
team1 = gen_team(app.db, name="team1", email="team1@examplectf.com")
|
||||
team2 = gen_team(app.db, name="team2", email="team2@examplectf.com")
|
||||
|
||||
# Give users points with an award
|
||||
gen_award(app.db, user_id=team1.captain_id)
|
||||
gen_award(app.db, user_id=team2.captain_id)
|
||||
|
||||
captain1 = team1.captain.name
|
||||
captain2 = team2.captain.name
|
||||
|
||||
app.db.session.commit()
|
||||
|
||||
# First team unlocks hint
|
||||
with login_as_user(app, name=captain1) as client:
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 200
|
||||
r = client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
assert r.status_code == 200
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 200
|
||||
|
||||
# Second team unlocks hint
|
||||
with login_as_user(app, name=captain2) as client:
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 200
|
||||
r = client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
assert r.status_code == 200
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
81
tests/teams/test_invites.py
Normal file
81
tests/teams/test_invites.py
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from freezegun import freeze_time
|
||||
|
||||
from CTFd.exceptions import TeamTokenExpiredException, TeamTokenInvalidException
|
||||
from CTFd.models import Teams, Users
|
||||
from CTFd.utils import set_config
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, gen_team, gen_user, login_as_user
|
||||
|
||||
|
||||
def test_team_invite_codes():
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
team1 = gen_team(app.db, name="team1", email="team1@examplectf.com")
|
||||
with freeze_time("2017-10-7 00:00:00"):
|
||||
invite_code = team1.get_invite_code()
|
||||
team = Teams.load_invite_code(invite_code)
|
||||
assert team.id == team1.id
|
||||
|
||||
with freeze_time("2017-10-8 00:00:01"):
|
||||
try:
|
||||
team = Teams.load_invite_code(invite_code)
|
||||
except TeamTokenExpiredException:
|
||||
# This token should be expired and we shouldn't get a team object back
|
||||
pass
|
||||
else:
|
||||
print("Token should have expired")
|
||||
raise Exception
|
||||
|
||||
# Change team's password
|
||||
team.password = "new_test_password"
|
||||
app.db.session.commit()
|
||||
|
||||
with freeze_time("2017-10-7 00:00:00"):
|
||||
try:
|
||||
team = Teams.load_invite_code(invite_code)
|
||||
except TeamTokenInvalidException:
|
||||
pass
|
||||
else:
|
||||
print("Token should have been invalidated by password change")
|
||||
raise Exception
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_api_user_facing_invite_tokens():
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
team1 = gen_team(app.db, name="team1", email="team1@examplectf.com")
|
||||
user = team1.captain
|
||||
with login_as_user(app, name=user.name) as captain:
|
||||
r = captain.post("/api/v1/teams/me/members", json="")
|
||||
invite_code = r.get_json()["data"]["code"]
|
||||
assert invite_code
|
||||
|
||||
new_user = gen_user(app.db)
|
||||
with login_as_user(app, name=new_user.name) as user:
|
||||
url = f"/teams/invite?code={invite_code}"
|
||||
user.get(url)
|
||||
with user.session_transaction() as sess:
|
||||
data = {
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = user.post(url, data=data)
|
||||
assert r.status_code == 302
|
||||
assert r.location.endswith("/challenges")
|
||||
assert Users.query.filter_by(id=new_user.id).first().team_id == team1.id
|
||||
|
||||
# Test team size limits
|
||||
set_config("team_size", 1)
|
||||
new_user2 = gen_user(app.db, name="new_user2", email="new_user2@examplectf.com")
|
||||
with login_as_user(app, name=new_user2.name) as user:
|
||||
url = f"/teams/invite?code={invite_code}"
|
||||
user.get(url)
|
||||
with user.session_transaction() as sess:
|
||||
data = {
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = user.post(url, data=data)
|
||||
assert r.status_code == 403
|
||||
assert "has already reached the team size limit" in r.get_data(as_text=True)
|
||||
destroy_ctfd(app)
|
||||
33
tests/teams/test_scoreboard.py
Normal file
33
tests/teams/test_scoreboard.py
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.utils.scores import get_standings
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_flag,
|
||||
gen_team,
|
||||
gen_user,
|
||||
login_as_user,
|
||||
)
|
||||
|
||||
|
||||
def test_scoreboard_team_score():
|
||||
"""Is a user's submitted flag reflected on the team's score on /scoreboard"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user = gen_user(app.db, name="user")
|
||||
team = gen_team(app.db)
|
||||
user.team_id = team.id
|
||||
team.members.append(user)
|
||||
gen_challenge(app.db)
|
||||
gen_flag(app.db, 1)
|
||||
app.db.session.commit()
|
||||
with login_as_user(app) as client:
|
||||
flag = {"challenge_id": 1, "submission": "flag"}
|
||||
client.post("/api/v1/challenges/attempt", json=flag)
|
||||
standings = get_standings()
|
||||
assert standings[0].name == "team_name"
|
||||
assert standings[0].score == 100
|
||||
destroy_ctfd(app)
|
||||
251
tests/teams/test_teams.py
Normal file
251
tests/teams/test_teams.py
Normal file
@@ -0,0 +1,251 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Teams, Users
|
||||
from CTFd.utils import set_config
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_award,
|
||||
gen_team,
|
||||
gen_user,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_teams_get():
|
||||
"""Can a user get /teams"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
set_config("account_visibility", "public")
|
||||
r = client.get("/teams")
|
||||
assert r.status_code == 200
|
||||
set_config("account_visibility", "private")
|
||||
r = client.get("/teams")
|
||||
assert r.status_code == 302
|
||||
set_config("account_visibility", "admins")
|
||||
r = client.get("/teams")
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_accessing_hidden_teams():
|
||||
"""Hidden teams should not give any data from /teams or /api/v1/teams"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
register_user(app, name="visible_user", email="visible_user@examplectf.com")
|
||||
with login_as_user(app, name="visible_user") as client:
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
team = gen_team(app.db, name="visible_team", hidden=True)
|
||||
team.members.append(user)
|
||||
user.team_id = team.id
|
||||
app.db.session.commit()
|
||||
|
||||
assert client.get("/teams/1").status_code == 404
|
||||
assert client.get("/api/v1/teams/1").status_code == 404
|
||||
assert client.get("/api/v1/teams/1/solves").status_code == 404
|
||||
assert client.get("/api/v1/teams/1/fails").status_code == 404
|
||||
assert client.get("/api/v1/teams/1/awards").status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_hidden_teams_visibility():
|
||||
"""Hidden teams should not show up on /teams or /api/v1/teams or /api/v1/scoreboard"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
user_id = user.id
|
||||
team = gen_team(app.db, name="visible_team", hidden=True)
|
||||
team_id = team.id
|
||||
team_name = team.name
|
||||
team.members.append(user)
|
||||
user.team_id = team.id
|
||||
app.db.session.commit()
|
||||
|
||||
r = client.get("/teams")
|
||||
response = r.get_data(as_text=True)
|
||||
# Only search in body content
|
||||
body_start = response.find("<body>")
|
||||
body_end = response.find("</body>")
|
||||
response = response[body_start:body_end]
|
||||
assert team_name not in response
|
||||
|
||||
r = client.get("/api/v1/teams")
|
||||
response = r.get_json()
|
||||
assert team_name not in response
|
||||
|
||||
gen_award(app.db, user_id, team_id=team_id)
|
||||
|
||||
r = client.get("/scoreboard")
|
||||
response = r.get_data(as_text=True)
|
||||
# Only search in body content
|
||||
body_start = response.find("<body>")
|
||||
body_end = response.find("</body>")
|
||||
response = response[body_start:body_end]
|
||||
assert team_name not in response
|
||||
|
||||
r = client.get("/api/v1/scoreboard")
|
||||
response = r.get_json()
|
||||
assert team_name not in response
|
||||
|
||||
# Team should re-appear after disabling hiding
|
||||
# Use an API call to cause a cache clear
|
||||
with login_as_user(app, name="admin") as admin:
|
||||
r = admin.patch("/api/v1/teams/1", json={"hidden": False})
|
||||
assert r.status_code == 200
|
||||
|
||||
r = client.get("/teams")
|
||||
response = r.get_data(as_text=True)
|
||||
assert team_name in response
|
||||
|
||||
r = client.get("/api/v1/teams")
|
||||
response = r.get_data(as_text=True)
|
||||
assert team_name in response
|
||||
|
||||
r = client.get("/api/v1/scoreboard")
|
||||
response = r.get_data(as_text=True)
|
||||
assert team_name in response
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_teams_get_user_mode():
|
||||
"""Can a user get /teams if user mode"""
|
||||
app = create_ctfd(user_mode="users")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/teams")
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_team_get():
|
||||
"""Can a user get /team"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user = gen_user(app.db)
|
||||
team = gen_team(app.db)
|
||||
team.members.append(user)
|
||||
user.team_id = team.id
|
||||
app.db.session.commit()
|
||||
with login_as_user(app, name="user_name", password="password") as client:
|
||||
r = client.get("/team")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_teams_id_get():
|
||||
"""Can a user get /teams/<int:team_id>"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
user = gen_user(app.db)
|
||||
team = gen_team(app.db)
|
||||
team.members.append(user)
|
||||
user.team_id = team.id
|
||||
app.db.session.commit()
|
||||
with login_as_user(app, name="user_name", password="password") as client:
|
||||
r = client.get("/teams/1")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_team_size_limit():
|
||||
"""Only team_size amount of members can join a team"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
set_config("team_size", 1)
|
||||
|
||||
# Create a team with only one member
|
||||
team = gen_team(app.db, member_count=1)
|
||||
team_id = team.id
|
||||
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/teams/join")
|
||||
assert r.status_code == 200
|
||||
|
||||
# User should be blocked from joining
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "team_name",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/teams/join", data=data)
|
||||
resp = r.get_data(as_text=True)
|
||||
assert len(Teams.query.filter_by(id=team_id).first().members) == 1
|
||||
assert "already reached the team size limit of 1" in resp
|
||||
|
||||
# Can the user join after the size has been bumped
|
||||
set_config("team_size", 2)
|
||||
r = client.post("/teams/join", data=data)
|
||||
resp = r.get_data(as_text=True)
|
||||
assert len(Teams.query.filter_by(id=team_id).first().members) == 2
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_num_teams_limit():
|
||||
"""Only num_teams teams can be created"""
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
set_config("num_teams", 1)
|
||||
|
||||
# Create a team
|
||||
gen_team(app.db, member_count=1)
|
||||
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/teams/new")
|
||||
assert r.status_code == 403
|
||||
|
||||
# team should be blocked from creation
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "team1",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/teams/new", data=data)
|
||||
resp = r.get_data(as_text=True)
|
||||
assert Teams.query.count() == 1
|
||||
assert "Reached the maximum number of teams" in resp
|
||||
|
||||
# Can the team be created after the num has been bumped
|
||||
set_config("num_teams", 2)
|
||||
r = client.post("/teams/new", data=data)
|
||||
resp = r.get_data(as_text=True)
|
||||
assert Teams.query.count() == 2
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_team_creation_disable():
|
||||
app = create_ctfd(user_mode="teams")
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
# Team creation page should be available
|
||||
r = client.get("/teams/new")
|
||||
assert r.status_code == 200
|
||||
|
||||
# Disable team creation in config
|
||||
set_config("team_creation", False)
|
||||
|
||||
# Can't access the public team creation page
|
||||
r = client.get("/teams/new")
|
||||
assert r.status_code == 403
|
||||
|
||||
# User should be blocked from creating teams as well
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "team_name",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/teams/new", data=data)
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
450
tests/test_config.py
Normal file
450
tests/test_config.py
Normal file
@@ -0,0 +1,450 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from werkzeug.exceptions import SecurityError
|
||||
|
||||
from CTFd.config import TestingConfig
|
||||
from CTFd.models import Configs, Users, db
|
||||
from CTFd.utils import get_config
|
||||
from CTFd.utils.crypto import verify_password
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_user,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_reverse_proxy_config():
|
||||
"""Test that REVERSE_PROXY configuration behaves properly"""
|
||||
|
||||
class ReverseProxyConfig(TestingConfig):
|
||||
REVERSE_PROXY = "1,2,3,4"
|
||||
|
||||
app = create_ctfd(config=ReverseProxyConfig)
|
||||
with app.app_context():
|
||||
assert app.wsgi_app.x_for == 1
|
||||
assert app.wsgi_app.x_proto == 2
|
||||
assert app.wsgi_app.x_host == 3
|
||||
assert app.wsgi_app.x_port == 4
|
||||
assert app.wsgi_app.x_prefix == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
class ReverseProxyConfig(TestingConfig):
|
||||
REVERSE_PROXY = "true"
|
||||
|
||||
app = create_ctfd(config=ReverseProxyConfig)
|
||||
with app.app_context():
|
||||
assert app.wsgi_app.x_for == 1
|
||||
assert app.wsgi_app.x_proto == 1
|
||||
assert app.wsgi_app.x_host == 1
|
||||
assert app.wsgi_app.x_port == 1
|
||||
assert app.wsgi_app.x_prefix == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
class ReverseProxyConfig(TestingConfig):
|
||||
REVERSE_PROXY = True
|
||||
|
||||
app = create_ctfd(config=ReverseProxyConfig)
|
||||
with app.app_context():
|
||||
assert app.wsgi_app.x_for == 1
|
||||
assert app.wsgi_app.x_proto == 1
|
||||
assert app.wsgi_app.x_host == 1
|
||||
assert app.wsgi_app.x_port == 1
|
||||
assert app.wsgi_app.x_prefix == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_server_sent_events_config():
|
||||
"""Test that SERVER_SENT_EVENTS configuration behaves properly"""
|
||||
|
||||
class ServerSentEventsConfig(TestingConfig):
|
||||
SERVER_SENT_EVENTS = False
|
||||
|
||||
app = create_ctfd(config=ServerSentEventsConfig)
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/events")
|
||||
assert r.status_code == 204
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_trusted_hosts_config():
|
||||
"""Test that TRUSTED_HOSTS configuration behaves properly"""
|
||||
|
||||
class TrustedHostsConfig(TestingConfig):
|
||||
SERVER_NAME = "example.com"
|
||||
TRUSTED_HOSTS = ["example.com"]
|
||||
|
||||
app = create_ctfd(config=TrustedHostsConfig)
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/", headers={"Host": "example.com"})
|
||||
assert r.status_code == 200
|
||||
|
||||
# TODO: We need to allow either a 500 or a 400 because Flask-RestX
|
||||
# seems to be overriding Flask's error handler
|
||||
try:
|
||||
r = client.get("/", headers={"Host": "evil.com"})
|
||||
except SecurityError:
|
||||
pass
|
||||
else:
|
||||
if r.status_code != 400:
|
||||
raise SecurityError("Responded to untrusted request")
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_preset_admin_config():
|
||||
"""Test that PRESET_ADMIN configuration allows login and creates admin user"""
|
||||
|
||||
class PresetAdminConfig(TestingConfig):
|
||||
PRESET_ADMIN_NAME = "preset_admin"
|
||||
PRESET_ADMIN_EMAIL = "preset@example.com"
|
||||
PRESET_ADMIN_PASSWORD = "preset_password_123"
|
||||
|
||||
app = create_ctfd(config=PresetAdminConfig)
|
||||
with app.app_context():
|
||||
# Verify no preset admin exists initially
|
||||
preset_admin = Users.query.filter_by(name="preset_admin").first()
|
||||
assert preset_admin is None
|
||||
|
||||
# Attempt login with incorrect preset admin credentials via name
|
||||
client = app.test_client()
|
||||
login_data = {"name": "preset_admin", "password": "wrong_preset_password_123"}
|
||||
# Get login page first to get nonce
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
login_data["nonce"] = sess.get("nonce")
|
||||
r = client.post("/login", data=login_data)
|
||||
assert r.status_code == 200
|
||||
assert "incorrect" in r.get_data(as_text=True)
|
||||
preset_admin = Users.query.filter_by(name="preset_admin").first()
|
||||
assert preset_admin is None
|
||||
|
||||
# Attempt login with preset admin credentials via name
|
||||
client = app.test_client()
|
||||
login_data = {"name": "preset_admin", "password": "preset_password_123"}
|
||||
# Get login page first to get nonce
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
login_data["nonce"] = sess.get("nonce")
|
||||
r = client.post("/login", data=login_data)
|
||||
|
||||
# Should redirect to challenges page after successful login
|
||||
assert r.status_code == 302
|
||||
assert "/challenges" in r.location or r.location.endswith("/")
|
||||
|
||||
# Verify admin user was created
|
||||
preset_admin = Users.query.filter_by(name="preset_admin").first()
|
||||
assert preset_admin is not None
|
||||
assert preset_admin.email == "preset@example.com"
|
||||
assert preset_admin.type == "admin"
|
||||
assert preset_admin.verified is True
|
||||
|
||||
assert verify_password("preset_password_123", preset_admin.password) is True
|
||||
|
||||
# Test login via email as well
|
||||
client = app.test_client()
|
||||
login_data = {"name": "preset@example.com", "password": "preset_password_123"}
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
login_data["nonce"] = sess.get("nonce")
|
||||
r = client.post("/login", data=login_data)
|
||||
assert r.status_code == 302
|
||||
|
||||
# Test that wrong password fails
|
||||
client = app.test_client()
|
||||
login_data = {"name": "preset_admin", "password": "wrong_password"}
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
login_data["nonce"] = sess.get("nonce")
|
||||
r = client.post("/login", data=login_data)
|
||||
# Should return login page with error, not redirect
|
||||
assert r.status_code == 200
|
||||
assert b"incorrect" in r.data.lower() or b"invalid" in r.data.lower()
|
||||
|
||||
assert Users.query.filter_by(type="admin").count() == 2
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_preset_admin_token_config():
|
||||
"""Test that PRESET_ADMIN_TOKEN allows API access and creates admin user"""
|
||||
|
||||
class PresetAdminTokenConfig(TestingConfig):
|
||||
PRESET_ADMIN_NAME = "preset_token_admin"
|
||||
PRESET_ADMIN_EMAIL = "preset_token@example.com"
|
||||
PRESET_ADMIN_PASSWORD = "preset_token_password_123"
|
||||
PRESET_ADMIN_TOKEN = "preset_secret_token_12345"
|
||||
|
||||
app = create_ctfd(config=PresetAdminTokenConfig)
|
||||
with app.app_context():
|
||||
# Verify no preset admin exists initially
|
||||
preset_admin = Users.query.filter_by(name="preset_token_admin").first()
|
||||
assert preset_admin is None
|
||||
|
||||
# Test that wrong token fails
|
||||
client = app.test_client()
|
||||
wrong_headers = {
|
||||
"Authorization": "Token wrong_token_123",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
r = client.get("/api/v1/users/me", headers=wrong_headers)
|
||||
assert r.status_code in [401, 403] # Should be unauthorized
|
||||
|
||||
# Test API access without authentication (should fail)
|
||||
client = app.test_client()
|
||||
r = client.get("/api/v1/users/me", json=True)
|
||||
assert r.status_code in [401, 403] # Unauthorized or Forbidden
|
||||
|
||||
# Test API access with preset admin token
|
||||
headers = {
|
||||
"Authorization": "Token preset_secret_token_12345",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
r = client.get("/api/v1/users/me", headers=headers, json=True)
|
||||
|
||||
# Should succeed and create the admin user
|
||||
assert r.status_code == 200
|
||||
|
||||
# Verify admin user was created
|
||||
preset_admin = Users.query.filter_by(name="preset_token_admin").first()
|
||||
assert preset_admin is not None
|
||||
assert preset_admin.email == "preset_token@example.com"
|
||||
assert preset_admin.type == "admin"
|
||||
assert preset_admin.verified is True
|
||||
|
||||
assert (
|
||||
verify_password("preset_token_password_123", preset_admin.password) is True
|
||||
)
|
||||
|
||||
# Verify the API response contains the admin user information
|
||||
response_data = r.get_json()
|
||||
|
||||
assert response_data["success"] is True
|
||||
assert response_data["data"]["name"] == "preset_token_admin"
|
||||
assert response_data["data"]["email"] == "preset_token@example.com"
|
||||
|
||||
# Check that we are admin
|
||||
r = client.get("/api/v1/challenges/types", headers=headers, json=True)
|
||||
assert r.status_code == 200
|
||||
|
||||
# Test that wrong token fails
|
||||
wrong_headers = {
|
||||
"Authorization": "Token wrong_token_123",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
r = client.get("/api/v1/users/me", headers=wrong_headers, json=True)
|
||||
assert r.status_code in [401, 403] # Should be unauthorized
|
||||
|
||||
assert Users.query.filter_by(type="admin").count() == 2
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_preset_admin_no_promotion_existing_user():
|
||||
"""Test that existing regular users with preset credentials don't get promoted to admin"""
|
||||
|
||||
class PresetAdminConfig(TestingConfig):
|
||||
PRESET_ADMIN_NAME = "existing_user"
|
||||
PRESET_ADMIN_EMAIL = "existing@example.com"
|
||||
PRESET_ADMIN_PASSWORD = "preset_password_123"
|
||||
|
||||
app = create_ctfd(config=PresetAdminConfig)
|
||||
with app.app_context():
|
||||
# Create a regular user with the same credentials as preset admin
|
||||
gen_user(
|
||||
app.db,
|
||||
name="existing_user",
|
||||
email="existing@example.com",
|
||||
password="preset_password_123",
|
||||
type="user", # Regular user, not admin
|
||||
)
|
||||
|
||||
# Verify user exists and is not admin
|
||||
user = Users.query.filter_by(name="existing_user").first()
|
||||
assert user is not None
|
||||
assert user.type == "user"
|
||||
assert user.email == "existing@example.com"
|
||||
|
||||
# Attempt login with preset admin credentials
|
||||
client = app.test_client()
|
||||
login_data = {"name": "existing_user", "password": "preset_password_123"}
|
||||
# Get login page first to get nonce
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
login_data["nonce"] = sess.get("nonce")
|
||||
r = client.post("/login", data=login_data)
|
||||
|
||||
# Should fail in logging in
|
||||
assert r.status_code == 200
|
||||
assert (
|
||||
"Preset admin user could not be created. Please contact an administrator"
|
||||
in r.get_data(as_text=True)
|
||||
)
|
||||
|
||||
# Test that the user is not an admin
|
||||
r = client.get("/api/v1/challenges/types", json=True)
|
||||
assert r.status_code in [
|
||||
401,
|
||||
403,
|
||||
] # Should be unauthorized/forbidden for regular user
|
||||
|
||||
# Verify user is still not admin (no promotion)
|
||||
user = Users.query.filter_by(name="existing_user").first()
|
||||
assert user is not None
|
||||
assert user.type == "user" # Should still be regular user
|
||||
assert user.email == "existing@example.com"
|
||||
|
||||
# Also test login via email
|
||||
client = app.test_client()
|
||||
login_data = {"name": "existing@example.com", "password": "preset_password_123"}
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
login_data["nonce"] = sess.get("nonce")
|
||||
r = client.post("/login", data=login_data)
|
||||
# Should fail in logging in
|
||||
assert r.status_code == 200
|
||||
assert (
|
||||
"Preset admin user could not be created. Please contact an administrator"
|
||||
in r.get_data(as_text=True)
|
||||
)
|
||||
|
||||
# Test that the user is not an admin
|
||||
r = client.get("/api/v1/challenges/types", json=True)
|
||||
assert r.status_code in [
|
||||
401,
|
||||
403,
|
||||
] # Should be unauthorized/forbidden for regular user
|
||||
|
||||
# User should still be regular user after email login
|
||||
user = Users.query.filter_by(name="existing_user").first()
|
||||
assert user.type == "user"
|
||||
|
||||
# Verify only one admin exists (the default setup admin)
|
||||
admin_count = Users.query.filter_by(type="admin").count()
|
||||
assert admin_count == 1
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_preset_admin_empty_credentials():
|
||||
"""Test that empty preset credentials don't allow login"""
|
||||
|
||||
# Test empty password
|
||||
class PresetAdminEmptyPasswordConfig(TestingConfig):
|
||||
PRESET_ADMIN_NAME = "preset_admin_empty"
|
||||
PRESET_ADMIN_EMAIL = "preset_empty@example.com"
|
||||
PRESET_ADMIN_PASSWORD = "" # Empty password
|
||||
|
||||
app = create_ctfd(config=PresetAdminEmptyPasswordConfig)
|
||||
with app.app_context():
|
||||
# Verify no preset admin exists initially
|
||||
preset_admin = Users.query.filter_by(name="preset_admin_empty").first()
|
||||
assert preset_admin is None
|
||||
|
||||
# Attempt login with empty password (should fail)
|
||||
client = app.test_client()
|
||||
login_data = {"name": "preset_admin_empty", "password": ""}
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
login_data["nonce"] = sess.get("nonce")
|
||||
r = client.post("/login", data=login_data)
|
||||
|
||||
# Should not create user or allow login
|
||||
assert r.status_code == 200
|
||||
assert "incorrect" in r.get_data(as_text=True).lower()
|
||||
|
||||
# Verify no admin user was created
|
||||
preset_admin = Users.query.filter_by(name="preset_admin_empty").first()
|
||||
assert preset_admin is None
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
# Test empty token
|
||||
class PresetAdminEmptyTokenConfig(TestingConfig):
|
||||
PRESET_ADMIN_NAME = "preset_admin_empty_token"
|
||||
PRESET_ADMIN_EMAIL = "preset_empty_token@example.com"
|
||||
PRESET_ADMIN_PASSWORD = "some_password"
|
||||
PRESET_ADMIN_TOKEN = "" # Empty token
|
||||
|
||||
app = create_ctfd(config=PresetAdminEmptyTokenConfig)
|
||||
with app.app_context():
|
||||
# Verify no preset admin exists initially
|
||||
preset_admin = Users.query.filter_by(name="preset_admin_empty_token").first()
|
||||
assert preset_admin is None
|
||||
|
||||
# Test API access with empty token (should fail)
|
||||
client = app.test_client()
|
||||
empty_headers = {
|
||||
"Authorization": "Token ",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
r = client.get("/api/v1/users/me", headers=empty_headers)
|
||||
assert r.status_code in [401, 403] # Should be unauthorized
|
||||
|
||||
# Test API access without Authorization header (should also fail)
|
||||
r = client.get("/api/v1/users/me", json=True)
|
||||
assert r.status_code in [401, 403] # Should be unauthorized
|
||||
|
||||
# Verify no admin user was created
|
||||
preset_admin = Users.query.filter_by(name="preset_admin_empty_token").first()
|
||||
assert preset_admin is None
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_preset_configs():
|
||||
"""Test that PRESET_CONFIGS are loaded and accessible via get_config"""
|
||||
|
||||
class PresetConfigsConfig(TestingConfig):
|
||||
PRESET_CONFIGS = {
|
||||
"ctf_name": "Test CTF Name",
|
||||
"ctf_description": "This is a test CTF description",
|
||||
"user_mode": "users",
|
||||
"challenge_visibility": "public",
|
||||
"registration_visibility": "public",
|
||||
"score_visibility": "public",
|
||||
"account_visibility": "public",
|
||||
"custom_setting": "custom_value_123",
|
||||
}
|
||||
|
||||
app = create_ctfd(config=PresetConfigsConfig)
|
||||
with app.app_context():
|
||||
# Test that preset configs are accessible via get_config
|
||||
assert get_config("ctf_name") == "Test CTF Name"
|
||||
assert get_config("ctf_description") == "This is a test CTF description"
|
||||
assert get_config("user_mode") == "users"
|
||||
assert get_config("challenge_visibility") == "public"
|
||||
assert get_config("registration_visibility") == "public"
|
||||
assert get_config("score_visibility") == "public"
|
||||
assert get_config("account_visibility") == "public"
|
||||
assert get_config("custom_setting") == "custom_value_123"
|
||||
|
||||
# Test that non-existent config returns None (or default)
|
||||
assert get_config("non_existent_config") is None
|
||||
assert (
|
||||
get_config("non_existent_config", default="default_value")
|
||||
== "default_value"
|
||||
)
|
||||
|
||||
# Add a database config that conflicts with a preset config
|
||||
db_config = Configs(key="ctf_name", value="Database CTF Name")
|
||||
db.session.add(db_config)
|
||||
db.session.commit()
|
||||
|
||||
# The preset config should still take precedence (not overridden by database)
|
||||
assert get_config("ctf_name") == "Test CTF Name"
|
||||
|
||||
# Test that database configs work for keys not in PRESET_CONFIGS
|
||||
db_config_new = Configs(key="database_only_setting", value="database_value")
|
||||
db.session.add(db_config_new)
|
||||
db.session.commit()
|
||||
|
||||
# This should come from the database since it's not in presets
|
||||
assert get_config("database_only_setting") == "database_value"
|
||||
|
||||
destroy_ctfd(app)
|
||||
27
tests/test_legal.py
Normal file
27
tests/test_legal.py
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.utils import set_config
|
||||
from tests.helpers import create_ctfd, destroy_ctfd
|
||||
|
||||
|
||||
def test_legal_settings():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("tos_text", "Terms of Service")
|
||||
set_config("privacy_text", "Privacy Policy")
|
||||
|
||||
with app.test_client() as client:
|
||||
r = client.get("/register")
|
||||
assert r.status_code == 200
|
||||
assert "privacy policy" in r.get_data(as_text=True)
|
||||
assert "terms of service" in r.get_data(as_text=True)
|
||||
|
||||
r = client.get("/tos")
|
||||
assert r.status_code == 200
|
||||
assert "Terms of Service" in r.get_data(as_text=True)
|
||||
|
||||
r = client.get("/privacy")
|
||||
assert r.status_code == 200
|
||||
assert "Privacy Policy" in r.get_data(as_text=True)
|
||||
destroy_ctfd(app)
|
||||
226
tests/test_plugin_utils.py
Normal file
226
tests/test_plugin_utils.py
Normal file
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.plugins import (
|
||||
bypass_csrf_protection,
|
||||
get_admin_plugin_menu_bar,
|
||||
get_user_page_menu_bar,
|
||||
override_template,
|
||||
register_admin_plugin_menu_bar,
|
||||
register_admin_plugin_script,
|
||||
register_admin_plugin_stylesheet,
|
||||
register_plugin_asset,
|
||||
register_plugin_assets_directory,
|
||||
register_plugin_script,
|
||||
register_user_page_menu_bar,
|
||||
)
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
login_as_user,
|
||||
setup_ctfd,
|
||||
)
|
||||
|
||||
|
||||
def test_register_plugin_asset():
|
||||
"""Test that plugin asset registration works"""
|
||||
app = create_ctfd(setup=False)
|
||||
register_plugin_asset(app, asset_path="/plugins/__init__.py")
|
||||
app = setup_ctfd(app)
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/plugins/__init__.py")
|
||||
assert len(r.get_data(as_text=True)) > 0
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_register_plugin_assets_directory():
|
||||
"""Test that plugin asset directory registration works"""
|
||||
app = create_ctfd(setup=False)
|
||||
register_plugin_assets_directory(app, base_path="/plugins/")
|
||||
app = setup_ctfd(app)
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/plugins/__init__.py")
|
||||
assert len(r.get_data(as_text=True)) > 0
|
||||
assert r.status_code == 200
|
||||
r = client.get("/plugins/challenges/__init__.py")
|
||||
assert len(r.get_data(as_text=True)) > 0
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_override_template():
|
||||
"""Does override_template work properly for regular themes when used from a plugin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
override_template("login.html", "LOGIN OVERRIDE")
|
||||
with app.test_client() as client:
|
||||
r = client.get("/login")
|
||||
assert r.status_code == 200
|
||||
output = r.get_data(as_text=True)
|
||||
assert "LOGIN OVERRIDE" in output
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_admin_override_template():
|
||||
"""Does override_template work properly for the admin panel when used from a plugin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
override_template("admin/users/user.html", "ADMIN USER OVERRIDE")
|
||||
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
r = client.get("/admin/users/1")
|
||||
assert r.status_code == 200
|
||||
output = r.get_data(as_text=True)
|
||||
assert "ADMIN USER OVERRIDE" in output
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_register_plugin_script():
|
||||
"""Test that register_plugin_script adds script paths to the core theme when used from a plugin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_plugin_script("/fake/script/path.js")
|
||||
register_plugin_script("http://examplectf.com/fake/script/path.js")
|
||||
with app.test_client() as client:
|
||||
r = client.get("/")
|
||||
output = r.get_data(as_text=True)
|
||||
assert "/fake/script/path.js" in output
|
||||
assert "http://examplectf.com/fake/script/path.js" in output
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_register_plugin_stylesheet():
|
||||
"""Test that register_plugin_stylesheet adds stylesheet paths to the core theme when used from a plugin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_plugin_script("/fake/stylesheet/path.css")
|
||||
register_plugin_script("http://examplectf.com/fake/stylesheet/path.css")
|
||||
with app.test_client() as client:
|
||||
r = client.get("/")
|
||||
output = r.get_data(as_text=True)
|
||||
assert "/fake/stylesheet/path.css" in output
|
||||
assert "http://examplectf.com/fake/stylesheet/path.css" in output
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_register_admin_plugin_script():
|
||||
"""Test that register_admin_plugin_script adds script paths to the admin theme when used from a plugin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_admin_plugin_script("/fake/script/path.js")
|
||||
register_admin_plugin_script("http://examplectf.com/fake/script/path.js")
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.get("/admin/statistics")
|
||||
output = r.get_data(as_text=True)
|
||||
assert "/fake/script/path.js" in output
|
||||
assert "http://examplectf.com/fake/script/path.js" in output
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_register_admin_plugin_stylesheet():
|
||||
"""Test that register_admin_plugin_stylesheet adds stylesheet paths to the admin theme when used from a plugin"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_admin_plugin_stylesheet("/fake/stylesheet/path.css")
|
||||
register_admin_plugin_stylesheet(
|
||||
"http://examplectf.com/fake/stylesheet/path.css"
|
||||
)
|
||||
with login_as_user(app, name="admin") as client:
|
||||
r = client.get("/admin/statistics")
|
||||
output = r.get_data(as_text=True)
|
||||
assert "/fake/stylesheet/path.css" in output
|
||||
assert "http://examplectf.com/fake/stylesheet/path.css" in output
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_register_admin_plugin_menu_bar():
|
||||
"""
|
||||
Test that register_admin_plugin_menu_bar() properly inserts into HTML and get_admin_plugin_menu_bar()
|
||||
returns the proper list.
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_admin_plugin_menu_bar(
|
||||
title="test_admin_plugin_name", route="/test_plugin"
|
||||
)
|
||||
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
r = client.get("/admin/statistics")
|
||||
output = r.get_data(as_text=True)
|
||||
assert "/test_plugin" in output
|
||||
assert "test_admin_plugin_name" in output
|
||||
|
||||
menu_item = get_admin_plugin_menu_bar()[0]
|
||||
assert menu_item.title == "test_admin_plugin_name"
|
||||
assert menu_item.route == "/test_plugin"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_register_user_page_menu_bar():
|
||||
"""
|
||||
Test that the register_user_page_menu_bar() properly inserts into HTML and get_user_page_menu_bar() returns the
|
||||
proper list.
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user_page_menu_bar(
|
||||
title="test_user_menu_link", route="/test_user_href"
|
||||
)
|
||||
|
||||
with app.test_client() as client:
|
||||
r = client.get("/")
|
||||
output = r.get_data(as_text=True)
|
||||
assert "/test_user_href" in output
|
||||
assert "test_user_menu_link" in output
|
||||
|
||||
with app.test_request_context():
|
||||
menu_item = get_user_page_menu_bar()[0]
|
||||
assert menu_item.title == "test_user_menu_link"
|
||||
assert menu_item.route == "/test_user_href"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_bypass_csrf_protection():
|
||||
"""
|
||||
Test that the bypass_csrf_protection decorator functions properly
|
||||
"""
|
||||
app = create_ctfd()
|
||||
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.post("/login")
|
||||
output = r.get_data(as_text=True)
|
||||
assert r.status_code == 403
|
||||
|
||||
def bypass_csrf_protection_test_route():
|
||||
return "Success", 200
|
||||
|
||||
# Hijack an existing route to avoid any kind of hacks to create a test route
|
||||
app.view_functions["auth.login"] = bypass_csrf_protection(
|
||||
bypass_csrf_protection_test_route
|
||||
)
|
||||
|
||||
with app.test_client() as client:
|
||||
r = client.post("/login")
|
||||
output = r.get_data(as_text=True)
|
||||
assert r.status_code == 200
|
||||
assert output == "Success"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenges_model_access_plugin_class():
|
||||
"""
|
||||
Test that the Challenges model can access its plugin class
|
||||
"""
|
||||
app = create_ctfd()
|
||||
|
||||
with app.app_context():
|
||||
from CTFd.plugins.challenges import get_chal_class
|
||||
|
||||
chal = gen_challenge(app.db)
|
||||
assert chal.plugin_class == get_chal_class("standard")
|
||||
destroy_ctfd(app)
|
||||
31
tests/test_setup.py
Normal file
31
tests/test_setup.py
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.utils import get_config
|
||||
from CTFd.utils.security.csrf import generate_nonce
|
||||
from CTFd.utils.security.signing import serialize
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user
|
||||
|
||||
|
||||
def test_setup_integrations():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
user = login_as_user(app)
|
||||
r = user.get("/setup/integrations")
|
||||
assert r.status_code == 403
|
||||
|
||||
admin = login_as_user(app, "admin")
|
||||
r = admin.get("/setup/integrations")
|
||||
assert r.status_code == 403
|
||||
|
||||
admin = login_as_user(app, "admin")
|
||||
|
||||
url = "/setup/integrations?state={state}&mlc_client_id=client_id&mlc_client_secret=client_secret&name=mlc".format(
|
||||
state=serialize(generate_nonce())
|
||||
)
|
||||
r = admin.get(url)
|
||||
assert r.status_code == 200
|
||||
assert get_config("oauth_client_id") == "client_id"
|
||||
assert get_config("oauth_client_secret") == "client_secret"
|
||||
destroy_ctfd(app)
|
||||
112
tests/test_share.py
Normal file
112
tests/test_share.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import re
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
from CTFd.utils import set_config
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_solve,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_share_endpoints():
|
||||
"""Test that social share endpoints and disabling are working"""
|
||||
app = create_ctfd(ctf_theme="core")
|
||||
with app.app_context():
|
||||
chal_id = gen_challenge(app.db).id
|
||||
register_user(app)
|
||||
gen_solve(app.db, user_id=2, challenge_id=chal_id)
|
||||
|
||||
# social_shares are enabled by default now
|
||||
set_config("social_shares", False)
|
||||
|
||||
# Test disabled shares dont work
|
||||
with login_as_user(app) as client:
|
||||
r = client.post(
|
||||
"/api/v1/shares",
|
||||
json={"type": "solve", "user_id": 2, "challenge_id": 1},
|
||||
)
|
||||
assert r.status_code == 403
|
||||
|
||||
set_config("social_shares", True)
|
||||
|
||||
# Test working share
|
||||
with login_as_user(app) as client:
|
||||
r = client.post(
|
||||
"/api/v1/shares",
|
||||
json={"type": "solve", "user_id": 2, "challenge_id": 1},
|
||||
)
|
||||
resp = r.get_json()
|
||||
url = resp["data"]["url"]
|
||||
|
||||
# Test loadding share page
|
||||
with app.test_client() as client:
|
||||
r = client.get(url)
|
||||
resp = r.get_data(as_text=True)
|
||||
assert r.status_code == 200
|
||||
assert "user has solved" in resp
|
||||
assert "+100 points" in resp
|
||||
|
||||
# Test downloading asset image
|
||||
m = re.search(r"og:image(.*)", resp)
|
||||
# Remove extra text
|
||||
asset_url = m.group()[19:-4].replace("&", "&")
|
||||
r = client.get(asset_url)
|
||||
assert r.status_code == 200
|
||||
|
||||
# Test disabled social shares
|
||||
set_config("social_shares", False)
|
||||
with app.test_client() as client:
|
||||
r = client.get(url)
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_share_security():
|
||||
"""Test that shares and their assets do not load without a valid mac"""
|
||||
app = create_ctfd(ctf_theme="core")
|
||||
with app.app_context():
|
||||
chal_id = gen_challenge(app.db).id
|
||||
register_user(app)
|
||||
gen_solve(app.db, user_id=2, challenge_id=chal_id)
|
||||
|
||||
set_config("social_shares", True)
|
||||
|
||||
# Test working share
|
||||
with login_as_user(app) as client:
|
||||
r = client.post(
|
||||
"/api/v1/shares",
|
||||
json={"type": "solve", "user_id": 2, "challenge_id": 1},
|
||||
)
|
||||
resp = r.get_json()
|
||||
url = resp["data"]["url"]
|
||||
|
||||
with app.test_client() as client:
|
||||
r = client.get(url)
|
||||
resp = r.get_data(as_text=True)
|
||||
assert r.status_code == 200
|
||||
|
||||
# Test downloading asset image
|
||||
m = re.search(r"og:image(.*)", resp)
|
||||
# Remove extra text
|
||||
asset_url = m.group()[19:-4].replace("&", "&")
|
||||
r = client.get(asset_url)
|
||||
assert r.status_code == 200
|
||||
|
||||
parsed_asset_url = urlparse(asset_url)
|
||||
parsed_asset_url = parsed_asset_url._replace(
|
||||
path=(parsed_asset_url.path[:-5]) + "abc.png"
|
||||
)
|
||||
|
||||
asset_url = urlunparse(parsed_asset_url)
|
||||
r = client.get(asset_url)
|
||||
assert r.status_code == 404
|
||||
|
||||
# Test modified mac
|
||||
url = url[:-3] + "abc"
|
||||
r = client.get(url)
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
227
tests/test_themes.py
Normal file
227
tests/test_themes.py
Normal file
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
import pytest
|
||||
from flask import render_template, render_template_string, request
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
from jinja2.sandbox import SecurityError
|
||||
from werkzeug.test import Client
|
||||
|
||||
from CTFd.config import TestingConfig
|
||||
from CTFd.utils import get_config, set_config
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, gen_user, login_as_user
|
||||
|
||||
|
||||
def test_themes_run_in_sandbox():
|
||||
"""Does get_config and set_config work properly"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
try:
|
||||
app.jinja_env.from_string(
|
||||
"{{ ().__class__.__bases__[0].__subclasses__()[40]('./test_utils.py').read() }}"
|
||||
).render()
|
||||
except SecurityError:
|
||||
pass
|
||||
except Exception as e:
|
||||
raise e
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_themes_cant_access_configpy_attributes():
|
||||
"""Themes should not be able to access config.py attributes"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
assert app.config["SECRET_KEY"] == "AAAAAAAAAAAAAAAAAAAA"
|
||||
assert (
|
||||
app.jinja_env.from_string("{{ get_config('SECRET_KEY') }}").render()
|
||||
!= app.config["SECRET_KEY"]
|
||||
)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_themes_escape_html():
|
||||
"""Themes should escape XSS properly"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
user = gen_user(app.db, name="<script>alert(1)</script>")
|
||||
user.affiliation = "<script>alert(1)</script>"
|
||||
user.website = "<script>alert(1)</script>"
|
||||
user.country = "<script>alert(1)</script>"
|
||||
|
||||
with app.test_client() as client:
|
||||
r = client.get("/users")
|
||||
assert r.status_code == 200
|
||||
assert "<script>alert(1)</script>" not in r.get_data(as_text=True)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_theme_header():
|
||||
"""Config should be able to properly set CSS in theme header"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
|
||||
with login_as_user(app, "admin") as admin:
|
||||
css_value = """.test{}"""
|
||||
css_value2 = """.test2{}"""
|
||||
r = admin.patch("/api/v1/configs", json={"theme_header": css_value})
|
||||
assert r.status_code == 200
|
||||
assert get_config("theme_header") == css_value
|
||||
|
||||
r = admin.get("/")
|
||||
assert css_value in r.get_data(as_text=True)
|
||||
|
||||
r = admin.patch("/api/v1/configs", json={"theme_header": css_value2})
|
||||
r = admin.get("/")
|
||||
assert css_value2 in r.get_data(as_text=True)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_that_ctfd_can_be_deployed_in_subdir():
|
||||
"""Test that CTFd can be deployed in a subdirectory"""
|
||||
# This test is quite complicated. I do not suggest modifying it haphazardly.
|
||||
# Flask is automatically inserting the APPLICATION_ROOT into the
|
||||
# test urls which means when we hit /setup we hit /ctf/setup.
|
||||
# You can use the raw Werkzeug client to bypass this as we do below.
|
||||
app = create_ctfd(setup=False, application_root="/ctf")
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/")
|
||||
assert r.status_code == 302
|
||||
assert r.location == "/ctf/setup"
|
||||
|
||||
r = client.get("/setup")
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"ctf_name": "CTFd",
|
||||
"ctf_description": "CTF description",
|
||||
"name": "admin",
|
||||
"email": "admin@examplectf.com",
|
||||
"password": "password",
|
||||
"user_mode": "users",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/setup", data=data)
|
||||
assert r.status_code == 302
|
||||
assert r.location == "/ctf/"
|
||||
|
||||
c = Client(app)
|
||||
response = c.get("/")
|
||||
headers = dict(response.headers)
|
||||
assert response.status == "302 FOUND"
|
||||
assert headers["Location"] == "/ctf/?"
|
||||
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
assert "Challenges" in r.get_data(as_text=True)
|
||||
|
||||
r = client.get("/scoreboard")
|
||||
assert r.status_code == 200
|
||||
assert "Scoreboard" in r.get_data(as_text=True)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_that_request_path_hijacking_works_properly():
|
||||
"""Test that the CTFdRequest subclass correctly mimics the Flask Request when it should"""
|
||||
app = create_ctfd(setup=False, application_root="/ctf")
|
||||
assert app.request_class.__name__ == "CTFdRequest"
|
||||
with app.app_context():
|
||||
# Despite loading /challenges request.path should actually be /ctf/challenges because we are
|
||||
# preprending script_root and the test context already accounts for the application_root
|
||||
with app.test_request_context("/challenges"):
|
||||
assert request.path == "/ctf/challenges"
|
||||
destroy_ctfd(app)
|
||||
|
||||
app = create_ctfd()
|
||||
assert app.request_class.__name__ == "CTFdRequest"
|
||||
with app.app_context():
|
||||
# Under normal circumstances we should be an exact clone of BaseRequest
|
||||
with app.test_request_context("/challenges"):
|
||||
assert request.path == "/challenges"
|
||||
|
||||
from flask import Flask
|
||||
|
||||
test_app = Flask("test")
|
||||
assert test_app.request_class.__name__ == "Request"
|
||||
with test_app.test_request_context("/challenges"):
|
||||
assert request.path == "/challenges"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_theme_fallback_config():
|
||||
"""Test that the `THEME_FALLBACK` config properly falls themes back to the core theme"""
|
||||
|
||||
class ThemeFallbackConfig(TestingConfig):
|
||||
THEME_FALLBACK = False
|
||||
|
||||
app = create_ctfd(config=ThemeFallbackConfig)
|
||||
# Make an empty theme
|
||||
try:
|
||||
os.mkdir(os.path.join(app.root_path, "themes", "foo_fallback"))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Without theme fallback, missing themes should disappear
|
||||
with app.app_context():
|
||||
app.config["THEME_FALLBACK"] = False
|
||||
set_config("ctf_theme", "foo_fallback")
|
||||
assert app.config["THEME_FALLBACK"] == False
|
||||
with app.test_client() as client:
|
||||
try:
|
||||
r = client.get("/")
|
||||
except TemplateNotFound:
|
||||
pass
|
||||
try:
|
||||
r = client.get("/themes/foo_fallback/static/manifest.json")
|
||||
except TemplateNotFound:
|
||||
pass
|
||||
destroy_ctfd(app)
|
||||
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("ctf_theme", "foo_fallback")
|
||||
assert app.config["THEME_FALLBACK"] == True
|
||||
with app.test_client() as client:
|
||||
r = client.get("/")
|
||||
assert r.status_code == 200
|
||||
r = client.get("/themes/foo_fallback/static/manifest.json")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
# Remove empty theme
|
||||
os.rmdir(os.path.join(app.root_path, "themes", "foo_fallback"))
|
||||
|
||||
|
||||
def test_theme_template_loading_by_prefix():
|
||||
"""Test that we can load theme files by their folder prefix"""
|
||||
app = create_ctfd()
|
||||
with app.test_request_context():
|
||||
tpl1 = render_template_string("{% extends 'core/page.html' %}", content="test")
|
||||
tpl2 = render_template("page.html", content="test")
|
||||
assert tpl1 == tpl2
|
||||
|
||||
|
||||
def test_theme_template_disallow_loading_admin_templates():
|
||||
"""Test that admin files in a theme will not be loaded"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
try:
|
||||
# Make an empty malicious theme
|
||||
filename = os.path.join(
|
||||
app.root_path, "themes", "foo_disallow", "admin", "malicious.html"
|
||||
)
|
||||
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
||||
set_config("ctf_theme", "foo_disallow")
|
||||
with open(filename, "w") as f:
|
||||
f.write("malicious")
|
||||
|
||||
with pytest.raises(TemplateNotFound):
|
||||
render_template_string("{% include 'admin/malicious.html' %}")
|
||||
finally:
|
||||
# Remove empty theme
|
||||
shutil.rmtree(
|
||||
os.path.join(app.root_path, "themes", "foo_disallow"),
|
||||
ignore_errors=True,
|
||||
)
|
||||
552
tests/test_views.py
Normal file
552
tests/test_views.py
Normal file
@@ -0,0 +1,552 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
|
||||
from flask import url_for
|
||||
from freezegun import freeze_time
|
||||
|
||||
from CTFd.cache import clear_pages
|
||||
from CTFd.utils import set_config
|
||||
from CTFd.utils.config.pages import get_pages
|
||||
from CTFd.utils.encoding import hexencode
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_file,
|
||||
gen_page,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_index():
|
||||
"""Does the index page return a 200 by default"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_page():
|
||||
"""Test that users can access pages that are created in the database"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_page(
|
||||
app.db, title="Title", route="this-is-a-route", content="This is some HTML"
|
||||
)
|
||||
|
||||
with app.test_client() as client:
|
||||
r = client.get("/this-is-a-route")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_draft_pages():
|
||||
"""Test that draft pages can't be seen"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_page(
|
||||
app.db,
|
||||
title="Title",
|
||||
route="this-is-a-route",
|
||||
content="This is some HTML",
|
||||
draft=True,
|
||||
)
|
||||
|
||||
with app.test_client() as client:
|
||||
r = client.get("/this-is-a-route")
|
||||
assert r.status_code == 404
|
||||
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/this-is-a-route")
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_page_requiring_auth():
|
||||
"""Test that pages properly require authentication"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_page(
|
||||
app.db,
|
||||
title="Title",
|
||||
route="this-is-a-route",
|
||||
content="This is some HTML",
|
||||
auth_required=True,
|
||||
)
|
||||
|
||||
with app.test_client() as client:
|
||||
r = client.get("/this-is-a-route")
|
||||
assert r.status_code == 302
|
||||
assert r.location == "/login?next=%2Fthis-is-a-route%3F"
|
||||
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/this-is-a-route")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_hidden_pages():
|
||||
"""Test that hidden pages aren't on the navbar but can be loaded"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
page = gen_page(
|
||||
app.db,
|
||||
title="HiddenPageTitle",
|
||||
route="this-is-a-hidden-route",
|
||||
content="This is some HTML",
|
||||
hidden=True,
|
||||
)
|
||||
clear_pages()
|
||||
assert page not in get_pages()
|
||||
|
||||
with app.test_client() as client:
|
||||
r = client.get("/")
|
||||
assert r.status_code == 200
|
||||
assert "HiddenPageTitle" not in r.get_data(as_text=True)
|
||||
|
||||
with app.test_client() as client:
|
||||
r = client.get("/this-is-a-hidden-route")
|
||||
assert r.status_code == 200
|
||||
assert "This is some HTML" in r.get_data(as_text=True)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_not_found():
|
||||
"""Should return a 404 for pages that are not found"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/this-should-404")
|
||||
assert r.status_code == 404
|
||||
r = client.post("/this-should-404")
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_themes_handler():
|
||||
"""Test that the themes handler is working properly"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/themes/core/static/manifest.json")
|
||||
assert r.status_code == 200
|
||||
r = client.get("/themes/core/static/css/404_NOT_FOUND")
|
||||
assert r.status_code == 404
|
||||
r = client.get("/themes/core/static/%2e%2e/%2e%2e/%2e%2e/utils.py")
|
||||
assert r.status_code == 404
|
||||
r = client.get("/themes/core/static/%2e%2e%2f%2e%2e%2f%2e%2e%2futils.py")
|
||||
assert r.status_code == 404
|
||||
r = client.get("/themes/core/static/..%2f..%2f..%2futils.py")
|
||||
assert r.status_code == 404
|
||||
r = client.get("/themes/core/static/../../../utils.py")
|
||||
assert r.status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_pages_routing_and_rendering():
|
||||
"""Test that pages are routing and rendering"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
html = """## The quick brown fox jumped over the lazy dog"""
|
||||
route = "test"
|
||||
title = "Test"
|
||||
gen_page(app.db, title, route, html)
|
||||
|
||||
with app.test_client() as client:
|
||||
r = client.get("/test")
|
||||
output = r.get_data(as_text=True)
|
||||
print(output)
|
||||
assert "<h2>The quick brown fox jumped over the lazy dog</h2>" in output
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_profile():
|
||||
"""Can a registered user load their private profile (/profile)"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/profile")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_can_access_files():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
from CTFd.utils.uploads import rmdir
|
||||
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
path = app.config.get("UPLOAD_FOLDER")
|
||||
|
||||
location = os.path.join(path, "test_file_path", "test.txt")
|
||||
directory = os.path.dirname(location)
|
||||
model_path = os.path.join("test_file_path", "test.txt")
|
||||
|
||||
try:
|
||||
os.makedirs(directory)
|
||||
with open(location, "wb") as obj:
|
||||
obj.write("testing file load".encode())
|
||||
gen_file(app.db, location=model_path, challenge_id=chal_id)
|
||||
url = url_for("views.files", path=model_path)
|
||||
|
||||
# Unauthed user should be able to see challenges if challenges are public
|
||||
set_config("challenge_visibility", "public")
|
||||
with app.test_client() as client:
|
||||
r = client.get(url)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.get_data(as_text=True) == "testing file load"
|
||||
|
||||
# Unauthed user should not be able to see challenges if challenges are private
|
||||
set_config("challenge_visibility", "private")
|
||||
with app.test_client() as client:
|
||||
r = client.get(url)
|
||||
|
||||
assert r.status_code == 403
|
||||
assert r.get_data(as_text=True) != "testing file load"
|
||||
|
||||
# Authed user should be able to see files if challenges are private
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get(url)
|
||||
assert r.status_code == 200
|
||||
assert r.get_data(as_text=True) == "testing file load"
|
||||
|
||||
with freeze_time("2017-10-5"):
|
||||
# Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("start", "1507262400")
|
||||
for v in ("public", "private"):
|
||||
set_config("challenge_visibility", v)
|
||||
|
||||
# Unauthed users shouldn't be able to see files if the CTF hasn't started
|
||||
client = app.test_client()
|
||||
r = client.get(url)
|
||||
assert r.status_code == 403
|
||||
assert r.get_data(as_text=True) != "testing file load"
|
||||
|
||||
# Authed users shouldn't be able to see files if the CTF hasn't started
|
||||
client = login_as_user(app)
|
||||
r = client.get(url)
|
||||
assert r.status_code == 403
|
||||
assert r.get_data(as_text=True) != "testing file load"
|
||||
|
||||
# Admins should be able to see files if the CTF hasn't started
|
||||
admin = login_as_user(app, "admin")
|
||||
r = admin.get(url)
|
||||
assert r.status_code == 200
|
||||
assert r.get_data(as_text=True) == "testing file load"
|
||||
|
||||
with freeze_time("2017-10-7"):
|
||||
# Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("end", "1507262400")
|
||||
for v in ("public", "private"):
|
||||
set_config("challenge_visibility", v)
|
||||
|
||||
# Unauthed users shouldn't be able to see files if the CTF has ended
|
||||
client = app.test_client()
|
||||
r = client.get(url)
|
||||
assert r.status_code == 403
|
||||
assert r.get_data(as_text=True) != "testing file load"
|
||||
|
||||
# Authed users shouldn't be able to see files if the CTF has ended
|
||||
client = login_as_user(app)
|
||||
r = client.get(url)
|
||||
assert r.status_code == 403
|
||||
assert r.get_data(as_text=True) != "testing file load"
|
||||
|
||||
# Admins should be able to see files if the CTF has ended
|
||||
admin = login_as_user(app, "admin")
|
||||
r = admin.get(url)
|
||||
assert r.status_code == 200
|
||||
assert r.get_data(as_text=True) == "testing file load"
|
||||
finally:
|
||||
rmdir(directory)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_admin_access_files_with_auth_token():
|
||||
"""Test that admins can download files via token before CTF starts"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
from CTFd.utils.uploads import rmdir
|
||||
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
path = app.config.get("UPLOAD_FOLDER")
|
||||
|
||||
md5hash = hexencode(os.urandom(16))
|
||||
location = os.path.join(path, md5hash, "test.txt")
|
||||
directory = os.path.dirname(location)
|
||||
model_path = os.path.join(md5hash, "test.txt")
|
||||
|
||||
try:
|
||||
os.makedirs(directory)
|
||||
with open(location, "wb") as obj:
|
||||
obj.write("testing admin file load".encode())
|
||||
gen_file(app.db, location=model_path, challenge_id=chal_id)
|
||||
|
||||
# Register a regular user and admin user
|
||||
register_user(app)
|
||||
|
||||
# Get regular user token for comparison
|
||||
with login_as_user(app) as user_client:
|
||||
req = user_client.get("/api/v1/challenges/1")
|
||||
data = req.get_json()
|
||||
user_file_url = data["data"]["files"][0]
|
||||
|
||||
# Set CTF to start in the future (before CTF start scenario)
|
||||
with freeze_time("2017-10-5"):
|
||||
# Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("start", "1507262400")
|
||||
set_config("challenge_visibility", "private")
|
||||
|
||||
# Confirm that challenges aren't visible
|
||||
with login_as_user(app) as user_client:
|
||||
req = user_client.get("/challenges")
|
||||
assert req.status_code == 403
|
||||
|
||||
# Get admin token for file download
|
||||
with login_as_user(app, "admin") as admin_client:
|
||||
req = admin_client.get("/api/v1/challenges/1")
|
||||
data = req.get_json()
|
||||
admin_file_url = data["data"]["files"][0]
|
||||
|
||||
with app.test_client() as client:
|
||||
# Test that admin can download files via token before CTF starts
|
||||
r = client.get(admin_file_url)
|
||||
assert r.status_code == 200
|
||||
assert r.get_data(as_text=True) == "testing admin file load"
|
||||
|
||||
# Test that regular user cannot download files via token before CTF starts
|
||||
r = client.get(user_file_url)
|
||||
assert r.status_code == 403
|
||||
assert r.get_data(as_text=True) != "testing admin file load"
|
||||
|
||||
# Test direct file access without token should fail for both
|
||||
direct_url = url_for("views.files", path=model_path)
|
||||
r = client.get(direct_url)
|
||||
assert r.status_code == 403
|
||||
|
||||
# Test with invalid token should fail
|
||||
invalid_token_url = url_for(
|
||||
"views.files", path=model_path, token="invalid_token"
|
||||
)
|
||||
r = client.get(invalid_token_url)
|
||||
assert r.status_code == 403
|
||||
|
||||
finally:
|
||||
rmdir(directory)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_can_access_files_with_auth_token():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
from CTFd.utils.uploads import rmdir
|
||||
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
path = app.config.get("UPLOAD_FOLDER")
|
||||
|
||||
md5hash = hexencode(os.urandom(16))
|
||||
|
||||
location = os.path.join(path, md5hash, "test.txt")
|
||||
directory = os.path.dirname(location)
|
||||
model_path = os.path.join(md5hash, "test.txt")
|
||||
|
||||
try:
|
||||
os.makedirs(directory)
|
||||
with open(location, "wb") as obj:
|
||||
obj.write("testing file load".encode())
|
||||
gen_file(app.db, location=model_path, challenge_id=chal_id)
|
||||
url = url_for("views.files", path=model_path)
|
||||
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
req = client.get("/api/v1/challenges/1")
|
||||
data = req.get_json()
|
||||
file_url = data["data"]["files"][0]
|
||||
|
||||
with app.test_client() as client:
|
||||
r = client.get(url)
|
||||
assert r.status_code == 403
|
||||
assert r.get_data(as_text=True) != "testing file load"
|
||||
|
||||
r = client.get(
|
||||
url_for(
|
||||
"views.files",
|
||||
path=model_path,
|
||||
token="random_token_that_shouldnt_work",
|
||||
)
|
||||
)
|
||||
assert r.status_code == 403
|
||||
assert r.get_data(as_text=True) != "testing file load"
|
||||
|
||||
r = client.get(file_url)
|
||||
assert r.status_code == 200
|
||||
assert r.get_data(as_text=True) == "testing file load"
|
||||
|
||||
# Unauthed users shouldn't be able to see files if the CTF is admins only
|
||||
set_config("challenge_visibility", "admins")
|
||||
r = client.get(file_url)
|
||||
assert r.status_code == 403
|
||||
assert r.get_data(as_text=True) != "testing file load"
|
||||
set_config("challenge_visibility", "private")
|
||||
|
||||
with freeze_time("2017-10-5"):
|
||||
# Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("start", "1507262400")
|
||||
|
||||
# Unauthed users shouldn't be able to see files if the CTF hasn't started
|
||||
r = client.get(file_url)
|
||||
assert r.status_code == 403
|
||||
assert r.get_data(as_text=True) != "testing file load"
|
||||
|
||||
with freeze_time("2017-10-5"):
|
||||
# Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("start", "1507262400")
|
||||
for v in ("public", "private"):
|
||||
set_config("challenge_visibility", v)
|
||||
|
||||
# Unauthed users shouldn't be able to see files if the CTF hasn't started
|
||||
client = app.test_client()
|
||||
r = client.get(file_url)
|
||||
assert r.status_code == 403
|
||||
assert r.get_data(as_text=True) != "testing file load"
|
||||
|
||||
# Authed users shouldn't be able to see files if the CTF hasn't started
|
||||
client = login_as_user(app)
|
||||
r = client.get(file_url)
|
||||
assert r.status_code == 403
|
||||
assert r.get_data(as_text=True) != "testing file load"
|
||||
|
||||
# Admins should be able to see files if the CTF hasn't started
|
||||
admin = login_as_user(app, "admin")
|
||||
r = admin.get(file_url)
|
||||
assert r.status_code == 200
|
||||
assert r.get_data(as_text=True) == "testing file load"
|
||||
|
||||
with freeze_time("2017-10-7"):
|
||||
# Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("end", "1507262400")
|
||||
for v in ("public", "private"):
|
||||
set_config("challenge_visibility", v)
|
||||
|
||||
# Unauthed users shouldn't be able to see files if the CTF has ended
|
||||
client = app.test_client()
|
||||
r = client.get(file_url)
|
||||
assert r.status_code == 403
|
||||
assert r.get_data(as_text=True) != "testing file load"
|
||||
|
||||
# Authed users shouldn't be able to see files if the CTF has ended
|
||||
client = login_as_user(app)
|
||||
r = client.get(file_url)
|
||||
assert r.status_code == 403
|
||||
assert r.get_data(as_text=True) != "testing file load"
|
||||
|
||||
# Admins should be able to see files if the CTF has ended
|
||||
admin = login_as_user(app, "admin")
|
||||
r = admin.get(file_url)
|
||||
assert r.status_code == 200
|
||||
assert r.get_data(as_text=True) == "testing file load"
|
||||
|
||||
with freeze_time("2017-10-7"):
|
||||
# Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("end", "1507262400")
|
||||
set_config("view_after_ctf", True)
|
||||
|
||||
# Get file_url under current time
|
||||
with login_as_user(app) as user:
|
||||
req = user.get("/api/v1/challenges/1")
|
||||
data = req.get_json()
|
||||
file_url = data["data"]["files"][0]
|
||||
|
||||
for v in ("public", "private"):
|
||||
set_config("challenge_visibility", v)
|
||||
|
||||
# Unauthed users should be able to download if view_after_ctf
|
||||
client = app.test_client()
|
||||
r = client.get(file_url)
|
||||
assert r.status_code == 200
|
||||
assert r.get_data(as_text=True) == "testing file load"
|
||||
finally:
|
||||
rmdir(directory)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_can_access_files_if_view_after_ctf():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
from CTFd.utils.uploads import rmdir
|
||||
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
path = app.config.get("UPLOAD_FOLDER")
|
||||
|
||||
md5hash = hexencode(os.urandom(16))
|
||||
|
||||
location = os.path.join(path, md5hash, "test.txt")
|
||||
directory = os.path.dirname(location)
|
||||
model_path = os.path.join(md5hash, "test.txt")
|
||||
|
||||
try:
|
||||
os.makedirs(directory)
|
||||
with open(location, "wb") as obj:
|
||||
obj.write("testing file load".encode())
|
||||
gen_file(app.db, location=model_path, challenge_id=chal_id)
|
||||
|
||||
register_user(app)
|
||||
with login_as_user(app) as client:
|
||||
# After ctf end
|
||||
# Get file_url during freeze time
|
||||
with freeze_time("2017-10-7"):
|
||||
req = client.get("/api/v1/challenges/1")
|
||||
data = req.get_json()
|
||||
file_url = data["data"]["files"][0]
|
||||
|
||||
# Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config("end", "1507262400")
|
||||
|
||||
r = client.get(file_url)
|
||||
assert r.status_code == 403
|
||||
assert r.get_data(as_text=True) != "testing file load"
|
||||
|
||||
set_config("view_after_ctf", True)
|
||||
r = client.get(file_url)
|
||||
assert r.status_code == 200
|
||||
assert r.get_data(as_text=True) == "testing file load"
|
||||
|
||||
# Unauthed users should be able to download if view_after_ctf
|
||||
unauth_client = app.test_client()
|
||||
r = unauth_client.get(file_url)
|
||||
assert r.status_code == 200
|
||||
assert r.get_data(as_text=True) == "testing file load"
|
||||
finally:
|
||||
rmdir(directory)
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_robots_txt():
|
||||
"""Does the robots.txt page work"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/robots.txt")
|
||||
assert r.status_code == 200
|
||||
assert r.get_data(as_text=True) == "User-agent: *\nDisallow: /admin\n"
|
||||
set_config("robots_txt", "testing")
|
||||
with app.test_client() as client:
|
||||
r = client.get("/robots.txt")
|
||||
assert r.status_code == 200
|
||||
assert r.get_data(as_text=True) == "testing"
|
||||
destroy_ctfd(app)
|
||||
0
tests/users/__init__.py
Normal file
0
tests/users/__init__.py
Normal file
837
tests/users/test_auth.py
Normal file
837
tests/users/test_auth.py
Normal file
@@ -0,0 +1,837 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from CTFd.models import Users, db
|
||||
from CTFd.utils import get_config, set_config
|
||||
from CTFd.utils.crypto import verify_password
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user
|
||||
|
||||
|
||||
def test_register_user():
|
||||
"""Can a user be registered"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
user_count = Users.query.count()
|
||||
assert user_count == 2 # There's the admin user and the created user
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_register_unicode_user():
|
||||
"""Can a user with a unicode name be registered"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="你好")
|
||||
user_count = Users.query.count()
|
||||
assert user_count == 2 # There's the admin user and the created user
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_register_duplicate_username():
|
||||
"""A user shouldn't be able to use an already registered team name"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(
|
||||
app,
|
||||
name="user1",
|
||||
email="user1@examplectf.com",
|
||||
password="password",
|
||||
raise_for_error=False,
|
||||
)
|
||||
register_user(
|
||||
app,
|
||||
name="user1",
|
||||
email="user2@examplectf.com",
|
||||
password="password",
|
||||
raise_for_error=False,
|
||||
)
|
||||
register_user(
|
||||
app,
|
||||
name="admin ",
|
||||
email="admin2@examplectf.com",
|
||||
password="password",
|
||||
raise_for_error=False,
|
||||
)
|
||||
user_count = Users.query.count()
|
||||
assert user_count == 2 # There's the admin user and the first created user
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_register_duplicate_email():
|
||||
"""A user shouldn't be able to use an already registered email address"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(
|
||||
app,
|
||||
name="user1",
|
||||
email="user1@examplectf.com",
|
||||
password="password",
|
||||
raise_for_error=False,
|
||||
)
|
||||
register_user(
|
||||
app,
|
||||
name="user2",
|
||||
email="user1@examplectf.com",
|
||||
password="password",
|
||||
raise_for_error=False,
|
||||
)
|
||||
user_count = Users.query.count()
|
||||
assert user_count == 2 # There's the admin user and the first created user
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_register_whitelisted_email():
|
||||
"""A user shouldn't be able to register with an email that isn't on the whitelist"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config(
|
||||
"domain_whitelist", "whitelisted.com, whitelisted.org, whitelisted.net"
|
||||
)
|
||||
register_user(
|
||||
app, name="not_whitelisted", email="user@nope.com", raise_for_error=False
|
||||
)
|
||||
assert Users.query.count() == 1
|
||||
|
||||
register_user(app, name="user1", email="user@whitelisted.com")
|
||||
assert Users.query.count() == 2
|
||||
|
||||
register_user(app, name="user2", email="user@whitelisted.org")
|
||||
assert Users.query.count() == 3
|
||||
|
||||
register_user(app, name="user3", email="user@whitelisted.net")
|
||||
assert Users.query.count() == 4
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_register_blacklisted_email():
|
||||
"""A user shouldn't be able to register with an email that is on the blacklist"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config(
|
||||
"domain_blacklist", "blacklisted.com, blacklisted.org, blacklisted.net"
|
||||
)
|
||||
register_user(
|
||||
app, name="blacklisted", email="user@blacklisted.com", raise_for_error=False
|
||||
)
|
||||
assert Users.query.count() == 1
|
||||
|
||||
register_user(app, name="user1", email="user@yep.com")
|
||||
assert Users.query.count() == 2
|
||||
|
||||
register_user(app, name="user2", email="user@yay.org")
|
||||
assert Users.query.count() == 3
|
||||
|
||||
register_user(app, name="user3", email="user@yipee.net")
|
||||
assert Users.query.count() == 4
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_bad_login():
|
||||
"""A user should not be able to login with an incorrect password"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(
|
||||
app, name="user", password="wrong_password", raise_for_error=False
|
||||
)
|
||||
with client.session_transaction() as sess:
|
||||
assert sess.get("id") is None
|
||||
r = client.get("/profile")
|
||||
assert r.location.startswith("/login") # We got redirected to login
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_login():
|
||||
"""Can a registered user can login"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/profile")
|
||||
assert r.location is None # We didn't get redirected to login
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_login_with_email():
|
||||
"""Can a registered user can login with an email address instead of a team name"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app, name="user@examplectf.com", password="password")
|
||||
r = client.get("/profile")
|
||||
assert r.location is None # We didn't get redirected to login
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_logout():
|
||||
"""Can a registered user load /logout"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
client.get("/logout", follow_redirects=True)
|
||||
r = client.get("/challenges")
|
||||
assert r.location == "/login?next=%2Fchallenges%3F"
|
||||
assert r.status_code == 302
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_isnt_admin():
|
||||
"""A registered user cannot access admin pages"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
for page in [
|
||||
"pages",
|
||||
"users",
|
||||
"teams",
|
||||
"scoreboard",
|
||||
"challenges",
|
||||
"statistics",
|
||||
"config",
|
||||
]:
|
||||
r = client.get("/admin/{}".format(page))
|
||||
assert r.location.startswith("/login?next=")
|
||||
assert r.status_code == 302
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_expired_confirmation_links():
|
||||
"""Test that expired confirmation links are reported to the user"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("mail_server", "localhost")
|
||||
set_config("mail_port", 25)
|
||||
set_config("mail_useauth", True)
|
||||
set_config("mail_username", "username")
|
||||
set_config("mail_password", "password")
|
||||
set_config("verify_emails", True)
|
||||
|
||||
register_user(app, email="user@user.com")
|
||||
client = login_as_user(app, name="user", password="password")
|
||||
|
||||
# user@user.com "2012-01-14 03:21:34"
|
||||
confirm_link = (
|
||||
"http://localhost/confirm/bb8a8526146e50778b211ae63074595880edbc0b"
|
||||
)
|
||||
r = client.get(confirm_link)
|
||||
|
||||
assert (
|
||||
"Your confirmation link is invalid, please generate a new one"
|
||||
in r.get_data(as_text=True)
|
||||
)
|
||||
user = Users.query.filter_by(email="user@user.com").first()
|
||||
assert user.verified is not True
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_invalid_confirmation_links():
|
||||
"""Test that invalid confirmation links are reported to the user"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("mail_server", "localhost")
|
||||
set_config("mail_port", 25)
|
||||
set_config("mail_useauth", True)
|
||||
set_config("mail_username", "username")
|
||||
set_config("mail_password", "password")
|
||||
set_config("verify_emails", True)
|
||||
|
||||
register_user(app, email="user@user.com")
|
||||
client = login_as_user(app, name="user", password="password")
|
||||
|
||||
# user@user.com "2012-01-14 03:21:34"
|
||||
confirm_link = "http://localhost/confirm/a8375iyu<script>alert(1)<script>hn3048wueorighkgnsfg"
|
||||
r = client.get(confirm_link)
|
||||
|
||||
assert (
|
||||
"Your confirmation link is invalid, please generate a new one"
|
||||
in r.get_data(as_text=True)
|
||||
)
|
||||
user = Users.query.filter_by(email="user@user.com").first()
|
||||
assert user.verified is not True
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_expired_reset_password_link():
|
||||
"""Test that expired reset password links are reported to the user"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("mail_server", "localhost")
|
||||
set_config("mail_port", 25)
|
||||
set_config("mail_useauth", True)
|
||||
set_config("mail_username", "username")
|
||||
set_config("mail_password", "password")
|
||||
|
||||
register_user(app, name="user1", email="user@user.com")
|
||||
|
||||
with app.test_client() as client:
|
||||
forgot_link = "http://localhost/reset_password/bb8a8526146e50778b211ae63074595880edbc0b"
|
||||
r = client.get(forgot_link)
|
||||
|
||||
assert (
|
||||
"Your reset link is invalid, please generate a new one"
|
||||
in r.get_data(as_text=True)
|
||||
)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_invalid_reset_password_link():
|
||||
"""Test that invalid reset password links are reported to the user"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("mail_server", "localhost")
|
||||
set_config("mail_port", 25)
|
||||
set_config("mail_useauth", True)
|
||||
set_config("mail_username", "username")
|
||||
set_config("mail_password", "password")
|
||||
|
||||
register_user(app, name="user1", email="user@user.com")
|
||||
|
||||
with app.test_client() as client:
|
||||
# user@user.com "2012-01-14 03:21:34"
|
||||
forgot_link = "http://localhost/reset_password/5678ytfghjiu876tyfg<INVALID DATA>hvbnmkoi9u87y6trdf"
|
||||
r = client.get(forgot_link)
|
||||
|
||||
assert (
|
||||
"Your reset link is invalid, please generate a new one"
|
||||
in r.get_data(as_text=True)
|
||||
)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_contact_for_password_reset():
|
||||
"""Test that if there is no mailserver configured, users should contact admins"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="user1", email="user@user.com")
|
||||
|
||||
with app.test_client() as client:
|
||||
forgot_link = "http://localhost/reset_password"
|
||||
r = client.get(forgot_link)
|
||||
|
||||
assert "contact an organizer" in r.get_data(as_text=True)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
@patch("smtplib.SMTP")
|
||||
def test_user_can_confirm_email(mock_smtp):
|
||||
"""Test that a user is capable of confirming their email address"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2012-01-14 03:21:34"):
|
||||
# Set CTFd to only allow confirmed users and send emails
|
||||
set_config("verify_emails", True)
|
||||
set_config("mail_server", "localhost")
|
||||
set_config("mail_port", 25)
|
||||
set_config("mail_useauth", True)
|
||||
set_config("mail_username", "username")
|
||||
set_config("mail_password", "password")
|
||||
|
||||
register_user(app, name="user1", email="user@user.com")
|
||||
|
||||
# Teams are not verified by default
|
||||
user = Users.query.filter_by(email="user@user.com").first()
|
||||
assert user.verified is False
|
||||
|
||||
client = login_as_user(app, name="user1", password="password")
|
||||
|
||||
r = client.get("/confirm")
|
||||
assert "We've sent a confirmation email" in r.get_data(as_text=True)
|
||||
|
||||
# smtp send message function was called
|
||||
mock_smtp.return_value.send_message.assert_called()
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
urandom_value = b"\xff" * 32
|
||||
with patch("os.urandom", return_value=urandom_value):
|
||||
data = {"nonce": sess.get("nonce")}
|
||||
r = client.post("http://localhost/confirm", data=data)
|
||||
assert "Confirmation email sent to" in r.get_data(as_text=True)
|
||||
|
||||
r = client.get("/challenges")
|
||||
assert r.location == "/confirm" # We got redirected to /confirm
|
||||
|
||||
r = client.get(
|
||||
"http://localhost/confirm/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
)
|
||||
assert r.location == "/challenges"
|
||||
|
||||
# The team is now verified
|
||||
user = Users.query.filter_by(email="user@user.com").first()
|
||||
assert user.verified is True
|
||||
|
||||
r = client.get("http://localhost/confirm")
|
||||
assert r.location == "/settings"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
@patch("smtplib.SMTP")
|
||||
def test_user_can_reset_password(mock_smtp):
|
||||
"""Test that a user is capable of resetting their password"""
|
||||
from email.message import EmailMessage
|
||||
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Set CTFd to send emails
|
||||
set_config("mail_server", "localhost")
|
||||
set_config("mail_port", 25)
|
||||
set_config("mail_useauth", True)
|
||||
set_config("mail_username", "username")
|
||||
set_config("mail_password", "password")
|
||||
|
||||
# Create a user
|
||||
register_user(app, name="user1", email="user@user.com")
|
||||
|
||||
with app.test_client() as client:
|
||||
client.get("/reset_password")
|
||||
|
||||
# Build reset password data
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "email": "user@user.com"}
|
||||
|
||||
# Issue the password reset request
|
||||
urandom_value = b"\xff" * 32
|
||||
with patch("os.urandom", return_value=urandom_value):
|
||||
client.post("/reset_password", data=data)
|
||||
|
||||
ctf_name = get_config("ctf_name")
|
||||
from_addr = get_config("mailfrom_addr") or app.config.get("MAILFROM_ADDR")
|
||||
from_addr = "{} <{}>".format(ctf_name, from_addr)
|
||||
|
||||
to_addr = "user@user.com"
|
||||
|
||||
# Build the email
|
||||
msg = (
|
||||
"Did you initiate a password reset on CTFd? If you didn't initiate this request you can ignore this email. "
|
||||
"\n\nClick the following link to reset your password:\n"
|
||||
"http://localhost/reset_password/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\n"
|
||||
"If the link is not clickable, try copying and pasting it into your browser."
|
||||
)
|
||||
ctf_name = get_config("ctf_name")
|
||||
|
||||
email_msg = EmailMessage()
|
||||
email_msg.set_content(msg)
|
||||
|
||||
email_msg["Subject"] = "Password Reset Request from {ctf_name}".format(
|
||||
ctf_name=ctf_name
|
||||
)
|
||||
email_msg["From"] = from_addr
|
||||
email_msg["To"] = to_addr
|
||||
|
||||
# Make sure that the reset password email is sent
|
||||
mock_smtp.return_value.send_message.assert_called()
|
||||
assert str(mock_smtp.return_value.send_message.call_args[0][0]) == str(
|
||||
email_msg
|
||||
)
|
||||
|
||||
# Get user's original password
|
||||
user = Users.query.filter_by(email="user@user.com").first()
|
||||
|
||||
# Build the POST data
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "password": "passwordtwo"}
|
||||
|
||||
# Do the password reset
|
||||
client.get(
|
||||
"/reset_password/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
)
|
||||
client.post(
|
||||
"/reset_password/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
data=data,
|
||||
)
|
||||
|
||||
# Make sure that the user's password changed
|
||||
user = Users.query.filter_by(email="user@user.com").first()
|
||||
assert verify_password("passwordtwo", user.password)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_banned_user():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
user.banned = True
|
||||
db.session.commit()
|
||||
|
||||
routes = ["/", "/challenges", "/api/v1/challenges"]
|
||||
for route in routes:
|
||||
r = client.get(route)
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_registration_code_required():
|
||||
"""
|
||||
Test that registration code configuration properly blocks logins
|
||||
with missing and incorrect registration codes
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Set a registration code
|
||||
set_config("registration_code", "secret-sauce")
|
||||
|
||||
with app.test_client() as client:
|
||||
# Load CSRF nonce
|
||||
r = client.get("/register")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "Registration Code" in resp
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user1@examplectf.com",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
# Attempt registration without password
|
||||
r = client.post("/register", data=data)
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "The registration code you entered was incorrect" in resp
|
||||
|
||||
# Attempt registration with wrong password
|
||||
data["registration_code"] = "wrong-sauce"
|
||||
r = client.post("/register", data=data)
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "The registration code you entered was incorrect" in resp
|
||||
|
||||
# Attempt registration with right password
|
||||
data["registration_code"] = "secret-sauce"
|
||||
r = client.post("/register", data=data)
|
||||
assert r.status_code == 302
|
||||
assert r.location.startswith("/challenges")
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_registration_code_allows_numeric():
|
||||
"""
|
||||
Test that registration code is allowed to be all numeric
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Set a registration code
|
||||
set_config("registration_code", "1234567890")
|
||||
|
||||
with app.test_client() as client:
|
||||
# Load CSRF nonce
|
||||
r = client.get("/register")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "Registration Code" in resp
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user1@examplectf.com",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
|
||||
# Attempt registration with numeric registration code
|
||||
data["registration_code"] = "1234567890"
|
||||
r = client.post("/register", data=data)
|
||||
assert r.status_code == 302
|
||||
assert r.location.startswith("/challenges")
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_registration_password_minimum_length():
|
||||
"""
|
||||
Test that registration enforces minimum password length when configured
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Set a minimum password length
|
||||
set_config("password_min_length", 8)
|
||||
|
||||
with app.test_client() as client:
|
||||
# Load CSRF nonce
|
||||
r = client.get("/register")
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user1@examplectf.com",
|
||||
"password": "short", # Only 5 characters
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
|
||||
# Attempt registration with password too short
|
||||
r = client.post("/register", data=data)
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "Password must be at least 8 characters" in resp
|
||||
assert r.status_code == 200 # Should stay on registration page
|
||||
|
||||
# Verify user was not created
|
||||
user_count = Users.query.count()
|
||||
assert user_count == 1 # Only admin user exists
|
||||
|
||||
# Attempt registration with password meeting minimum length
|
||||
data["password"] = "validpassword" # 13 characters, meets minimum
|
||||
r = client.post("/register", data=data)
|
||||
assert r.status_code == 302
|
||||
assert r.location.startswith("/challenges")
|
||||
|
||||
# Verify user was created
|
||||
user_count = Users.query.count()
|
||||
assert user_count == 2 # Admin user + new user
|
||||
|
||||
# Test with minimum length set to 0 (disabled)
|
||||
set_config("password_min_length", 0)
|
||||
|
||||
with app.test_client() as client:
|
||||
# Load CSRF nonce
|
||||
r = client.get("/register")
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user2",
|
||||
"email": "user2@examplectf.com",
|
||||
"password": "x", # Only 1 character
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
|
||||
# Should allow short password when minimum length is 0
|
||||
r = client.post("/register", data=data)
|
||||
assert r.status_code == 302
|
||||
assert r.location.startswith("/challenges")
|
||||
|
||||
# Verify user was created
|
||||
user_count = Users.query.count()
|
||||
assert user_count == 3 # Admin user + 2 new users
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_change_password_required():
|
||||
"""
|
||||
Test that users with change_password=True are redirected to reset password
|
||||
and cannot access other pages until they change their password
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Create a user with change_password=True
|
||||
register_user(
|
||||
app, name="testuser", email="test@example.com", password="oldpassword"
|
||||
)
|
||||
user = Users.query.filter_by(name="testuser").first()
|
||||
user.change_password = True
|
||||
db.session.commit()
|
||||
|
||||
with app.test_client() as client:
|
||||
# Login as the user
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "testuser",
|
||||
"password": "oldpassword",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
|
||||
# Get login page first to get nonce
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
data["nonce"] = sess.get("nonce")
|
||||
|
||||
# Login
|
||||
r = client.post("/login", data=data)
|
||||
assert r.status_code == 302
|
||||
|
||||
# Test that user is redirected to reset_password when accessing various pages
|
||||
protected_routes = [
|
||||
"/",
|
||||
"/challenges",
|
||||
"/scoreboard",
|
||||
"/profile",
|
||||
"/settings",
|
||||
"/api/v1/challenges",
|
||||
]
|
||||
|
||||
for route in protected_routes:
|
||||
r = client.get(route, follow_redirects=False)
|
||||
# Should be redirected to reset_password with a token
|
||||
assert r.status_code == 302
|
||||
assert "/reset_password/" in r.location
|
||||
|
||||
# Test that the user can access the reset_password page directly
|
||||
r = client.get(route, follow_redirects=True)
|
||||
# Should end up on reset_password page
|
||||
final_url = r.request.path
|
||||
assert "/reset_password/" in final_url
|
||||
|
||||
# Get redirected to reset password page with token
|
||||
r = client.get("/challenges", follow_redirects=False)
|
||||
assert r.status_code == 302
|
||||
assert "/reset_password/" in r.location
|
||||
|
||||
# Extract the token from the redirect URL
|
||||
reset_url = r.location
|
||||
token = reset_url.split("/reset_password/")[-1]
|
||||
|
||||
# Access the reset password page with the token
|
||||
r = client.get(f"/reset_password/{token}")
|
||||
assert r.status_code == 200
|
||||
|
||||
# Actually reset the password using the reset password form
|
||||
with client.session_transaction() as sess:
|
||||
reset_data = {"password": "newpassword123", "nonce": sess.get("nonce")}
|
||||
|
||||
# Submit the password reset form
|
||||
r = client.post(f"/reset_password/{token}", data=reset_data)
|
||||
assert r.status_code == 302 # Should redirect after successful reset
|
||||
|
||||
# Verify the password was actually changed and change_password flag was cleared
|
||||
user = Users.query.filter_by(name="testuser").first()
|
||||
assert user.change_password is False
|
||||
assert verify_password("newpassword123", user.password)
|
||||
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "testuser",
|
||||
"password": "newpassword123",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
|
||||
r = client.post("/login", data=data)
|
||||
assert r.status_code == 302
|
||||
|
||||
# Now user should be able to access protected routes normally
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
assert r.location is None # No redirect
|
||||
|
||||
r = client.get("/profile")
|
||||
assert r.status_code == 200
|
||||
assert r.location is None # No redirect
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_admin_can_set_change_password_via_api():
|
||||
"""
|
||||
Test that admins can set the change_password attribute via the API
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Login as admin
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
# Create a user via API with change_password=True
|
||||
r = client.post(
|
||||
"/api/v1/users",
|
||||
json={
|
||||
"name": "apiuser",
|
||||
"email": "apiuser@example.com",
|
||||
"password": "password123",
|
||||
"change_password": True,
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
# Verify the user was created with change_password=True
|
||||
response_data = r.get_json()
|
||||
assert response_data["success"] is True
|
||||
user_id = response_data["data"]["id"]
|
||||
|
||||
user = Users.query.filter_by(id=user_id).first()
|
||||
assert user is not None
|
||||
assert user.change_password is True
|
||||
|
||||
# Update user via API to set change_password=False
|
||||
r = client.patch(f"/api/v1/users/{user_id}", json={"change_password": False})
|
||||
assert r.status_code == 200
|
||||
|
||||
# Verify the change_password was updated
|
||||
user = Users.query.filter_by(id=user_id).first()
|
||||
assert user.change_password is False
|
||||
|
||||
# Test that non-admin users cannot set change_password via API
|
||||
register_user(app, name="normaluser", email="normal@example.com")
|
||||
normal_client = login_as_user(app, name="normaluser", password="password")
|
||||
|
||||
# Try to modify change_password on their own account (should fail)
|
||||
normal_user = Users.query.filter_by(name="normaluser").first()
|
||||
r = normal_client.patch(
|
||||
f"/api/v1/users/{normal_user.id}",
|
||||
json={
|
||||
"change_password": True,
|
||||
},
|
||||
)
|
||||
# Normal users shouldn't be able to modify change_password field
|
||||
assert r.status_code == 403 # Forbidden
|
||||
|
||||
# Verify that change_password was not modified
|
||||
normal_user = Users.query.filter_by(name="normaluser").first()
|
||||
assert normal_user.change_password is False
|
||||
|
||||
# Also test via the /me endpoint
|
||||
r = normal_client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={
|
||||
"change_password": True,
|
||||
},
|
||||
)
|
||||
# Request goes through but doesn't actually modify the attribute
|
||||
assert r.status_code == 200
|
||||
|
||||
# Verify that change_password was still not modified
|
||||
normal_user = Users.query.filter_by(name="normaluser").first()
|
||||
assert normal_user.change_password is False
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
@patch("smtplib.SMTP")
|
||||
def test_user_reset_password_rate_limit(mock_smtp):
|
||||
"""
|
||||
Test that a user can only create 5 reset password attempts before they are rate limited
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Create a user before configuring mail to not send any extra emails
|
||||
register_user(app, name="user1", email="user@user.com")
|
||||
|
||||
# Set CTFd to send emails
|
||||
set_config("mail_server", "localhost")
|
||||
set_config("mail_port", 25)
|
||||
set_config("mail_useauth", True)
|
||||
set_config("mail_username", "username")
|
||||
set_config("mail_password", "password")
|
||||
|
||||
with app.test_client() as client:
|
||||
# Make 5 password reset requests (which should all succeed)
|
||||
for _ in range(5):
|
||||
client.get("/reset_password")
|
||||
|
||||
# Build reset password data
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "email": "user@user.com"}
|
||||
|
||||
# Issue the password reset request
|
||||
r = client.post("/reset_password", data=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_data(as_text=True)
|
||||
assert (
|
||||
"If that account exists you will receive an email, please check your inbox"
|
||||
in resp
|
||||
)
|
||||
assert "Too many password reset attempts" not in resp
|
||||
|
||||
# Verify that the email was sent 5 times
|
||||
assert mock_smtp.return_value.send_message.call_count == 5
|
||||
|
||||
# 6th attempt should be rate limited
|
||||
client.get("/reset_password")
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "email": "user@user.com"}
|
||||
|
||||
r = client.post("/reset_password", data=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "Too many password reset attempts. Please try again later." in resp
|
||||
|
||||
# Verify that no additional email was sent
|
||||
assert mock_smtp.return_value.send_message.call_count == 5
|
||||
|
||||
destroy_ctfd(app)
|
||||
950
tests/users/test_challenges.py
Normal file
950
tests/users/test_challenges.py
Normal file
@@ -0,0 +1,950 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from CTFd.models import Challenges, Fails, Ratelimiteds, Solves
|
||||
from CTFd.utils import set_config, text_type
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_fail,
|
||||
gen_flag,
|
||||
gen_hint,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_user_get_challenges():
|
||||
"""
|
||||
Can a registered user load /challenges
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_chals():
|
||||
"""
|
||||
Can a registered user load /chals
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_viewing_challenges():
|
||||
"""
|
||||
Test that users can see added challenges
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
gen_challenge(app.db)
|
||||
r = client.get("/api/v1/challenges")
|
||||
chals = r.get_json()["data"]
|
||||
assert len(chals) == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_viewing_challenge():
|
||||
"""Test that users can see individual challenges"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
gen_challenge(app.db)
|
||||
r = client.get("/api/v1/challenges/1")
|
||||
assert r.get_json()
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
# def test_chals_solves():
|
||||
# """Test that the /chals/solves endpoint works properly"""
|
||||
# app = create_ctfd()
|
||||
# with app.app_context():
|
||||
# # Generate 5 users
|
||||
# for c in range(1, 6):
|
||||
# name = "user{}".format(c)
|
||||
# email = "user{}@examplectf.com".format(c)
|
||||
# register_user(app, name=name, email=email, password="password")
|
||||
#
|
||||
# # Generate 5 challenges
|
||||
# for c in range(6):
|
||||
# chal1 = gen_challenge(app.db, value=100)
|
||||
#
|
||||
# user_ids = list(range(2, 7))
|
||||
# chal_ids = list(range(1, 6))
|
||||
# for u in user_ids:
|
||||
# for c in chal_ids:
|
||||
# gen_solve(app.db, teamid=u, chalid=c)
|
||||
# chal_ids.pop()
|
||||
#
|
||||
# client = login_as_user(app, name="user1")
|
||||
#
|
||||
# with client.session_transaction() as sess:
|
||||
# r = client.get('/chals/solves')
|
||||
# output = r.get_data(as_text=True)
|
||||
# saved = json.loads('''{
|
||||
# "1": 5,
|
||||
# "2": 4,
|
||||
# "3": 3,
|
||||
# "4": 2,
|
||||
# "5": 1,
|
||||
# "6": 0
|
||||
# }
|
||||
# ''')
|
||||
# received = json.loads(output)
|
||||
# assert saved == received
|
||||
# set_config('hide_scores', True)
|
||||
# with client.session_transaction():
|
||||
# r = client.get('/chals/solves')
|
||||
# output = r.get_data(as_text=True)
|
||||
# saved = json.loads('''{
|
||||
# "1": -1,
|
||||
# "2": -1,
|
||||
# "3": -1,
|
||||
# "4": -1,
|
||||
# "5": -1,
|
||||
# "6": -1
|
||||
# }
|
||||
# ''')
|
||||
# received = json.loads(output)
|
||||
# assert saved == received
|
||||
# destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_submitting_correct_flag():
|
||||
"""Test that correct flags are correct"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
data = {"submission": "flag", "challenge_id": chal.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "correct"
|
||||
assert resp.get("message") == "Correct"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_submitting_correct_static_case_insensitive_flag():
|
||||
"""Test that correct static flags are correct if the static flag is marked case_insensitive"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag", data="case_insensitive")
|
||||
data = {"submission": "FLAG", "challenge_id": chal.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "correct"
|
||||
assert resp.get("message") == "Correct"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_submitting_correct_regex_case_insensitive_flag():
|
||||
"""Test that correct regex flags are correct if the regex flag is marked case_insensitive"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
gen_flag(
|
||||
app.db,
|
||||
challenge_id=chal.id,
|
||||
type="regex",
|
||||
content="flag",
|
||||
data="case_insensitive",
|
||||
)
|
||||
data = {"submission": "FLAG", "challenge_id": chal.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "correct"
|
||||
assert resp.get("message") == "Correct"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_submitting_invalid_regex_flag():
|
||||
"""Test that invalid regex flags are errored out to the user"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
gen_flag(
|
||||
app.db,
|
||||
challenge_id=chal.id,
|
||||
type="regex",
|
||||
content="**",
|
||||
data="case_insensitive",
|
||||
)
|
||||
data = {"submission": "FLAG", "challenge_id": chal.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "incorrect"
|
||||
assert resp.get("message") == "Regex parse error occured"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_submitting_incorrect_flag():
|
||||
"""Test that incorrect flags are incorrect"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
data = {"submission": "notflag", "challenge_id": chal.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "incorrect"
|
||||
assert resp.get("message") == "Incorrect"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_submitting_unicode_flag():
|
||||
"""Test that users can submit a unicode flag"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal.id, content="你好")
|
||||
with client.session_transaction():
|
||||
data = {"submission": "你好", "challenge_id": chal.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "correct"
|
||||
assert resp.get("message") == "Correct"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenges_with_max_attempts():
|
||||
"""Test that users are locked out of a challenge after they reach max_attempts"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal = Challenges.query.filter_by(id=chal.id).first()
|
||||
chal_id = chal.id
|
||||
chal.max_attempts = 3
|
||||
app.db.session.commit()
|
||||
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
for _ in range(3):
|
||||
data = {"submission": "notflag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
wrong_keys = Fails.query.count()
|
||||
assert wrong_keys == 3
|
||||
|
||||
data = {"submission": "flag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 403
|
||||
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "ratelimited"
|
||||
assert resp.get("message") == "Not accepted. You have 0 tries remaining"
|
||||
|
||||
solves = Solves.query.count()
|
||||
assert solves == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenges_with_max_attempts_timeout_behavior():
|
||||
"""Test that users are temporarily locked out of a challenge after reaching max_attempts with timeout behavior"""
|
||||
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("max_attempts_behavior", "timeout")
|
||||
set_config("max_attempts_timeout", 300) # 300 seconds timeout for test
|
||||
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal = Challenges.query.filter_by(id=chal.id).first()
|
||||
chal_id = chal.id
|
||||
chal.max_attempts = 2
|
||||
app.db.session.commit()
|
||||
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
for _ in range(2):
|
||||
data = {"submission": "notflag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
|
||||
# Should be locked out now
|
||||
with freeze_time(timedelta(seconds=0)):
|
||||
data = {"submission": "flag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 429
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "ratelimited"
|
||||
assert "Not accepted. Try again in 300 seconds" in resp.get("message")
|
||||
|
||||
# Use freeze_time to advance time by 290 seconds
|
||||
with freeze_time(timedelta(seconds=290)):
|
||||
data = {"submission": "flag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 429
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "ratelimited"
|
||||
assert "Not accepted. Try again in 10 seconds" in resp.get("message")
|
||||
|
||||
# Use freeze_time to advance time by 301 seconds
|
||||
with freeze_time(timedelta(seconds=301)):
|
||||
data = {"submission": "flag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
# Should be correct now
|
||||
assert resp.get("status") == "correct"
|
||||
assert resp.get("message") == "Correct"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenges_with_max_attempts_timeout_ratelimit():
|
||||
"""Test that max_attempts timeout ratelimit and global ratelimit work together correctly"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("max_attempts_behavior", "timeout")
|
||||
set_config("max_attempts_timeout", 30) # 30 seconds timeout for test
|
||||
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
|
||||
# Challenge 1 with max_attempts = 5
|
||||
chal1 = gen_challenge(app.db)
|
||||
chal1_obj = Challenges.query.filter_by(id=chal1.id).first()
|
||||
chal1_obj.max_attempts = 5
|
||||
app.db.session.commit()
|
||||
gen_flag(app.db, challenge_id=chal1.id, content="flag1")
|
||||
|
||||
# Challenge 2 with no max_attempts
|
||||
chal2 = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal2.id, content="flag2")
|
||||
|
||||
base_time = datetime.utcnow()
|
||||
|
||||
# Submit 5 wrong attempts to challenge 1 (triggers max_attempts ratelimit)
|
||||
with freeze_time(base_time):
|
||||
for _ in range(5):
|
||||
data = {"submission": "wrong", "challenge_id": chal1.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
|
||||
# 6th attempt should be blocked by max_attempts timeout
|
||||
data = {"submission": "flag1", "challenge_id": chal1.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 429
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "ratelimited"
|
||||
assert "Try again in 30 seconds" in resp.get("message")
|
||||
|
||||
# Now submit 5 more wrong attempts to challenge 2 (total 10 fails, triggers global ratelimit)
|
||||
for i in range(6):
|
||||
data = {"submission": "wrong", "challenge_id": chal2.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
if i < 5:
|
||||
assert r.status_code == 200
|
||||
else:
|
||||
# 11th attempt should be blocked by global ratelimit (60 seconds)
|
||||
assert r.status_code == 429
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "ratelimited"
|
||||
assert "You're submitting flags too fast" in resp.get("message")
|
||||
|
||||
# Check counts
|
||||
wrong_keys = Fails.query.count()
|
||||
ratelimiteds = Ratelimiteds.query.count()
|
||||
assert wrong_keys == 10
|
||||
assert (
|
||||
ratelimiteds == 2
|
||||
) # One max_attempts ratelimit + one global ratelimit
|
||||
|
||||
# After 30 seconds, max_attempts timeout should release but global ratelimit (60s) still active
|
||||
with freeze_time(base_time + timedelta(seconds=31)):
|
||||
# Try challenge 1 - should still be blocked by global ratelimit
|
||||
data = {"submission": "flag1", "challenge_id": chal1.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 429
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "ratelimited"
|
||||
assert "Try again in 30 seconds" in resp.get("message")
|
||||
|
||||
ratelimiteds = Ratelimiteds.query.count()
|
||||
assert ratelimiteds == 3 # Another ratelimit entry
|
||||
|
||||
# After 60 seconds, both ratelimits should be released
|
||||
with freeze_time(base_time + timedelta(seconds=61)):
|
||||
# Should be able to solve challenge 1 now
|
||||
data = {"submission": "flag1", "challenge_id": chal1.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "correct"
|
||||
assert resp.get("message") == "Correct"
|
||||
|
||||
# Verify solve was recorded
|
||||
solves = Solves.query.count()
|
||||
assert solves == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenges_max_attempts_timeout_config_change():
|
||||
"""Test that changing max_attempts_timeout resets attempt count because cache key changes"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("max_attempts_behavior", "timeout")
|
||||
set_config("max_attempts_timeout", 300) # Start with 300 seconds
|
||||
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal = Challenges.query.filter_by(id=chal.id).first()
|
||||
chal_id = chal.id
|
||||
chal.max_attempts = 3
|
||||
app.db.session.commit()
|
||||
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
|
||||
# Make 3 wrong attempts (hit the limit)
|
||||
for _ in range(3):
|
||||
data = {"submission": "notflag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
|
||||
# Verify we're locked out
|
||||
data = {"submission": "flag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 429
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "ratelimited"
|
||||
assert "300 seconds" in resp.get("message")
|
||||
|
||||
# Change the max_attempts_timeout config (this changes the cache key)
|
||||
set_config("max_attempts_timeout", 30) # Change to 30 seconds
|
||||
|
||||
# Jump forward in time to ensure we're past the new 30 second rate limit
|
||||
with freeze_time(timedelta(seconds=35)):
|
||||
# Now we should be able to submit again because the cache key changed
|
||||
# Old submissions have also fallen off the 30 second window
|
||||
data = {"submission": "flag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "correct"
|
||||
assert resp.get("message") == "Correct"
|
||||
|
||||
# Verify solve was recorded
|
||||
solves = Solves.query.count()
|
||||
assert solves == 1
|
||||
|
||||
# Verify the fail count is still accurate (3 fails from before)
|
||||
fails = Fails.query.count()
|
||||
assert fails == 3
|
||||
|
||||
ratelimiteds = Ratelimiteds.query.count()
|
||||
assert ratelimiteds == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenge_kpm_limit_no_freeze():
|
||||
"""Test that users are properly ratelimited when submitting flags"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
for _ in range(11):
|
||||
with client.session_transaction():
|
||||
data = {"submission": "notflag", "challenge_id": chal_id}
|
||||
client.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
wrong_keys = Fails.query.count()
|
||||
ratelimiteds = Ratelimiteds.query.count()
|
||||
assert wrong_keys == 10
|
||||
assert ratelimiteds == 1
|
||||
|
||||
# We just want a consistent flag response countdown
|
||||
with freeze_time(timedelta(seconds=0)):
|
||||
data = {"submission": "flag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 429
|
||||
|
||||
wrong_keys = Fails.query.count()
|
||||
ratelimiteds = Ratelimiteds.query.count()
|
||||
assert wrong_keys == 10
|
||||
assert ratelimiteds == 2
|
||||
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "ratelimited"
|
||||
assert (
|
||||
resp.get("message")
|
||||
== "You're submitting flags too fast. Try again in 60 seconds."
|
||||
)
|
||||
|
||||
solves = Solves.query.count()
|
||||
assert solves == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenge_kpm_limit_freeze_time():
|
||||
"""Test that users are properly ratelimited when submitting flags"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
base_time = datetime.utcnow()
|
||||
|
||||
# First section: Use API to generate 10 fails + 1 ratelimit
|
||||
with freeze_time(base_time):
|
||||
for _ in range(11):
|
||||
with client.session_transaction():
|
||||
data = {"submission": "notflag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
wrong_keys = Fails.query.count()
|
||||
ratelimiteds = Ratelimiteds.query.count()
|
||||
assert wrong_keys == 10
|
||||
assert ratelimiteds == 1
|
||||
|
||||
# Within the 1 min time frame we should still be ratelimited
|
||||
with freeze_time(base_time + timedelta(seconds=11)):
|
||||
data = {"submission": "flag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 429
|
||||
|
||||
wrong_keys = Fails.query.count()
|
||||
ratelimiteds = Ratelimiteds.query.count()
|
||||
assert wrong_keys == 10
|
||||
assert ratelimiteds == 2
|
||||
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "ratelimited"
|
||||
assert (
|
||||
resp.get("message")
|
||||
== "You're submitting flags too fast. Try again in 50 seconds."
|
||||
)
|
||||
|
||||
# Generate 10 more fails at +60 seconds using gen_fail because freezegun cannot patch to sqlalchemy's default
|
||||
for _ in range(10):
|
||||
fail = gen_fail(app.db, user_id=2, challenge_id=chal_id, provided="notflag")
|
||||
fail.date = base_time + timedelta(seconds=60)
|
||||
app.db.session.commit()
|
||||
|
||||
# The 11th attempt via API should trigger another ratelimit
|
||||
with freeze_time(base_time + timedelta(seconds=60)):
|
||||
data = {"submission": "notflag", "challenge_id": chal_id}
|
||||
client.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
wrong_keys = Fails.query.count()
|
||||
ratelimiteds = Ratelimiteds.query.count()
|
||||
assert wrong_keys == 20
|
||||
assert ratelimiteds == 3
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_that_view_challenges_unregistered_works():
|
||||
"""Test that view_challenges_unregistered works"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
chal = gen_challenge(app.db, name=text_type("🐺"))
|
||||
chal_id = chal.id
|
||||
gen_hint(app.db, chal_id)
|
||||
|
||||
client = app.test_client()
|
||||
r = client.get("/api/v1/challenges", json="")
|
||||
assert r.status_code == 403
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 302
|
||||
|
||||
set_config("challenge_visibility", "public")
|
||||
|
||||
client = app.test_client()
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.get_json()["data"]
|
||||
|
||||
r = client.get("/api/v1/challenges/1/solves")
|
||||
assert r.get_json().get("data") is not None
|
||||
|
||||
data = {"submission": "not_flag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 403
|
||||
assert r.get_json().get("data").get("status") == "authentication_required"
|
||||
assert r.get_json().get("data").get("message") is None
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_hidden_challenge_is_unreachable():
|
||||
"""Test that hidden challenges return 404 and do not insert a solve or wrong key"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db, state="hidden")
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
chal_id = chal.id
|
||||
|
||||
assert Challenges.query.count() == 1
|
||||
|
||||
r = client.get("/api/v1/challenges", json="")
|
||||
data = r.get_json().get("data")
|
||||
assert data == []
|
||||
|
||||
r = client.get("/api/v1/challenges/1", json="")
|
||||
assert r.status_code == 404
|
||||
data = r.get_json().get("data")
|
||||
assert data is None
|
||||
|
||||
data = {"submission": "flag", "challenge_id": chal_id}
|
||||
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 404
|
||||
|
||||
r = client.post("/api/v1/challenges/attempt?preview=true", json=data)
|
||||
assert r.status_code == 404
|
||||
assert r.get_json().get("data") is None
|
||||
|
||||
solves = Solves.query.count()
|
||||
assert solves == 0
|
||||
|
||||
wrong_keys = Fails.query.count()
|
||||
assert wrong_keys == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_hidden_challenge_is_unsolveable():
|
||||
"""Test that hidden challenges return 404 and do not insert a solve or wrong key"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db, state="hidden")
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
|
||||
data = {"submission": "flag", "challenge_id": chal.id}
|
||||
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 404
|
||||
|
||||
solves = Solves.query.count()
|
||||
assert solves == 0
|
||||
|
||||
wrong_keys = Fails.query.count()
|
||||
assert wrong_keys == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_invalid_requirements_are_rejected():
|
||||
"""Test that invalid requirements JSON blobs are rejected by the API"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
gen_challenge(app.db)
|
||||
with login_as_user(app, "admin") as client:
|
||||
# Test None/null values
|
||||
r = client.patch(
|
||||
"/api/v1/challenges/1", json={"requirements": {"prerequisites": [None]}}
|
||||
)
|
||||
assert r.status_code == 400
|
||||
assert r.get_json() == {
|
||||
"success": False,
|
||||
"errors": {
|
||||
"requirements": [
|
||||
"Challenge requirements cannot have a null prerequisite"
|
||||
]
|
||||
},
|
||||
}
|
||||
# Test empty strings
|
||||
r = client.patch(
|
||||
"/api/v1/challenges/1", json={"requirements": {"prerequisites": [""]}}
|
||||
)
|
||||
assert r.status_code == 400
|
||||
assert r.get_json() == {
|
||||
"success": False,
|
||||
"errors": {
|
||||
"requirements": [
|
||||
"Challenge requirements cannot have a null prerequisite"
|
||||
]
|
||||
},
|
||||
}
|
||||
# Test a valid integer
|
||||
r = client.patch(
|
||||
"/api/v1/challenges/1", json={"requirements": {"prerequisites": [2]}}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenge_with_requirements_is_unsolveable():
|
||||
"""Test that a challenge with a requirement is unsolveable without first solving the requirement"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal1 = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal1.id, content="flag")
|
||||
|
||||
requirements = {"prerequisites": [1]}
|
||||
chal2 = gen_challenge(app.db, requirements=requirements)
|
||||
app.db.session.commit()
|
||||
|
||||
gen_flag(app.db, challenge_id=chal2.id, content="flag")
|
||||
|
||||
r = client.get("/api/v1/challenges")
|
||||
challenges = r.get_json()["data"]
|
||||
assert len(challenges) == 1
|
||||
assert challenges[0]["id"] == 1
|
||||
|
||||
r = client.get("/api/v1/challenges/2")
|
||||
assert r.status_code == 403
|
||||
assert r.get_json().get("data") is None
|
||||
|
||||
# Attempt to solve hidden Challenge 2
|
||||
data = {"submission": "flag", "challenge_id": 2}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 403
|
||||
assert r.get_json().get("data") is None
|
||||
|
||||
# Solve Challenge 1
|
||||
data = {"submission": "flag", "challenge_id": 1}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
resp = r.get_json()["data"]
|
||||
assert resp["status"] == "correct"
|
||||
|
||||
# Challenge 2 should now be visible
|
||||
r = client.get("/api/v1/challenges")
|
||||
challenges = r.get_json()["data"]
|
||||
assert len(challenges) == 2
|
||||
|
||||
r = client.get("/api/v1/challenges/2")
|
||||
assert r.status_code == 200
|
||||
assert r.get_json().get("data")["id"] == 2
|
||||
|
||||
# Attempt to solve the now-visible Challenge 2
|
||||
data = {"submission": "flag", "challenge_id": 2}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
assert resp["status"] == "correct"
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenges_cannot_be_solved_while_paused():
|
||||
"""Test that challenges cannot be solved when the CTF is paused"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("paused", True)
|
||||
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
|
||||
# Assert that there is a paused message
|
||||
data = r.get_data(as_text=True)
|
||||
assert "paused" in data
|
||||
|
||||
chal = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
|
||||
data = {"submission": "flag", "challenge_id": chal.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
# Assert that the JSON message is correct
|
||||
resp = r.get_json()["data"]
|
||||
assert r.status_code == 403
|
||||
assert resp["status"] == "paused"
|
||||
assert resp["message"] == "CTFd is paused"
|
||||
|
||||
# There are no solves saved
|
||||
solves = Solves.query.count()
|
||||
assert solves == 0
|
||||
|
||||
# There are no wrong keys saved
|
||||
wrong_keys = Fails.query.count()
|
||||
assert wrong_keys == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenge_board_under_view_after_ctf():
|
||||
"""Test that the challenge board does not show an error under view_after_ctf"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("view_after_ctf", True)
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
|
||||
gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=1, content="flag")
|
||||
|
||||
gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=2, content="flag")
|
||||
|
||||
# CTF hasn't started yet. There should be an error message.
|
||||
with freeze_time("2017-10-3"):
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 403
|
||||
assert "has not started yet" in r.get_data(as_text=True)
|
||||
|
||||
data = {"submission": "flag", "challenge_id": 2}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 403
|
||||
assert Solves.query.count() == 0
|
||||
|
||||
# CTF is ongoing. Normal operation.
|
||||
with freeze_time("2017-10-5"):
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
assert "has ended" not in r.get_data(as_text=True)
|
||||
|
||||
data = {"submission": "flag", "challenge_id": 1}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"]["status"] == "correct"
|
||||
assert Solves.query.count() == 1
|
||||
|
||||
# CTF is now over. There should be a message and challenges should show submission status but not store solves
|
||||
with freeze_time("2017-10-7"):
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
assert "has ended" in r.get_data(as_text=True)
|
||||
|
||||
data = {"submission": "flag", "challenge_id": 2}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"]["status"] == "correct"
|
||||
assert Solves.query.count() == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenges_under_view_after_ctf():
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-7"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
|
||||
gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=1, content="flag")
|
||||
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 403
|
||||
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 403
|
||||
assert r.get_json().get("data") is None
|
||||
|
||||
r = client.get("/api/v1/challenges/1")
|
||||
assert r.status_code == 403
|
||||
assert r.get_json().get("data") is None
|
||||
|
||||
data = {"submission": "flag", "challenge_id": 1}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 403
|
||||
assert r.get_json().get("data") is None
|
||||
assert Solves.query.count() == 0
|
||||
|
||||
data = {"submission": "notflag", "challenge_id": 1}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 403
|
||||
assert r.get_json().get("data") is None
|
||||
assert Fails.query.count() == 0
|
||||
|
||||
set_config("view_after_ctf", True)
|
||||
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"][0]["id"] == 1
|
||||
|
||||
r = client.get("/api/v1/challenges/1")
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"]["id"] == 1
|
||||
|
||||
data = {"submission": "flag", "challenge_id": 1}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"]["status"] == "correct"
|
||||
assert Solves.query.count() == 0
|
||||
|
||||
data = {"submission": "notflag", "challenge_id": 1}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"]["status"] == "incorrect"
|
||||
assert Fails.query.count() == 0
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenges_admin_only_as_user():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("challenge_visibility", "admins")
|
||||
|
||||
register_user(app)
|
||||
admin = login_as_user(app, name="admin")
|
||||
|
||||
gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=1, content="flag")
|
||||
|
||||
r = admin.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
|
||||
r = admin.get("/api/v1/challenges", json="")
|
||||
assert r.status_code == 200
|
||||
|
||||
r = admin.get("/api/v1/challenges/1", json="")
|
||||
assert r.status_code == 200
|
||||
|
||||
data = {"submission": "flag", "challenge_id": 1}
|
||||
r = admin.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
319
tests/users/test_fields.py
Normal file
319
tests/users/test_fields.py
Normal file
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import UserFieldEntries
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_field,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_new_fields_show_on_pages():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
|
||||
gen_field(app.db)
|
||||
|
||||
with app.test_client() as client:
|
||||
r = client.get("/register")
|
||||
assert "CustomField" in r.get_data(as_text=True)
|
||||
assert "CustomFieldDescription" in r.get_data(as_text=True)
|
||||
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/settings")
|
||||
assert "CustomField" in r.get_data(as_text=True)
|
||||
assert "CustomFieldDescription" in r.get_data(as_text=True)
|
||||
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={"fields": [{"field_id": 1, "value": "CustomFieldEntry"}]},
|
||||
)
|
||||
resp = r.get_json()
|
||||
assert resp["success"] is True
|
||||
assert resp["data"]["fields"][0]["value"] == "CustomFieldEntry"
|
||||
assert resp["data"]["fields"][0]["description"] == "CustomFieldDescription"
|
||||
assert resp["data"]["fields"][0]["name"] == "CustomField"
|
||||
assert resp["data"]["fields"][0]["field_id"] == 1
|
||||
|
||||
r = client.get("/user")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField" in resp
|
||||
assert "CustomFieldEntry" in resp
|
||||
|
||||
r = client.get("/users/2")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField" in resp
|
||||
assert "CustomFieldEntry" in resp
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_fields_required_on_register():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_field(app.db)
|
||||
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
client.get("/register")
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
client.post("/register", data=data)
|
||||
with client.session_transaction() as sess:
|
||||
assert sess.get("id") is None
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"password": "password",
|
||||
"fields[1]": "custom_field_value",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
client.post("/register", data=data)
|
||||
with client.session_transaction() as sess:
|
||||
assert sess["id"]
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_fields_properties():
|
||||
"""Test that users can set and edit custom fields"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_field(
|
||||
app.db, name="CustomField1", required=True, public=True, editable=True
|
||||
)
|
||||
gen_field(
|
||||
app.db, name="CustomField2", required=False, public=True, editable=True
|
||||
)
|
||||
gen_field(
|
||||
app.db, name="CustomField3", required=False, public=False, editable=True
|
||||
)
|
||||
gen_field(
|
||||
app.db, name="CustomField4", required=False, public=False, editable=False
|
||||
)
|
||||
|
||||
with app.test_client() as client:
|
||||
r = client.get("/register")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" in resp
|
||||
assert "CustomField4" in resp
|
||||
|
||||
# Manually register user so that we can populate the required field
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"password": "password",
|
||||
"fields[1]": "custom_field_value",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
client.post("/register", data=data)
|
||||
with client.session_transaction() as sess:
|
||||
assert sess["id"]
|
||||
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/settings")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" in resp
|
||||
assert "CustomField4" not in resp
|
||||
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={
|
||||
"fields": [
|
||||
{"field_id": 1, "value": "CustomFieldEntry1"},
|
||||
{"field_id": 2, "value": "CustomFieldEntry2"},
|
||||
{"field_id": 3, "value": "CustomFieldEntry3"},
|
||||
{"field_id": 4, "value": "CustomFieldEntry4"},
|
||||
]
|
||||
},
|
||||
)
|
||||
resp = r.get_json()
|
||||
assert resp == {
|
||||
"success": False,
|
||||
"errors": {"fields": ["Field 'CustomField4' cannot be editted"]},
|
||||
}
|
||||
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={
|
||||
"fields": [
|
||||
{"field_id": 1, "value": "CustomFieldEntry1"},
|
||||
{"field_id": 2, "value": "CustomFieldEntry2"},
|
||||
{"field_id": 3, "value": "CustomFieldEntry3"},
|
||||
]
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
r = client.get("/user")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" not in resp
|
||||
assert "CustomField4" not in resp
|
||||
|
||||
r = client.get("/users/2")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" not in resp
|
||||
assert "CustomField4" not in resp
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_boolean_checkbox_field():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_field(app.db, name="CustomField1", field_type="boolean", required=False)
|
||||
|
||||
with app.test_client() as client:
|
||||
r = client.get("/register")
|
||||
resp = r.get_data(as_text=True)
|
||||
|
||||
# We should have rendered a checkbox input
|
||||
assert "checkbox" in resp
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
"fields[1]": "y",
|
||||
}
|
||||
client.post("/register", data=data)
|
||||
with client.session_transaction() as sess:
|
||||
assert sess["id"]
|
||||
|
||||
assert UserFieldEntries.query.count() == 1
|
||||
assert UserFieldEntries.query.filter_by(id=1).first().value is True
|
||||
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/settings")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "checkbox" in resp
|
||||
|
||||
r = client.patch(
|
||||
"/api/v1/users/me", json={"fields": [{"field_id": 1, "value": False}]}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert UserFieldEntries.query.count() == 1
|
||||
assert UserFieldEntries.query.filter_by(id=1).first().value is False
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_needs_all_required_fields():
|
||||
"""Test that users need to submit all required fields before viewing challenges"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Manually create a user who has no fields set
|
||||
register_user(app)
|
||||
|
||||
# Create the fields that we want
|
||||
gen_field(
|
||||
app.db, name="CustomField1", required=True, public=True, editable=True
|
||||
)
|
||||
gen_field(
|
||||
app.db, name="CustomField2", required=False, public=True, editable=True
|
||||
)
|
||||
gen_field(
|
||||
app.db, name="CustomField3", required=False, public=False, editable=True
|
||||
)
|
||||
gen_field(
|
||||
app.db, name="CustomField4", required=False, public=False, editable=False
|
||||
)
|
||||
|
||||
# We can see all fields when we try to register
|
||||
with app.test_client() as client:
|
||||
r = client.get("/register")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" in resp
|
||||
assert "CustomField4" in resp
|
||||
|
||||
# When we login with our manually made user
|
||||
# we should see all fields because we are missing a required field
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/settings")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" in resp
|
||||
assert "CustomField4" in resp
|
||||
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 302
|
||||
assert r.location.startswith("/settings")
|
||||
|
||||
# Populate the non-required fields
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={
|
||||
"fields": [
|
||||
{"field_id": 2, "value": "CustomFieldEntry2"},
|
||||
{"field_id": 3, "value": "CustomFieldEntry3"},
|
||||
{"field_id": 4, "value": "CustomFieldEntry4"},
|
||||
]
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
# I should still be restricted from seeing challenges
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 302
|
||||
assert r.location.startswith("/settings")
|
||||
|
||||
# I should still see all fields b/c I don't have a complete profile
|
||||
r = client.get("/settings")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" in resp
|
||||
assert "CustomField4" in resp
|
||||
|
||||
# Populate the required fields
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={"fields": [{"field_id": 1, "value": "CustomFieldEntry1"}]},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
# I can now go to challenges
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
|
||||
# I should only see edittable fields
|
||||
r = client.get("/settings")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" in resp
|
||||
assert "CustomField4" not in resp
|
||||
|
||||
# I can't edit a non-editable field
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={"fields": [{"field_id": 4, "value": "CustomFieldEntry4"}]},
|
||||
)
|
||||
resp = r.get_json()
|
||||
assert resp == {
|
||||
"success": False,
|
||||
"errors": {"fields": ["Field 'CustomField4' cannot be editted"]},
|
||||
}
|
||||
destroy_ctfd(app)
|
||||
289
tests/users/test_hints.py
Normal file
289
tests/users/test_hints.py
Normal file
@@ -0,0 +1,289 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from CTFd.models import Unlocks, Users, db
|
||||
from CTFd.utils import set_config, text_type
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_award,
|
||||
gen_challenge,
|
||||
gen_flag,
|
||||
gen_hint,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_user_cannot_unlock_hint():
|
||||
"""Test that a user can't unlock a hint if they don't have enough points"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client():
|
||||
register_user(app, name="user1", email="user1@examplectf.com")
|
||||
|
||||
chal = gen_challenge(app.db, value=100)
|
||||
chal_id = chal.id
|
||||
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
|
||||
hint = gen_hint(db, chal_id, cost=10)
|
||||
hint_id = hint.id
|
||||
|
||||
client = login_as_user(app, name="user1", password="password")
|
||||
|
||||
with client.session_transaction():
|
||||
r = client.get("/api/v1/hints/{}".format(hint_id))
|
||||
resp = r.get_json()
|
||||
assert resp["data"].get("content") is None
|
||||
assert resp["data"].get("cost") == 10
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_can_unlock_hint():
|
||||
"""Test that a user can unlock a hint if they have enough points"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client():
|
||||
register_user(app, name="user1", email="user1@examplectf.com")
|
||||
|
||||
chal = gen_challenge(app.db, value=100)
|
||||
chal_id = chal.id
|
||||
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
|
||||
hint = gen_hint(app.db, chal_id, cost=10)
|
||||
hint_id = hint.id
|
||||
|
||||
gen_award(app.db, user_id=2, value=15)
|
||||
|
||||
client = login_as_user(app, name="user1", password="password")
|
||||
|
||||
user = Users.query.filter_by(name="user1").first()
|
||||
assert user.score == 15
|
||||
|
||||
with client.session_transaction():
|
||||
r = client.get("/api/v1/hints/{}".format(hint_id))
|
||||
resp = r.get_json()
|
||||
assert resp["data"].get("content") is None
|
||||
|
||||
params = {"target": hint_id, "type": "hints"}
|
||||
|
||||
r = client.post("/api/v1/unlocks", json=params)
|
||||
resp = r.get_json()
|
||||
assert resp["success"] is True
|
||||
|
||||
r = client.get("/api/v1/hints/{}".format(hint_id))
|
||||
resp = r.get_json()
|
||||
assert resp["data"].get("content") == "This is a hint"
|
||||
|
||||
user = Users.query.filter_by(name="user1").first()
|
||||
assert user.score == 5
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_unlocking_hints_with_no_cost():
|
||||
"""Test that hints with no cost can be unlocked"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
gen_hint(app.db, chal_id)
|
||||
client = login_as_user(app)
|
||||
# Attempt to access hint
|
||||
r = client.get("/api/v1/hints/1")
|
||||
resp = r.get_json()["data"]
|
||||
|
||||
# Hint does not provide content until an unlock is generated
|
||||
assert resp.get("content") is None
|
||||
|
||||
# We generate an unlock for the free hint
|
||||
client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
|
||||
# We should now be able to see content
|
||||
r = client.get("/api/v1/hints/1")
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("content") == "This is a hint"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_unlocking_hints_with_cost_during_ctf_with_points():
|
||||
"""Test that hints with a cost are unlocked if you have the points"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
gen_hint(app.db, chal_id, cost=10)
|
||||
gen_award(app.db, user_id=2)
|
||||
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.get_json()["data"].get("content") is None
|
||||
|
||||
client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.get_json()["data"].get("content") == "This is a hint"
|
||||
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.score == 90
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_unlocking_hints_with_cost_during_ctf_without_points():
|
||||
"""Test that hints with a cost are not unlocked if you don't have the points"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
gen_hint(app.db, chal_id, cost=10)
|
||||
|
||||
client = login_as_user(app)
|
||||
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.get_json()["data"].get("content") is None
|
||||
|
||||
r = client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
assert (
|
||||
r.get_json()["errors"]["score"]
|
||||
== "You do not have enough points to unlock this hint"
|
||||
)
|
||||
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.get_json()["data"].get("content") is None
|
||||
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.score == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_unlocking_hints_with_cost_before_ctf():
|
||||
"""Test that hints are not unlocked if the CTF hasn't begun"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
gen_hint(app.db, chal_id)
|
||||
gen_award(app.db, user_id=2)
|
||||
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
|
||||
with freeze_time("2017-10-1"):
|
||||
client = login_as_user(app)
|
||||
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 403
|
||||
assert r.get_json().get("data") is None
|
||||
|
||||
r = client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
assert r.status_code == 403
|
||||
assert r.get_json().get("data") is None
|
||||
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.get_json().get("data") is None
|
||||
assert r.status_code == 403
|
||||
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
|
||||
assert user.score == 100
|
||||
assert Unlocks.query.count() == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_unlocking_hints_with_cost_during_ended_ctf():
|
||||
"""Test that hints with a cost are not unlocked if the CTF has ended"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
gen_hint(app.db, chal_id, cost=10)
|
||||
gen_award(app.db, user_id=2)
|
||||
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
|
||||
with freeze_time("2017-11-4"):
|
||||
client = login_as_user(app)
|
||||
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.get_json().get("data") is None
|
||||
assert r.status_code == 403
|
||||
|
||||
r = client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
assert r.status_code == 403
|
||||
assert r.get_json()
|
||||
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 403
|
||||
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.score == 100
|
||||
assert Unlocks.query.count() == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_unlocking_hints_with_cost_during_frozen_ctf():
|
||||
"""Test that hints with a cost are unlocked if the CTF is frozen."""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config(
|
||||
"freeze", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
with freeze_time("2017-10-4"):
|
||||
register_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
gen_hint(app.db, chal_id, cost=10)
|
||||
gen_award(app.db, user_id=2)
|
||||
|
||||
with freeze_time("2017-10-8"):
|
||||
client = login_as_user(app)
|
||||
|
||||
client.get("/api/v1/hints/1")
|
||||
|
||||
client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
|
||||
r = client.get("/api/v1/hints/1")
|
||||
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("content") == "This is a hint"
|
||||
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.score == 100
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_unlocking_hint_for_unicode_challenge():
|
||||
"""Test that hints for challenges with unicode names can be unlocked"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
chal = gen_challenge(app.db, name=text_type("🐺"))
|
||||
chal_id = chal.id
|
||||
gen_hint(app.db, chal_id)
|
||||
|
||||
client = login_as_user(app)
|
||||
|
||||
# Generate an unlock for the free hint
|
||||
client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("content") == "This is a hint"
|
||||
destroy_ctfd(app)
|
||||
37
tests/users/test_profile.py
Normal file
37
tests/users/test_profile.py
Normal file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Users
|
||||
from CTFd.utils.crypto import verify_password
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user
|
||||
|
||||
|
||||
def test_email_cannot_be_changed_without_password():
|
||||
"""Test that a user can't update their email address without current password"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
|
||||
data = {"name": "user", "email": "user2@examplectf.com"}
|
||||
|
||||
r = client.patch("/api/v1/users/me", json=data)
|
||||
assert r.status_code == 400
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.email == "user@examplectf.com"
|
||||
|
||||
data = {"name": "user", "email": "user2@examplectf.com", "confirm": "asdf"}
|
||||
|
||||
r = client.patch("/api/v1/users/me", json=data)
|
||||
assert r.status_code == 400
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.email == "user@examplectf.com"
|
||||
|
||||
data = {"name": "user", "email": "user2@examplectf.com", "confirm": "password"}
|
||||
|
||||
r = client.patch("/api/v1/users/me", json=data)
|
||||
assert r.status_code == 200
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.email == "user2@examplectf.com"
|
||||
assert verify_password(plaintext="password", ciphertext=user.password)
|
||||
destroy_ctfd(app)
|
||||
365
tests/users/test_scoreboard.py
Normal file
365
tests/users/test_scoreboard.py
Normal file
@@ -0,0 +1,365 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from CTFd.models import Users
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_award,
|
||||
gen_bracket,
|
||||
gen_challenge,
|
||||
gen_flag,
|
||||
gen_solve,
|
||||
get_scores,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_user_get_scoreboard_components():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
|
||||
# test_user_get_scoreboard
|
||||
"""Can a registered user load scoreboard components"""
|
||||
r = client.get("/scoreboard")
|
||||
assert r.status_code == 200
|
||||
|
||||
# test_user_get_scores
|
||||
"""Can a registered user load /api/v1/scoreboard"""
|
||||
r = client.get("/api/v1/scoreboard")
|
||||
assert r.status_code == 200
|
||||
|
||||
# test_user_get_topteams
|
||||
"""Can a registered user load /api/v1/scoreboard/top/10"""
|
||||
r = client.get("/api/v1/scoreboard/top/10")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_score_is_correct():
|
||||
"""Test that a user's score is correct"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# create user1
|
||||
register_user(app, name="user1", email="user1@examplectf.com")
|
||||
|
||||
# create challenge
|
||||
chal = gen_challenge(app.db, value=100)
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
chal_id = chal.id
|
||||
|
||||
# create a solve for the challenge for user1. (the id is 2 because of the admin)
|
||||
gen_solve(app.db, user_id=2, challenge_id=chal_id)
|
||||
user1 = Users.query.filter_by(id=2).first()
|
||||
|
||||
# assert that user1's score is 100
|
||||
assert user1.score == 100
|
||||
assert user1.place == "1st"
|
||||
|
||||
# create user2
|
||||
register_user(app, name="user2", email="user2@examplectf.com")
|
||||
|
||||
# user2 solves the challenge
|
||||
gen_solve(app.db, 3, challenge_id=chal_id)
|
||||
|
||||
# assert that user2's score is 100 but is in 2nd place
|
||||
user2 = Users.query.filter_by(id=3).first()
|
||||
assert user2.score == 100
|
||||
assert user2.place == "2nd"
|
||||
|
||||
# create an award for user2
|
||||
gen_award(app.db, user_id=3, value=5)
|
||||
|
||||
# assert that user2's score is now 105 and is in 1st place
|
||||
assert user2.score == 105
|
||||
assert user2.place == "1st"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_top_10():
|
||||
"""Make sure top10 returns correct information"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_bracket(app.db, name="players1")
|
||||
gen_bracket(app.db, name="players2")
|
||||
register_user(app, name="user1", email="user1@examplectf.com", bracket_id=1)
|
||||
register_user(app, name="user2", email="user2@examplectf.com", bracket_id=2)
|
||||
register_user(app, bracket_id=1)
|
||||
|
||||
chal1 = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal1.id, content="flag")
|
||||
chal1_id = chal1.id
|
||||
|
||||
chal2 = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal2.id, content="flag")
|
||||
chal2_id = chal2.id
|
||||
|
||||
# Generates solve for user1
|
||||
with freeze_time("2017-10-3 03:21:34"):
|
||||
gen_solve(app.db, user_id=2, challenge_id=chal1_id)
|
||||
|
||||
with freeze_time("2017-10-4 03:25:45"):
|
||||
gen_solve(app.db, user_id=2, challenge_id=chal2_id)
|
||||
|
||||
# Generate solve for user2
|
||||
with freeze_time("2017-10-3 03:21:34"):
|
||||
gen_solve(app.db, user_id=3, challenge_id=chal1_id)
|
||||
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/scoreboard/top/10")
|
||||
response = r.get_json()["data"]
|
||||
|
||||
saved = {
|
||||
"1": {
|
||||
"id": 2,
|
||||
"account_url": "/users/2",
|
||||
"name": "user1",
|
||||
"score": 200,
|
||||
"bracket_id": 1,
|
||||
"bracket_name": "players1",
|
||||
"solves": [
|
||||
{
|
||||
"date": "2017-10-03T03:21:34Z",
|
||||
"challenge_id": 1,
|
||||
"account_id": 2,
|
||||
"user_id": 2,
|
||||
"team_id": None,
|
||||
"value": 100,
|
||||
},
|
||||
{
|
||||
"date": "2017-10-04T03:25:45Z",
|
||||
"challenge_id": 2,
|
||||
"account_id": 2,
|
||||
"user_id": 2,
|
||||
"team_id": None,
|
||||
"value": 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
"2": {
|
||||
"id": 3,
|
||||
"account_url": "/users/3",
|
||||
"name": "user2",
|
||||
"score": 100,
|
||||
"bracket_id": 2,
|
||||
"bracket_name": "players2",
|
||||
"solves": [
|
||||
{
|
||||
"date": "2017-10-03T03:21:34Z",
|
||||
"challenge_id": 1,
|
||||
"account_id": 3,
|
||||
"user_id": 3,
|
||||
"team_id": None,
|
||||
"value": 100,
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
assert saved == response
|
||||
|
||||
r = client.get("/api/v1/scoreboard/top/10?bracket_id=2")
|
||||
response = r.get_json()["data"]
|
||||
saved = {
|
||||
"1": {
|
||||
"id": 3,
|
||||
"account_url": "/users/3",
|
||||
"name": "user2",
|
||||
"score": 100,
|
||||
"bracket_id": 2,
|
||||
"bracket_name": "players2",
|
||||
"solves": [
|
||||
{
|
||||
"date": "2017-10-03T03:21:34Z",
|
||||
"challenge_id": 1,
|
||||
"account_id": 3,
|
||||
"user_id": 3,
|
||||
"team_id": None,
|
||||
"value": 100,
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
assert saved == response
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_scoring_logic():
|
||||
"""Test that scoring logic is correct"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
admin = login_as_user(app, name="admin", password="password")
|
||||
|
||||
register_user(
|
||||
app, name="user1", email="user1@examplectf.com", password="password"
|
||||
)
|
||||
client1 = login_as_user(app, name="user1", password="password")
|
||||
register_user(
|
||||
app, name="user2", email="user2@examplectf.com", password="password"
|
||||
)
|
||||
client2 = login_as_user(app, name="user2", password="password")
|
||||
|
||||
chal1 = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal1.id, content="flag")
|
||||
chal1_id = chal1.id
|
||||
|
||||
chal2 = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal2.id, content="flag")
|
||||
chal2_id = chal2.id
|
||||
|
||||
# user1 solves chal1
|
||||
with freeze_time("2017-10-3 03:21:34"):
|
||||
with client1.session_transaction():
|
||||
data = {"submission": "flag", "challenge_id": chal1_id}
|
||||
client1.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
# user1 is now on top
|
||||
scores = get_scores(admin)
|
||||
assert scores[0]["name"] == "user1"
|
||||
|
||||
# user2 solves chal1 and chal2
|
||||
with freeze_time("2017-10-4 03:30:34"):
|
||||
with client2.session_transaction():
|
||||
# solve chal1
|
||||
data = {"submission": "flag", "challenge_id": chal1_id}
|
||||
client2.post("/api/v1/challenges/attempt", json=data)
|
||||
# solve chal2
|
||||
data = {"submission": "flag", "challenge_id": chal2_id}
|
||||
client2.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
# user2 is now on top
|
||||
scores = get_scores(admin)
|
||||
assert scores[0]["name"] == "user2"
|
||||
|
||||
# user1 solves chal2
|
||||
with freeze_time("2017-10-5 03:50:34"):
|
||||
with client1.session_transaction():
|
||||
data = {"submission": "flag", "challenge_id": chal2_id}
|
||||
client1.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
# user2 should still be on top because they solved chal2 first
|
||||
scores = get_scores(admin)
|
||||
assert scores[0]["name"] == "user2"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_scoring_logic_with_zero_point_challenges():
|
||||
"""Test that scoring logic is correct with zero point challenges. Zero point challenges should not tie break"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
admin = login_as_user(app, name="admin", password="password")
|
||||
|
||||
register_user(
|
||||
app, name="user1", email="user1@examplectf.com", password="password"
|
||||
)
|
||||
client1 = login_as_user(app, name="user1", password="password")
|
||||
register_user(
|
||||
app, name="user2", email="user2@examplectf.com", password="password"
|
||||
)
|
||||
client2 = login_as_user(app, name="user2", password="password")
|
||||
|
||||
chal1 = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal1.id, content="flag")
|
||||
chal1_id = chal1.id
|
||||
|
||||
chal2 = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal2.id, content="flag")
|
||||
chal2_id = chal2.id
|
||||
|
||||
# A 0 point challenge shouldn't influence the scoreboard (see #577)
|
||||
chal0 = gen_challenge(app.db, value=0)
|
||||
gen_flag(app.db, challenge_id=chal0.id, content="flag")
|
||||
chal0_id = chal0.id
|
||||
|
||||
# user1 solves chal1
|
||||
with freeze_time("2017-10-3 03:21:34"):
|
||||
with client1.session_transaction():
|
||||
data = {"submission": "flag", "challenge_id": chal1_id}
|
||||
client1.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
# user1 is now on top
|
||||
scores = get_scores(admin)
|
||||
assert scores[0]["name"] == "user1"
|
||||
|
||||
# user2 solves chal1 and chal2
|
||||
with freeze_time("2017-10-4 03:30:34"):
|
||||
with client2.session_transaction():
|
||||
# solve chal1
|
||||
data = {"submission": "flag", "challenge_id": chal1_id}
|
||||
client2.post("/api/v1/challenges/attempt", json=data)
|
||||
# solve chal2
|
||||
data = {"submission": "flag", "challenge_id": chal2_id}
|
||||
client2.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
# user2 is now on top
|
||||
scores = get_scores(admin)
|
||||
assert scores[0]["name"] == "user2"
|
||||
|
||||
# user1 solves chal2
|
||||
with freeze_time("2017-10-5 03:50:34"):
|
||||
with client1.session_transaction():
|
||||
data = {"submission": "flag", "challenge_id": chal2_id}
|
||||
client1.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
# user2 should still be on top because they solved chal2 first
|
||||
scores = get_scores(admin)
|
||||
assert scores[0]["name"] == "user2"
|
||||
|
||||
# user2 solves a 0 point challenge
|
||||
with freeze_time("2017-10-5 03:55:34"):
|
||||
with client2.session_transaction():
|
||||
data = {"submission": "flag", "challenge_id": chal0_id}
|
||||
client2.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
# user2 should still be on top because 0 point challenges should not tie break
|
||||
scores = get_scores(admin)
|
||||
assert scores[0]["name"] == "user2"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_hidden_users_should_not_influence_scores():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(
|
||||
app, name="user1", email="user1@examplectf.com", password="password"
|
||||
)
|
||||
register_user(
|
||||
app, name="user2", email="user2@examplectf.com", password="password"
|
||||
)
|
||||
register_user(
|
||||
app, name="user3", email="user3@examplectf.com", password="password"
|
||||
)
|
||||
|
||||
user = Users.query.filter_by(name="user3").first()
|
||||
user.hidden = True
|
||||
app.db.session.commit()
|
||||
|
||||
client1 = login_as_user(app, name="user1", password="password")
|
||||
|
||||
# User 1 solves 1st challenge
|
||||
chal1 = gen_challenge(app.db)
|
||||
gen_solve(app.db, user_id=2, challenge_id=chal1.id)
|
||||
|
||||
# User 2 solves 2nd challenge
|
||||
chal2 = gen_challenge(app.db)
|
||||
gen_solve(app.db, user_id=3, challenge_id=chal2.id)
|
||||
|
||||
# User 3 solves both
|
||||
gen_solve(app.db, user_id=4, challenge_id=chal1.id)
|
||||
gen_solve(app.db, user_id=4, challenge_id=chal2.id)
|
||||
|
||||
scores = get_scores(client1)
|
||||
|
||||
for entry in scores:
|
||||
assert entry["name"] != "user3"
|
||||
|
||||
user1 = Users.query.filter_by(name="user1").first()
|
||||
assert user1.place == "1st"
|
||||
|
||||
user2 = Users.query.filter_by(name="user2").first()
|
||||
assert user2.place == "2nd"
|
||||
destroy_ctfd(app)
|
||||
101
tests/users/test_settings.py
Normal file
101
tests/users/test_settings.py
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Users
|
||||
from CTFd.utils.crypto import verify_password
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user
|
||||
|
||||
|
||||
def test_user_set_profile():
|
||||
"""Test that a user can set and remove their information in their profile"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"confirm": "",
|
||||
"password": "",
|
||||
"affiliation": "affiliation_test",
|
||||
"website": "https://examplectf.com",
|
||||
"country": "US",
|
||||
}
|
||||
|
||||
r = client.patch("/api/v1/users/me", json=data)
|
||||
assert r.status_code == 200
|
||||
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.affiliation == data["affiliation"]
|
||||
assert user.website == data["website"]
|
||||
assert user.country == data["country"]
|
||||
|
||||
r = client.get("/settings")
|
||||
resp = r.get_data(as_text=True)
|
||||
for _k, v in data.items():
|
||||
assert v in resp
|
||||
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"confirm": "",
|
||||
"password": "",
|
||||
"affiliation": "",
|
||||
"website": "",
|
||||
"country": "",
|
||||
}
|
||||
|
||||
r = client.patch("/api/v1/users/me", json=data)
|
||||
assert r.status_code == 200
|
||||
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.affiliation == data["affiliation"]
|
||||
assert user.website == data["website"]
|
||||
assert user.country == data["country"]
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_can_change_password():
|
||||
"""Test that a user can change their password and is prompted properly"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"confirm": "",
|
||||
"password": "new_password",
|
||||
"affiliation": "",
|
||||
"website": "",
|
||||
"country": "",
|
||||
}
|
||||
|
||||
r = client.patch("/api/v1/users/me", json=data)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert verify_password(data["password"], user.password) is False
|
||||
assert r.status_code == 400
|
||||
assert r.get_json() == {
|
||||
"errors": {"confirm": ["Please confirm your current password"]},
|
||||
"success": False,
|
||||
}
|
||||
|
||||
data["confirm"] = "wrong_password"
|
||||
|
||||
r = client.patch("/api/v1/users/me", json=data)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert verify_password(data["password"], user.password) is False
|
||||
assert r.status_code == 400
|
||||
assert r.get_json() == {
|
||||
"errors": {"confirm": ["Your previous password is incorrect"]},
|
||||
"success": False,
|
||||
}
|
||||
|
||||
data["confirm"] = "password"
|
||||
r = client.patch("/api/v1/users/me", json=data)
|
||||
assert r.status_code == 200
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert verify_password(data["password"], user.password) is True
|
||||
destroy_ctfd(app)
|
||||
56
tests/users/test_setup.py
Normal file
56
tests/users/test_setup.py
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, gen_user
|
||||
|
||||
|
||||
def test_ctfd_setup_redirect():
|
||||
"""Test that a fresh CTFd instance redirects to /setup"""
|
||||
app = create_ctfd(setup=False)
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/users")
|
||||
assert r.status_code == 302
|
||||
assert r.location == "/setup"
|
||||
|
||||
# Files in /themes load properly
|
||||
r = client.get("/themes/core/static/manifest.json")
|
||||
r = client.get("/themes/core/static/img/favicon.ico")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_ctfd_setup_verification():
|
||||
app = create_ctfd(setup=False)
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/setup")
|
||||
assert r.status_code == 200
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"ctf_name": "CTFd",
|
||||
"ctf_description": "CTF description",
|
||||
"name": "test",
|
||||
"email": "test@examplectf.com",
|
||||
"password": "",
|
||||
"user_mode": "users",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/setup", data=data)
|
||||
assert "longer password" in r.get_data(as_text=True)
|
||||
|
||||
gen_user(app.db, name="test", email="test@examplectf.com")
|
||||
|
||||
data["password"] = "password"
|
||||
r = client.post("/setup", data=data)
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "email has already been used" in resp
|
||||
assert "name is already taken" in resp
|
||||
|
||||
data["name"] = "admin"
|
||||
data["email"] = "admin@examplectf.com"
|
||||
r = client.post("/setup", data=data)
|
||||
assert r.status_code == 302
|
||||
assert r.location == "/"
|
||||
destroy_ctfd(app)
|
||||
105
tests/users/test_submissions.py
Normal file
105
tests/users/test_submissions.py
Normal file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user
|
||||
|
||||
|
||||
def test_user_get_private_solves():
|
||||
"""Can a registered user load /api/v1/users/me/solves"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/users/me/solves")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_public_solves():
|
||||
"""Can a registered user load /api/v1/users/1/solves"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/users/2/solves")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_another_public_solves():
|
||||
"""Can a registered user load public solves page of another user"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="user1", email="user1@examplectf.com") # ID 2
|
||||
register_user(app, name="user2", email="user2@examplectf.com") # ID 3
|
||||
client = login_as_user(app, name="user2")
|
||||
r = client.get("/api/v1/users/2/solves")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_private_fails():
|
||||
"""Can a registered user load /api/v1/users/me/fails"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/users/me/fails")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_public_fails():
|
||||
"""Can a registered user load /api/v1/users/2/fails"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/users/2/fails")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_another_public_fails():
|
||||
"""Can a registered user load public fails page of another user"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="user1", email="user1@examplectf.com") # ID 2
|
||||
register_user(app, name="user2", email="user2@examplectf.com") # ID 3
|
||||
client = login_as_user(app, name="user2")
|
||||
r = client.get("/api/v1/users/2/fails")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_public_team_page():
|
||||
"""Can a registered user load their public profile (/profile)"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/profile")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_another_public_team_page():
|
||||
"""Can a registered user load the public profile of another user (/users/1)"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="user1", email="user1@examplectf.com") # ID 2
|
||||
register_user(app, name="user2", email="user2@examplectf.com") # ID 3
|
||||
client = login_as_user(app, name="user2")
|
||||
r = client.get("/users/2")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_private_team_page():
|
||||
"""Can a registered user load their private team page /user"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/user")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
151
tests/users/test_users.py
Normal file
151
tests/users/test_users.py
Normal file
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Users
|
||||
from CTFd.utils import set_config
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_award,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_accessing_hidden_users():
|
||||
"""Hidden users should not give any data from /users or /api/v1/users"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(
|
||||
app, name="visible_user", email="visible_user@examplectf.com"
|
||||
) # ID 2
|
||||
register_user(
|
||||
app, name="hidden_user", email="hidden_user@examplectf.com"
|
||||
) # ID 3
|
||||
register_user(
|
||||
app, name="banned_user", email="banned_user@examplectf.com"
|
||||
) # ID 4
|
||||
user = Users.query.filter_by(name="hidden_user").first()
|
||||
user.hidden = True
|
||||
app.db.session.commit()
|
||||
user = Users.query.filter_by(name="banned_user").first()
|
||||
user.banned = True
|
||||
app.db.session.commit()
|
||||
|
||||
with login_as_user(app, name="visible_user") as client:
|
||||
assert client.get("/users/3").status_code == 404
|
||||
assert client.get("/api/v1/users/3").status_code == 404
|
||||
assert client.get("/api/v1/users/3/solves").status_code == 404
|
||||
assert client.get("/api/v1/users/3/fails").status_code == 404
|
||||
assert client.get("/api/v1/users/3/awards").status_code == 404
|
||||
|
||||
assert client.get("/users/4").status_code == 404
|
||||
assert client.get("/api/v1/users/4").status_code == 404
|
||||
assert client.get("/api/v1/users/4/solves").status_code == 404
|
||||
assert client.get("/api/v1/users/4/fails").status_code == 404
|
||||
assert client.get("/api/v1/users/4/awards").status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_hidden_user_visibility():
|
||||
"""Hidden users should not show up on /users or /api/v1/users or /api/v1/scoreboard"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="hidden_user")
|
||||
|
||||
with login_as_user(app, name="hidden_user") as client:
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
user_id = user.id
|
||||
user_name = user.name
|
||||
user.hidden = True
|
||||
app.db.session.commit()
|
||||
|
||||
r = client.get("/users")
|
||||
response = r.get_data(as_text=True)
|
||||
# Only search in body content
|
||||
body_start = response.find("<body>")
|
||||
body_end = response.find("</body>")
|
||||
response = response[body_start:body_end]
|
||||
assert user_name not in response
|
||||
|
||||
r = client.get("/api/v1/users")
|
||||
response = r.get_json()
|
||||
assert user_name not in response
|
||||
|
||||
gen_award(app.db, user_id)
|
||||
|
||||
r = client.get("/scoreboard")
|
||||
response = r.get_data(as_text=True)
|
||||
# Only search in body content
|
||||
body_start = response.find("<body>")
|
||||
body_end = response.find("</body>")
|
||||
response = response[body_start:body_end]
|
||||
assert user_name not in response
|
||||
|
||||
r = client.get("/api/v1/scoreboard")
|
||||
response = r.get_json()
|
||||
assert user_name not in response
|
||||
|
||||
# User should re-appear after disabling hiding
|
||||
# Use an API call to cause a cache clear
|
||||
with login_as_user(app, name="admin") as admin:
|
||||
r = admin.patch("/api/v1/users/2", json={"hidden": False})
|
||||
assert r.status_code == 200
|
||||
|
||||
r = client.get("/users")
|
||||
response = r.get_data(as_text=True)
|
||||
# Only search in body content
|
||||
body_start = response.find("<body>")
|
||||
body_end = response.find("</body>")
|
||||
response = response[body_start:body_end]
|
||||
assert user_name in response
|
||||
|
||||
r = client.get("/api/v1/users")
|
||||
response = r.get_data(as_text=True)
|
||||
assert user_name in response
|
||||
|
||||
r = client.get("/api/v1/scoreboard")
|
||||
response = r.get_data(as_text=True)
|
||||
assert user_name in response
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_num_users_limit():
|
||||
"""Only num_users users can be created"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("num_users", 1)
|
||||
|
||||
register_user(app)
|
||||
with app.test_client() as client:
|
||||
r = client.get("/register")
|
||||
assert r.status_code == 403
|
||||
|
||||
# team should be blocked from creation
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/register", data=data)
|
||||
resp = r.get_data(as_text=True)
|
||||
# This number is 2 to account for the admin and the registered user
|
||||
assert Users.query.count() == 2
|
||||
assert "Reached the maximum number of users" in resp
|
||||
|
||||
# Can the team be created after the num has been bumped
|
||||
set_config("num_users", 2)
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user1",
|
||||
"email": "user1@examplectf.com",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/register", data=data)
|
||||
resp = r.get_data(as_text=True)
|
||||
assert r.status_code == 302
|
||||
assert Users.query.count() == 3
|
||||
destroy_ctfd(app)
|
||||
24
tests/utils/__init__.py
Normal file
24
tests/utils/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.utils import get_config, set_config
|
||||
from tests.helpers import create_ctfd, destroy_ctfd
|
||||
|
||||
|
||||
def test_ctf_version_is_set():
|
||||
"""Does ctf_version get set correctly"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
assert get_config("ctf_version") == app.VERSION
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_get_config_and_set_config():
|
||||
"""Does get_config and set_config work properly"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
assert get_config("setup") == True
|
||||
config = set_config("TEST_CONFIG_ENTRY", "test_config_entry")
|
||||
assert config.value == "test_config_entry"
|
||||
assert get_config("TEST_CONFIG_ENTRY") == "test_config_entry"
|
||||
destroy_ctfd(app)
|
||||
239
tests/utils/test_ctftime.py
Normal file
239
tests/utils/test_ctftime.py
Normal file
@@ -0,0 +1,239 @@
|
||||
from datetime import datetime as DateTime
|
||||
from datetime import timezone as TimeZone
|
||||
|
||||
import pytest
|
||||
|
||||
from CTFd.models import Solves
|
||||
from CTFd.utils.dates import (
|
||||
ctf_ended,
|
||||
ctf_started,
|
||||
isoformat,
|
||||
unix_time,
|
||||
unix_time_millis,
|
||||
unix_time_to_utc,
|
||||
)
|
||||
from CTFd.utils.modes import TEAMS_MODE
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
ctftime,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_flag,
|
||||
gen_team,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_ctftime_prevents_accessing_challenges_before_ctf():
|
||||
"""Test that the ctftime function prevents users from accessing challenges before the ctf"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with ctftime.init():
|
||||
register_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
|
||||
with ctftime.not_started():
|
||||
client = login_as_user(app)
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 403
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {"key": "flag", "nonce": sess.get("nonce")}
|
||||
r = client.get("/api/v1/challenges/{}".format(chal_id), data=data)
|
||||
data = r.get_data(as_text=True)
|
||||
assert r.status_code == 403
|
||||
solve_count = app.db.session.query(app.db.func.count(Solves.id)).first()[0]
|
||||
assert solve_count == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_ctftime_redirects_to_teams_page_in_teams_mode_before_ctf():
|
||||
"""
|
||||
Test that the ctftime function redirects users to the team creation page in teams mode before the ctf if the user
|
||||
has no team yet.
|
||||
"""
|
||||
app = create_ctfd(user_mode=TEAMS_MODE)
|
||||
with app.app_context():
|
||||
with ctftime.init():
|
||||
register_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
|
||||
with ctftime.not_started():
|
||||
client = login_as_user(app)
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 302
|
||||
|
||||
gen_team(app.db, name="test", password="password")
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/teams/join")
|
||||
assert r.status_code == 200
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "test",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/teams/join", data=data)
|
||||
assert r.status_code == 302
|
||||
|
||||
with ctftime.not_started():
|
||||
client = login_as_user(app)
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_ctftime_allows_accessing_challenges_during_ctf():
|
||||
"""Test that the ctftime function allows accessing challenges during the ctf"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with ctftime.init():
|
||||
register_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
|
||||
with ctftime.started():
|
||||
client = login_as_user(app)
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"submission": "flag",
|
||||
"challenge_id": chal_id,
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/api/v1/challenges/attempt", data=data)
|
||||
assert r.status_code == 200
|
||||
solve_count = app.db.session.query(app.db.func.count(Solves.id)).first()[0]
|
||||
assert solve_count == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_ctftime_prevents_accessing_challenges_after_ctf():
|
||||
"""Test that the ctftime function prevents accessing challenges after the ctf"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with ctftime.init():
|
||||
|
||||
register_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
|
||||
with ctftime.ended():
|
||||
client = login_as_user(app)
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 403
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"submission": "flag",
|
||||
"challenge_id": chal_id,
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/api/v1/challenges/attempt", data=data)
|
||||
assert r.status_code == 403
|
||||
solve_count = app.db.session.query(app.db.func.count(Solves.id)).first()[0]
|
||||
assert solve_count == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_ctf_started():
|
||||
"""
|
||||
Tests that the ctf_started function returns the correct value
|
||||
:return:
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
assert ctf_started() is True
|
||||
|
||||
with ctftime.init():
|
||||
|
||||
with ctftime.not_started():
|
||||
ctf_started()
|
||||
assert ctf_started() is False
|
||||
|
||||
with ctftime.started():
|
||||
assert ctf_started() is True
|
||||
|
||||
with ctftime.ended():
|
||||
assert ctf_started() is True
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_ctf_ended():
|
||||
"""
|
||||
Tests that the ctf_ended function returns the correct value
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
assert ctf_ended() is False
|
||||
with ctftime.init():
|
||||
|
||||
with ctftime.not_started():
|
||||
assert ctf_ended() is False
|
||||
|
||||
with ctftime.started():
|
||||
assert ctf_ended() is False
|
||||
|
||||
with ctftime.ended():
|
||||
assert ctf_ended() is True
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_unix_time():
|
||||
"""
|
||||
Tests that the unix_time function returns the correct value and fails gracefully for strange inputs
|
||||
"""
|
||||
assert unix_time(DateTime(2017, 1, 1)) == 1483228800
|
||||
assert type(unix_time(DateTime(2017, 1, 1))) == int
|
||||
assert unix_time(None) is None
|
||||
assert unix_time("test") is None
|
||||
assert unix_time(1) is None
|
||||
|
||||
|
||||
def test_unix_time_millis():
|
||||
"""
|
||||
Tests that the unix_time function returns the correct value and fails gracefully for strange inputs
|
||||
"""
|
||||
# Aware datetime object
|
||||
assert unix_time_millis(DateTime(2017, 1, 1)) == 1483228800000
|
||||
assert type(unix_time_millis(DateTime(2017, 1, 1))) == int
|
||||
assert unix_time_millis(None) is None
|
||||
assert unix_time_millis("test") is None
|
||||
assert unix_time_millis(1) is None
|
||||
|
||||
|
||||
def test_unix_time_to_utc():
|
||||
"""
|
||||
Tests that the unix_time function returns the correct value and fails gracefully for strange inputs
|
||||
"""
|
||||
assert unix_time_to_utc(0) == DateTime(1970, 1, 1)
|
||||
assert unix_time_to_utc(1483228800) == DateTime(2017, 1, 1)
|
||||
assert type(unix_time_to_utc(1483228800)) == DateTime
|
||||
assert unix_time_to_utc(None) is None
|
||||
with pytest.raises(TypeError):
|
||||
unix_time_to_utc("test")
|
||||
with pytest.raises(TypeError):
|
||||
unix_time_to_utc(DateTime(2017, 1, 1))
|
||||
|
||||
|
||||
def test_isoformat():
|
||||
"""
|
||||
Tests that the unix_time function returns the correct value and fails gracefully for strange inputs
|
||||
"""
|
||||
assert (
|
||||
isoformat(DateTime(2017, 1, 1, tzinfo=TimeZone.utc))
|
||||
== "2017-01-01T00:00:00+00:00Z"
|
||||
)
|
||||
assert isoformat(DateTime(2017, 1, 1)) == "2017-01-01T00:00:00Z"
|
||||
assert isoformat(DateTime(2017, 1, 1, tzinfo=None)) == "2017-01-01T00:00:00Z"
|
||||
assert isoformat(None) is None
|
||||
assert isoformat("test") is None
|
||||
assert isoformat(1) is None
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user