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

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

0
tests/__init__.py Normal file
View File

0
tests/admin/__init__.py Normal file
View File

View 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
View 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
View 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)

View 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)

View File

95
tests/admin/test_pages.py Normal file
View 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)

View 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)

View File

View 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)

View File

49
tests/admin/test_users.py Normal file
View 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
View 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
View File

105
tests/api/test_tokens.py Normal file
View 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
View File

View 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)

View File

View 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)

View File

View 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)

View 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)

View 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
View 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)

View 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)

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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)

View 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
View 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
View 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
View 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
View 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)

View 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
View 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)

View 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)

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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
View 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
View 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
View 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)

View File

View 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)

View 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)

View 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"]

View File

View 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)

View 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)

View 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
View File

110
tests/cache/test_cache.py vendored Normal file
View 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
View 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)

View File

View 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)

View 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)

View 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)

View 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)

View File

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

View 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)

View 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
View 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
View 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)

View 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
View File

View 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
View 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
View 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
View File

245
tests/teams/test_auth.py Normal file
View 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)

View 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
View 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)

View 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
View 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)

View 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)

View 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
View 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
View 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
View 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
View 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
View 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
View 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("&amp;", "&")
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("&amp;", "&")
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
View 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
View 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
View File

837
tests/users/test_auth.py Normal file
View 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)

View 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
View 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
View 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)

View 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)

View 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)

View 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
View 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)

View 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
View 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
View 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
View 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