diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 33586f7b..7be6205e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,7 +23,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install -r development.txt + python -m pip install -r linting.txt sudo yarn install --non-interactive sudo yarn global add prettier@1.17.0 diff --git a/CTFd/__init__.py b/CTFd/__init__.py index 46267c3c..03c708e2 100644 --- a/CTFd/__init__.py +++ b/CTFd/__init__.py @@ -12,7 +12,6 @@ from flask_migrate import upgrade from jinja2 import FileSystemLoader from jinja2.sandbox import SandboxedEnvironment from werkzeug.middleware.proxy_fix import ProxyFix -from werkzeug.utils import cached_property import CTFd.utils.config from CTFd import utils @@ -36,16 +35,14 @@ __channel__ = "oss" class CTFdRequest(Request): - @cached_property - def path(self): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) """ Hijack the original Flask request path because it does not account for subdirectory deployments in an intuitive manner. We append script_root so that the path always points to the full path as seen in the browser. e.g. /subdirectory/path/route vs /path/route - - :return: string """ - return self.script_root + super(CTFdRequest, self).path + self.path = self.script_root + self.path class CTFdFlask(Flask): @@ -86,24 +83,30 @@ class SandboxedBaseEnvironment(SandboxedEnvironment): theme = str(utils.get_config("ctf_theme")) cache_name = theme + "/" + name - # Rest of this code is copied from Jinja - # https://github.com/pallets/jinja/blob/master/src/jinja2/environment.py#L802-L815 + # Rest of this code roughly copied from Jinja + # https://github.com/pallets/jinja/blob/b08cd4bc64bb980df86ed2876978ae5735572280/src/jinja2/environment.py#L956-L973 cache_key = (weakref.ref(self.loader), cache_name) if self.cache is not None: template = self.cache.get(cache_key) if template is not None and ( not self.auto_reload or template.is_up_to_date ): + # template.globals is a ChainMap, modifying it will only + # affect the template, not the environment globals. + if globals: + template.globals.update(globals) + return template - template = self.loader.load(self, name, globals) + + template = self.loader.load(self, name, self.make_globals(globals)) + if self.cache is not None: self.cache[cache_key] = template return template class ThemeLoader(FileSystemLoader): - """Custom FileSystemLoader that is aware of theme structure and config. - """ + """Custom FileSystemLoader that is aware of theme structure and config.""" DEFAULT_THEMES_PATH = os.path.join(os.path.dirname(__file__), "themes") _ADMIN_THEME_PREFIX = ADMIN_THEME + "/" diff --git a/CTFd/admin/__init__.py b/CTFd/admin/__init__.py index 3d6af66a..3270006a 100644 --- a/CTFd/admin/__init__.py +++ b/CTFd/admin/__init__.py @@ -120,7 +120,7 @@ def export_ctf(): backup = export_ctf_util() ctf_name = ctf_config.ctf_name() day = datetime.datetime.now().strftime("%Y-%m-%d_%T") - full_name = u"{}.{}.zip".format(ctf_name, day) + full_name = "{}.{}.zip".format(ctf_name, day) return send_file( backup, cache_timeout=-1, as_attachment=True, attachment_filename=full_name ) diff --git a/CTFd/api/v1/helpers/schemas.py b/CTFd/api/v1/helpers/schemas.py index cd3ed4f6..f87b71af 100644 --- a/CTFd/api/v1/helpers/schemas.py +++ b/CTFd/api/v1/helpers/schemas.py @@ -31,7 +31,5 @@ def sqlalchemy_to_pydantic( for name, python_type in include.items(): default = None fields[name] = (python_type, default) - pydantic_model = create_model( - db_model.__name__, **fields # type: ignore - ) + pydantic_model = create_model(db_model.__name__, **fields) # type: ignore return pydantic_model diff --git a/CTFd/api/v1/topics.py b/CTFd/api/v1/topics.py index ef50389b..1c453d0c 100644 --- a/CTFd/api/v1/topics.py +++ b/CTFd/api/v1/topics.py @@ -51,7 +51,10 @@ class TopicList(Resource): { "value": (str, None), "q": (str, None), - "field": (RawEnum("TopicFields", {"value": "value"}), None,), + "field": ( + RawEnum("TopicFields", {"value": "value"}), + None, + ), }, location="query", ) @@ -122,7 +125,8 @@ class TopicList(Resource): responses={200: ("Success", "APISimpleSuccessResponse")}, ) @validate_args( - {"type": (str, None), "target_id": (int, 0)}, location="query", + {"type": (str, None), "target_id": (int, 0)}, + location="query", ) def delete(self, query_args): topic_type = query_args.get("type") diff --git a/CTFd/auth.py b/CTFd/auth.py index aacea14b..390f57c7 100644 --- a/CTFd/auth.py +++ b/CTFd/auth.py @@ -208,9 +208,11 @@ def register(): registration_code = str(request.form.get("registration_code", "")) name_len = len(name) == 0 - names = Users.query.add_columns("name", "id").filter_by(name=name).first() + names = ( + Users.query.add_columns(Users.name, Users.id).filter_by(name=name).first() + ) emails = ( - Users.query.add_columns("email", "id") + Users.query.add_columns(Users.email, Users.id) .filter_by(email=email_address) .first() ) diff --git a/CTFd/cache/__init__.py b/CTFd/cache/__init__.py index bcbcc35c..95104173 100644 --- a/CTFd/cache/__init__.py +++ b/CTFd/cache/__init__.py @@ -21,7 +21,7 @@ def timed_lru_cache(timeout: int = 300, maxsize: int = 64, typed: bool = False): def wrapper_cache(func): func = lru_cache(maxsize=maxsize, typed=typed)(func) - func.delta = timeout * 10 ** 9 + func.delta = timeout * 10**9 func.expiration = monotonic_ns() + func.delta @wraps(func) diff --git a/CTFd/errors.py b/CTFd/errors.py index d2c3c88f..5e654912 100644 --- a/CTFd/errors.py +++ b/CTFd/errors.py @@ -12,7 +12,8 @@ def render_error(error): try: return ( render_template( - "errors/{}.html".format(error.code), error=error.description, + "errors/{}.html".format(error.code), + error=error.description, ), error.code, ) diff --git a/CTFd/forms/config.py b/CTFd/forms/config.py index bc68dee6..3a051670 100644 --- a/CTFd/forms/config.py +++ b/CTFd/forms/config.py @@ -49,7 +49,8 @@ class AccountSettingsForm(BaseForm): description="Max number of teams (Teams mode only)", ) num_users = IntegerField( - widget=NumberInput(min=0), description="Max number of users", + widget=NumberInput(min=0), + description="Max number of users", ) verify_emails = SelectField( "Verify Emails", @@ -101,13 +102,15 @@ class LegalSettingsForm(BaseForm): description="External URL to a Terms of Service document hosted elsewhere", ) tos_text = TextAreaField( - "Terms of Service", description="Text shown on the Terms of Service page", + "Terms of Service", + description="Text shown on the Terms of Service page", ) privacy_url = URLField( "Privacy Policy URL", description="External URL to a Privacy Policy document hosted elsewhere", ) privacy_text = TextAreaField( - "Privacy Policy", description="Text shown on the Privacy Policy page", + "Privacy Policy", + description="Text shown on the Privacy Policy page", ) submit = SubmitField("Update") diff --git a/CTFd/models/__init__.py b/CTFd/models/__init__.py index a09bbc66..d7a9fef0 100644 --- a/CTFd/models/__init__.py +++ b/CTFd/models/__init__.py @@ -15,12 +15,13 @@ ma = Marshmallow() def get_class_by_tablename(tablename): """Return class reference mapped to table. - https://stackoverflow.com/a/23754464 + https://stackoverflow.com/a/66666783 :param tablename: String with name of table. :return: Class reference or None. """ - for c in db.Model._decl_class_registry.values(): + for m in db.Model.registry.mappers: + c = m.class_ if hasattr(c, "__tablename__") and c.__tablename__ == tablename: return c return None diff --git a/CTFd/plugins/dynamic_challenges/__init__.py b/CTFd/plugins/dynamic_challenges/__init__.py index 06a138f7..94ebe20a 100644 --- a/CTFd/plugins/dynamic_challenges/__init__.py +++ b/CTFd/plugins/dynamic_challenges/__init__.py @@ -25,11 +25,13 @@ class DynamicChallenge(Challenges): class DynamicValueChallenge(BaseChallenge): id = "dynamic" # Unique identifier used to register challenges name = "dynamic" # Name of a challenge type - templates = { # Handlebars templates used for each aspect of challenge editing & viewing - "create": "/plugins/dynamic_challenges/assets/create.html", - "update": "/plugins/dynamic_challenges/assets/update.html", - "view": "/plugins/dynamic_challenges/assets/view.html", - } + templates = ( + { # Handlebars templates used for each aspect of challenge editing & viewing + "create": "/plugins/dynamic_challenges/assets/create.html", + "update": "/plugins/dynamic_challenges/assets/update.html", + "view": "/plugins/dynamic_challenges/assets/view.html", + } + ) scripts = { # Scripts that are loaded when a template is loaded "create": "/plugins/dynamic_challenges/assets/create.js", "update": "/plugins/dynamic_challenges/assets/update.js", diff --git a/CTFd/plugins/dynamic_challenges/decay.py b/CTFd/plugins/dynamic_challenges/decay.py index a1a49efb..8db468f9 100644 --- a/CTFd/plugins/dynamic_challenges/decay.py +++ b/CTFd/plugins/dynamic_challenges/decay.py @@ -57,8 +57,8 @@ def logarithmic(challenge): # It is important that this calculation takes into account floats. # Hence this file uses from __future__ import division value = ( - ((challenge.minimum - challenge.initial) / (challenge.decay ** 2)) - * (solve_count ** 2) + ((challenge.minimum - challenge.initial) / (challenge.decay**2)) + * (solve_count**2) ) + challenge.initial value = math.ceil(value) diff --git a/CTFd/schemas/challenges.py b/CTFd/schemas/challenges.py index 3cd9615a..468ab32d 100644 --- a/CTFd/schemas/challenges.py +++ b/CTFd/schemas/challenges.py @@ -68,5 +68,7 @@ class ChallengeSchema(ma.ModelSchema): ) requirements = field_for( - Challenges, "requirements", validate=[ChallengeRequirementsValidator()], + Challenges, + "requirements", + validate=[ChallengeRequirementsValidator()], ) diff --git a/CTFd/schemas/pages.py b/CTFd/schemas/pages.py index de7b9d99..30160c84 100644 --- a/CTFd/schemas/pages.py +++ b/CTFd/schemas/pages.py @@ -16,7 +16,9 @@ class PageSchema(ma.ModelSchema): "title", validate=[ validate.Length( - min=0, max=80, error="Page could not be saved. Your title is too long.", + min=0, + max=80, + error="Page could not be saved. Your title is too long.", ) ], ) diff --git a/CTFd/utils/challenges/__init__.py b/CTFd/utils/challenges/__init__.py index 83a8b0a9..86ecb2f0 100644 --- a/CTFd/utils/challenges/__init__.py +++ b/CTFd/utils/challenges/__init__.py @@ -111,10 +111,14 @@ def get_solve_counts_for_challenges(challenge_id=None, admin=False): else: freeze_cond = true() exclude_solves_cond = and_( - AccountModel.banned == false(), AccountModel.hidden == false(), + AccountModel.banned == false(), + AccountModel.hidden == false(), ) solves_q = ( - db.session.query(Solves.challenge_id, sa_func.count(Solves.challenge_id),) + db.session.query( + Solves.challenge_id, + sa_func.count(Solves.challenge_id), + ) .join(AccountModel) .filter(*challenge_id_filter, freeze_cond, exclude_solves_cond) .group_by(Solves.challenge_id) diff --git a/CTFd/utils/csv/__init__.py b/CTFd/utils/csv/__init__.py index 553a45f1..4b24ecbb 100644 --- a/CTFd/utils/csv/__init__.py +++ b/CTFd/utils/csv/__init__.py @@ -368,21 +368,31 @@ def load_challenges_csv(dict_reader): if flags: flags = [flag.strip() for flag in flags.split(",")] for flag in flags: - f = Flags(type="static", challenge_id=challenge.id, content=flag,) + f = Flags( + type="static", + challenge_id=challenge.id, + content=flag, + ) db.session.add(f) db.session.commit() if tags: tags = [tag.strip() for tag in tags.split(",")] for tag in tags: - t = Tags(challenge_id=challenge.id, value=tag,) + t = Tags( + challenge_id=challenge.id, + value=tag, + ) db.session.add(t) db.session.commit() if hints: hints = [hint.strip() for hint in hints.split(",")] for hint in hints: - h = Hints(challenge_id=challenge.id, content=hint,) + h = Hints( + challenge_id=challenge.id, + content=hint, + ) db.session.add(h) db.session.commit() if errors: diff --git a/CTFd/utils/dates/__init__.py b/CTFd/utils/dates/__init__.py index 5ce97208..2f0b4b85 100644 --- a/CTFd/utils/dates/__init__.py +++ b/CTFd/utils/dates/__init__.py @@ -5,7 +5,7 @@ from CTFd.utils import get_config def ctftime(): - """ Checks whether it's CTF time or not. """ + """Checks whether it's CTF time or not.""" start = get_config("start") end = get_config("end") diff --git a/CTFd/utils/exports/__init__.py b/CTFd/utils/exports/__init__.py index 784b85e4..0e7933a3 100644 --- a/CTFd/utils/exports/__init__.py +++ b/CTFd/utils/exports/__init__.py @@ -84,6 +84,7 @@ def export_ctf(): backup_zip.close() backup.seek(0) + db.close() return backup diff --git a/CTFd/utils/migrations/__init__.py b/CTFd/utils/migrations/__init__.py index 4202e5e4..09c78a69 100644 --- a/CTFd/utils/migrations/__init__.py +++ b/CTFd/utils/migrations/__init__.py @@ -17,10 +17,10 @@ migrations = Migrate() def create_database(): url = make_url(app.config["SQLALCHEMY_DATABASE_URI"]) if url.drivername == "postgres": - url.drivername = "postgresql" + url = url.set(drivername="postgresql") if url.drivername.startswith("mysql"): - url.query["charset"] = "utf8mb4" + url = url.update_query_dict({"charset": "utf8mb4"}) # Creates database if the database database does not exist if not database_exists_util(url): @@ -34,7 +34,7 @@ def create_database(): def drop_database(): url = make_url(app.config["SQLALCHEMY_DATABASE_URI"]) if url.drivername == "postgres": - url.drivername = "postgresql" + url = url.set(drivername="postgresql") drop_database_util(url) diff --git a/CTFd/utils/user/__init__.py b/CTFd/utils/user/__init__.py index 2bbcf622..ffb1de49 100644 --- a/CTFd/utils/user/__init__.py +++ b/CTFd/utils/user/__init__.py @@ -152,15 +152,15 @@ def is_verified(): def get_ip(req=None): - """ Returns the IP address of the currently in scope request. The approach is to define a list of trusted proxies - (in this case the local network), and only trust the most recently defined untrusted IP address. - Taken from http://stackoverflow.com/a/22936947/4285524 but the generator there makes no sense. - The trusted_proxies regexes is taken from Ruby on Rails. + """Returns the IP address of the currently in scope request. The approach is to define a list of trusted proxies + (in this case the local network), and only trust the most recently defined untrusted IP address. + Taken from http://stackoverflow.com/a/22936947/4285524 but the generator there makes no sense. + The trusted_proxies regexes is taken from Ruby on Rails. - This has issues if the clients are also on the local network so you can remove proxies from config.py. + This has issues if the clients are also on the local network so you can remove proxies from config.py. - CTFd does not use IP address for anything besides cursory tracking of teams and it is ill-advised to do much - more than that if you do not know what you're doing. + CTFd does not use IP address for anything besides cursory tracking of teams and it is ill-advised to do much + more than that if you do not know what you're doing. """ if req is None: req = request diff --git a/CTFd/views.py b/CTFd/views.py index 57536288..70bfac97 100644 --- a/CTFd/views.py +++ b/CTFd/views.py @@ -124,9 +124,15 @@ def setup(): password = request.form["password"] name_len = len(name) == 0 - names = Users.query.add_columns("name", "id").filter_by(name=name).first() + names = ( + Users.query.add_columns(Users.name, Users.id) + .filter_by(name=name) + .first() + ) emails = ( - Users.query.add_columns("email", "id").filter_by(email=email).first() + Users.query.add_columns(Users.email, Users.id) + .filter_by(email=email) + .first() ) pass_short = len(password) == 0 pass_long = len(password) > 128 diff --git a/development.txt b/development.txt index 9ec385ba..1075c528 100644 --- a/development.txt +++ b/development.txt @@ -3,17 +3,14 @@ pip-tools==5.4.0 pytest==7.3.1 pytest-randomly==3.12.0 coverage==7.2.3 -ruff==0.0.260 psycopg2-binary==2.9.6 -moto==1.3.16 +moto==4.1.11 bandit==1.6.2 flask_profiler==1.8.1 pytest-xdist==3.2.1 pytest-cov==4.0.0 sphinx_rtd_theme==0.4.3 flask-debugtoolbar==0.11.0 -isort==4.3.21 Faker==4.1.0 pipdeptree==2.2.0 -black==19.10b0 pytest-sugar==0.9.7 diff --git a/linting.txt b/linting.txt new file mode 100644 index 00000000..32b452f0 --- /dev/null +++ b/linting.txt @@ -0,0 +1,3 @@ +black==22.3.0 +isort==4.3.21 +ruff==0.0.260 diff --git a/ping.py b/ping.py index 1b85b9bd..4f39640e 100644 --- a/ping.py +++ b/ping.py @@ -17,7 +17,7 @@ if url.drivername.startswith("sqlite"): # Null out the database so raw_connection doesnt error if it doesnt exist # CTFd will create the database if it doesnt exist -url.database = None +url = url.set(database=None) # Wait for the database server to be available engine = create_engine(url) diff --git a/requirements.in b/requirements.in index 6b6bae3a..82728a31 100644 --- a/requirements.in +++ b/requirements.in @@ -1,24 +1,22 @@ -Flask==1.1.2 -Werkzeug==1.0.1 -Jinja2==2.11.3 -Flask-SQLAlchemy==2.4.3 -Flask-Caching==1.8.0 +Flask==2.0.3 +Werkzeug==2.1.2 +Flask-SQLAlchemy==2.5.1 +Flask-Caching==2.0.2 Flask-Migrate==2.5.3 Flask-Script==2.0.6 -SQLAlchemy==1.3.17 -SQLAlchemy-Utils==0.41.0 +SQLAlchemy==1.4.48 +SQLAlchemy-Utils==0.41.1 passlib==1.7.4 bcrypt==4.0.1 -itsdangerous==1.1.0 requests==2.28.1 PyMySQL[rsa]==0.9.3 gunicorn==20.1.0 -dataset==1.3.1 +dataset==1.5.2 cmarkgfm==2022.10.27 -redis==4.4.4 +redis==4.5.5 gevent==22.10.2 python-dotenv==0.13.0 -flask-restx==0.5.1 +flask-restx==1.1.0 flask-marshmallow==0.10.1 marshmallow-sqlalchemy==0.17.0 boto3==1.13.9 diff --git a/requirements.txt b/requirements.txt index 804dc614..81187d54 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: # # ./scripts/pip-compile.sh # @@ -16,6 +16,8 @@ attrs==20.3.0 # via jsonschema babel==2.12.1 # via flask-babel +banal==1.0.6 + # via dataset bcrypt==4.0.1 # via -r requirements.in boto3==1.13.9 @@ -24,6 +26,8 @@ botocore==1.16.26 # via # boto3 # s3transfer +cachelib==0.9.0 + # via flask-caching certifi==2022.12.7 # via requests cffi==1.15.0 @@ -39,11 +43,11 @@ cmarkgfm==2022.10.27 # via -r requirements.in cryptography==40.0.2 # via pymysql -dataset==1.3.1 +dataset==1.5.2 # via -r requirements.in docutils==0.15.2 # via botocore -flask==1.1.2 +flask==2.0.3 # via # -r requirements.in # flask-babel @@ -55,17 +59,17 @@ flask==1.1.2 # flask-sqlalchemy flask-babel==2.0.0 # via -r requirements.in -flask-caching==1.8.0 +flask-caching==2.0.2 # via -r requirements.in flask-marshmallow==0.10.1 # via -r requirements.in flask-migrate==2.5.3 # via -r requirements.in -flask-restx==0.5.1 +flask-restx==1.1.0 # via -r requirements.in flask-script==2.0.6 # via -r requirements.in -flask-sqlalchemy==2.4.3 +flask-sqlalchemy==2.5.1 # via # -r requirements.in # flask-migrate @@ -74,18 +78,17 @@ freezegun==1.2.2 gevent==22.10.2 # via -r requirements.in greenlet==2.0.1 - # via gevent + # via + # gevent + # sqlalchemy gunicorn==20.1.0 # via -r requirements.in idna==2.10 # via requests -itsdangerous==1.1.0 +itsdangerous==2.1.2 + # via flask +jinja2==3.1.2 # via - # -r requirements.in - # flask -jinja2==2.11.3 - # via - # -r requirements.in # flask # flask-babel jmespath==0.10.0 @@ -96,7 +99,7 @@ jsonschema==3.2.0 # via flask-restx mako==1.1.3 # via alembic -markupsafe==1.1.1 +markupsafe==2.1.3 # via # jinja2 # mako @@ -139,7 +142,7 @@ pytz==2020.4 # via # flask-babel # flask-restx -redis==3.5.2 +redis==4.5.5 # via -r requirements.in requests==2.28.1 # via -r requirements.in @@ -148,11 +151,10 @@ s3transfer==0.3.3 six==1.15.0 # via # flask-marshmallow - # flask-restx # jsonschema # python-dateutil # tenacity -sqlalchemy==1.3.17 +sqlalchemy==1.4.48 # via # -r requirements.in # alembic @@ -160,7 +162,7 @@ sqlalchemy==1.3.17 # flask-sqlalchemy # marshmallow-sqlalchemy # sqlalchemy-utils -sqlalchemy-utils==0.41.0 +sqlalchemy-utils==0.41.1 # via -r requirements.in tenacity==6.2.0 # via -r requirements.in @@ -168,7 +170,7 @@ urllib3==1.25.11 # via # botocore # requests -werkzeug==1.0.1 +werkzeug==2.1.2 # via # -r requirements.in # flask diff --git a/scripts/pip-compile.sh b/scripts/pip-compile.sh index 0c24ebbd..5c255c1a 100755 --- a/scripts/pip-compile.sh +++ b/scripts/pip-compile.sh @@ -7,4 +7,4 @@ docker run \ -v $ROOTDIR:/mnt/CTFd \ -e CUSTOM_COMPILE_COMMAND='./scripts/pip-compile.sh' \ -it python:3.9-slim-buster \ - -c 'cd /mnt/CTFd && pip install pip-tools==6.6.0 && pip-compile' + -c 'cd /mnt/CTFd && pip install pip-tools==6.13.0 && pip-compile' diff --git a/tests/admin/test_views.py b/tests/admin/test_views.py index 2a9c75ad..a8cc7397 100644 --- a/tests/admin/test_views.py +++ b/tests/admin/test_views.py @@ -59,7 +59,7 @@ def test_admin_access(): for route in routes: r = client.get(route) assert r.status_code == 302 - assert r.location.startswith("http://localhost/login") + assert r.location.startswith("/login") admin = login_as_user(app, name="admin") routes.remove("/admin") @@ -78,5 +78,5 @@ def test_get_admin_as_user(): client = login_as_user(app) r = client.get("/admin") assert r.status_code == 302 - assert r.location.startswith("http://localhost/login") + assert r.location.startswith("/login") destroy_ctfd(app) diff --git a/tests/api/v1/test_config.py b/tests/api/v1/test_config.py index 5c7f2c0c..a845e98c 100644 --- a/tests/api/v1/test_config.py +++ b/tests/api/v1/test_config.py @@ -132,7 +132,8 @@ def test_config_value_types(): # Test regular length strings r = admin.patch( - "/api/v1/configs", json={"ctf_footer": "// regular length string"}, + "/api/v1/configs", + json={"ctf_footer": "// regular length string"}, ) assert r.status_code == 200 assert get_config("ctf_footer") == "// regular length string" diff --git a/tests/api/v1/user/test_admin_access.py b/tests/api/v1/user/test_admin_access.py index fb712085..da28f468 100644 --- a/tests/api/v1/user/test_admin_access.py +++ b/tests/api/v1/user/test_admin_access.py @@ -44,5 +44,5 @@ def test_api_hint_404(): for endpoint in endpoints: r = client.get(endpoint.format(1)) assert r.status_code == 302 - assert r.location.startswith("http://localhost/login") + assert r.location.startswith("/login") destroy_ctfd(app) diff --git a/tests/helpers.py b/tests/helpers.py index 665f321d..90226732 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -138,7 +138,7 @@ def create_ctfd( config.APPLICATION_ROOT = application_root url = make_url(config.SQLALCHEMY_DATABASE_URI) if url.database: - url.database = str(uuid.uuid4()) + url = url.set(database=str(uuid.uuid4())) config.SQLALCHEMY_DATABASE_URI = str(url) app = create_app(config) diff --git a/tests/oauth/test_redirect.py b/tests/oauth/test_redirect.py index 488aea01..f54fa90f 100644 --- a/tests/oauth/test_redirect.py +++ b/tests/oauth/test_redirect.py @@ -18,7 +18,7 @@ def test_oauth_not_configured(): with app.app_context(): with app.test_client() as client: r = client.get("/oauth", follow_redirects=False) - assert r.location == "http://localhost/login" + assert r.location == "/login" r = client.get(r.location) resp = r.get_data(as_text=True) assert "OAuth Settings not configured" in resp diff --git a/tests/teams/test_challenges.py b/tests/teams/test_challenges.py index a5599e90..fde4afaf 100644 --- a/tests/teams/test_challenges.py +++ b/tests/teams/test_challenges.py @@ -51,7 +51,7 @@ def test_anonymous_users_view_public_challenges_without_team(): with app.test_client() as client: r = client.get("/challenges") assert r.status_code == 302 - assert r.location.startswith("http://localhost/login") + assert r.location.startswith("/login") set_config("challenge_visibility", "public") with app.test_client() as client: @@ -61,5 +61,5 @@ def test_anonymous_users_view_public_challenges_without_team(): with login_as_user(app) as client: r = client.get("/challenges") assert r.status_code == 302 - assert r.location.startswith("http://localhost/team") + assert r.location.startswith("/team") destroy_ctfd(app) diff --git a/tests/test_themes.py b/tests/test_themes.py index c9156758..112b597d 100644 --- a/tests/test_themes.py +++ b/tests/test_themes.py @@ -90,7 +90,7 @@ def test_that_ctfd_can_be_deployed_in_subdir(): with app.test_client() as client: r = client.get("/") assert r.status_code == 302 - assert r.location == "http://localhost/ctf/setup" + assert r.location == "/ctf/setup" r = client.get("/setup") with client.session_transaction() as sess: @@ -105,13 +105,13 @@ def test_that_ctfd_can_be_deployed_in_subdir(): } r = client.post("/setup", data=data) assert r.status_code == 302 - assert r.location == "http://localhost/ctf/" + assert r.location == "/ctf/" c = Client(app) - app_iter, status, headers = c.get("/") - headers = dict(headers) - assert status == "302 FOUND" - assert headers["Location"] == "http://localhost/ctf/" + 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 diff --git a/tests/test_views.py b/tests/test_views.py index 55da66bf..34113e3d 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -83,7 +83,7 @@ def test_page_requiring_auth(): with app.test_client() as client: r = client.get("/this-is-a-route") assert r.status_code == 302 - assert r.location == "http://localhost/login?next=%2Fthis-is-a-route%3F" + assert r.location == "/login?next=%2Fthis-is-a-route%3F" register_user(app) client = login_as_user(app) @@ -388,6 +388,13 @@ def test_user_can_access_files_with_auth_token(): # 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) @@ -424,12 +431,13 @@ def test_user_can_access_files_if_view_after_ctf(): 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] - # 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") @@ -443,8 +451,8 @@ def test_user_can_access_files_if_view_after_ctf(): assert r.get_data(as_text=True) == "testing file load" # Unauthed users should be able to download if view_after_ctf - client = app.test_client() - r = client.get(file_url) + 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: diff --git a/tests/users/test_auth.py b/tests/users/test_auth.py index 890666ac..3e3b5f61 100644 --- a/tests/users/test_auth.py +++ b/tests/users/test_auth.py @@ -119,9 +119,7 @@ def test_user_bad_login(): with client.session_transaction() as sess: assert sess.get("id") is None r = client.get("/profile") - assert r.location.startswith( - "http://localhost/login" - ) # We got redirected to login + assert r.location.startswith("/login") # We got redirected to login destroy_ctfd(app) @@ -132,9 +130,7 @@ def test_user_login(): register_user(app) client = login_as_user(app) r = client.get("/profile") - assert ( - r.location != "http://localhost/login" - ) # We didn't get redirected to login + assert r.location is None # We didn't get redirected to login assert r.status_code == 200 destroy_ctfd(app) @@ -146,9 +142,7 @@ def test_user_login_with_email(): register_user(app) client = login_as_user(app, name="user@examplectf.com", password="password") r = client.get("/profile") - assert ( - r.location != "http://localhost/login" - ) # We didn't get redirected to login + assert r.location is None # We didn't get redirected to login assert r.status_code == 200 destroy_ctfd(app) @@ -161,7 +155,7 @@ def test_user_get_logout(): client = login_as_user(app) client.get("/logout", follow_redirects=True) r = client.get("/challenges") - assert r.location == "http://localhost/login?next=%2Fchallenges%3F" + assert r.location == "/login?next=%2Fchallenges%3F" assert r.status_code == 302 destroy_ctfd(app) @@ -182,7 +176,7 @@ def test_user_isnt_admin(): "config", ]: r = client.get("/admin/{}".format(page)) - assert r.location.startswith("http://localhost/login?next=") + assert r.location.startswith("/login?next=") assert r.status_code == 302 destroy_ctfd(app) @@ -302,7 +296,7 @@ def test_user_can_confirm_email(mock_smtp): client = login_as_user(app, name="user1", password="password") - r = client.get("http://localhost/confirm") + r = client.get("/confirm") assert "We've sent a confirmation email" in r.get_data(as_text=True) # smtp send message function was called @@ -310,23 +304,21 @@ def test_user_can_confirm_email(mock_smtp): with client.session_transaction() as sess: data = {"nonce": sess.get("nonce")} - r = client.post("http://localhost/confirm", data=data) + r = client.post("/confirm", data=data) assert "Confirmation email sent to" in r.get_data(as_text=True) r = client.get("/challenges") - assert ( - r.location == "http://localhost/confirm" - ) # We got redirected to /confirm + assert r.location == "/confirm" # We got redirected to /confirm - r = client.get("http://localhost/confirm/" + serialize("user@user.com")) - assert r.location == "http://localhost/challenges" + r = client.get("/confirm/" + serialize("user@user.com")) + 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 == "http://localhost/settings" + r = client.get("/confirm") + assert r.location == "/settings" destroy_ctfd(app) @@ -462,7 +454,7 @@ def test_registration_code_required(): data["registration_code"] = "secret-sauce" r = client.post("/register", data=data) assert r.status_code == 302 - assert r.location.startswith("http://localhost/challenges") + assert r.location.startswith("/challenges") destroy_ctfd(app) @@ -492,5 +484,5 @@ def test_registration_code_allows_numeric(): data["registration_code"] = "1234567890" r = client.post("/register", data=data) assert r.status_code == 302 - assert r.location.startswith("http://localhost/challenges") + assert r.location.startswith("/challenges") destroy_ctfd(app) diff --git a/tests/users/test_challenges.py b/tests/users/test_challenges.py index 88181a56..63a80899 100644 --- a/tests/users/test_challenges.py +++ b/tests/users/test_challenges.py @@ -228,7 +228,7 @@ def test_submitting_unicode_flag(): register_user(app) client = login_as_user(app) chal = gen_challenge(app.db) - gen_flag(app.db, challenge_id=chal.id, content=u"你好") + 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) @@ -251,7 +251,7 @@ def test_challenges_with_max_attempts(): chal.max_attempts = 3 app.db.session.commit() - gen_flag(app.db, challenge_id=chal.id, content=u"flag") + 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) @@ -281,7 +281,7 @@ def test_challenge_kpm_limit(): chal = gen_challenge(app.db) chal_id = chal.id - gen_flag(app.db, challenge_id=chal.id, content=u"flag") + 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} diff --git a/tests/users/test_fields.py b/tests/users/test_fields.py index b118c651..a097845a 100644 --- a/tests/users/test_fields.py +++ b/tests/users/test_fields.py @@ -259,7 +259,7 @@ def test_user_needs_all_required_fields(): r = client.get("/challenges") assert r.status_code == 302 - assert r.location.startswith("http://localhost/settings") + assert r.location.startswith("/settings") # Populate the non-required fields r = client.patch( @@ -277,7 +277,7 @@ def test_user_needs_all_required_fields(): # I should still be restricted from seeing challenges r = client.get("/challenges") assert r.status_code == 302 - assert r.location.startswith("http://localhost/settings") + assert r.location.startswith("/settings") # I should still see all fields b/c I don't have a complete profile r = client.get("/settings") diff --git a/tests/users/test_setup.py b/tests/users/test_setup.py index 634b12e6..c033424f 100644 --- a/tests/users/test_setup.py +++ b/tests/users/test_setup.py @@ -11,7 +11,7 @@ def test_ctfd_setup_redirect(): with app.test_client() as client: r = client.get("/users") assert r.status_code == 302 - assert r.location == "http://localhost/setup" + assert r.location == "/setup" # Files in /themes load properly r = client.get("/themes/core/static/css/main.dev.css") @@ -52,5 +52,5 @@ def test_ctfd_setup_verification(): data["email"] = "admin@examplectf.com" r = client.post("/setup", data=data) assert r.status_code == 302 - assert r.location == "http://localhost/" + assert r.location == "/" destroy_ctfd(app) diff --git a/tests/utils/test_ctftime.py b/tests/utils/test_ctftime.py index 81e1abcc..f3027fa2 100644 --- a/tests/utils/test_ctftime.py +++ b/tests/utils/test_ctftime.py @@ -21,7 +21,7 @@ def test_ctftime_prevents_accessing_challenges_before_ctf(): register_user(app) chal = gen_challenge(app.db) chal_id = chal.id - gen_flag(app.db, challenge_id=chal.id, content=u"flag") + gen_flag(app.db, challenge_id=chal.id, content="flag") with ctftime.not_started(): client = login_as_user(app) @@ -48,7 +48,7 @@ def test_ctftime_redirects_to_teams_page_in_teams_mode_before_ctf(): with ctftime.init(): register_user(app) chal = gen_challenge(app.db) - gen_flag(app.db, challenge_id=chal.id, content=u"flag") + gen_flag(app.db, challenge_id=chal.id, content="flag") with ctftime.not_started(): client = login_as_user(app) @@ -83,7 +83,7 @@ def test_ctftime_allows_accessing_challenges_during_ctf(): register_user(app) chal = gen_challenge(app.db) chal_id = chal.id - gen_flag(app.db, challenge_id=chal.id, content=u"flag") + gen_flag(app.db, challenge_id=chal.id, content="flag") with ctftime.started(): client = login_as_user(app) @@ -112,7 +112,7 @@ def test_ctftime_prevents_accessing_challenges_after_ctf(): register_user(app) chal = gen_challenge(app.db) chal_id = chal.id - gen_flag(app.db, challenge_id=chal.id, content=u"flag") + gen_flag(app.db, challenge_id=chal.id, content="flag") with ctftime.ended(): client = login_as_user(app) diff --git a/tests/utils/test_email.py b/tests/utils/test_email.py index 85a7cd22..327c0ee5 100644 --- a/tests/utils/test_email.py +++ b/tests/utils/test_email.py @@ -104,7 +104,7 @@ def test_sendmail_with_mailgun_from_config_file(fake_post_request): args, kwargs = fake_post_request.call_args assert args[0] == "https://api.mailgun.net/v3/file.faked.com/messages" - assert kwargs["auth"] == ("api", u"key-1234567890-file-config") + assert kwargs["auth"] == ("api", "key-1234567890-file-config") assert kwargs["timeout"] == 1.0 assert kwargs["data"] == { "to": ["user@user.com"], @@ -144,7 +144,7 @@ def test_sendmail_with_mailgun_from_db_config(fake_post_request): args, kwargs = fake_post_request.call_args assert args[0] == "https://api.mailgun.net/v3/db.faked.com/messages" - assert kwargs["auth"] == ("api", u"key-1234567890-db-config") + assert kwargs["auth"] == ("api", "key-1234567890-db-config") assert kwargs["timeout"] == 1.0 assert kwargs["data"] == { "to": ["user@user.com"], diff --git a/tests/utils/test_sessions.py b/tests/utils/test_sessions.py index b01b1e09..89705e87 100644 --- a/tests/utils/test_sessions.py +++ b/tests/utils/test_sessions.py @@ -1,7 +1,6 @@ +from unittest.mock import Mock, patch from uuid import UUID -from mock import Mock, patch - from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user @@ -40,7 +39,7 @@ def test_session_invalidation_on_admin_password_change(): r = user.get("/settings") # User's password was changed # They should be logged out - assert r.location.startswith("http://localhost/login") + assert r.location.startswith("/login") assert r.status_code == 302 destroy_ctfd(app) diff --git a/tests/utils/test_updates.py b/tests/utils/test_updates.py index 55e8f302..19bf6f1d 100644 --- a/tests/utils/test_updates.py +++ b/tests/utils/test_updates.py @@ -69,14 +69,14 @@ def test_update_check_ignores_downgrades(fake_post_request): fake_response = Mock() fake_post_request.return_value = fake_response fake_response.json = lambda: { - u"resource": { - u"html_url": u"https://github.com/CTFd/CTFd/releases/tag/0.0.1", - u"download_url": u"https://api.github.com/repos/CTFd/CTFd/zipball/0.0.1", - u"published_at": u"Wed, 25 Oct 2017 19:39:42 -0000", - u"tag": u"0.0.1", - u"prerelease": False, - u"id": 6, - u"latest": True, + "resource": { + "html_url": "https://github.com/CTFd/CTFd/releases/tag/0.0.1", + "download_url": "https://api.github.com/repos/CTFd/CTFd/zipball/0.0.1", + "published_at": "Wed, 25 Oct 2017 19:39:42 -0000", + "tag": "0.0.1", + "prerelease": False, + "id": 6, + "latest": True, } } update_check() @@ -85,18 +85,18 @@ def test_update_check_ignores_downgrades(fake_post_request): fake_response = Mock() fake_post_request.return_value = fake_response fake_response.json = lambda: { - u"resource": { - u"html_url": u"https://github.com/CTFd/CTFd/releases/tag/{}".format( + "resource": { + "html_url": "https://github.com/CTFd/CTFd/releases/tag/{}".format( app.VERSION ), - u"download_url": u"https://api.github.com/repos/CTFd/CTFd/zipball/{}".format( + "download_url": "https://api.github.com/repos/CTFd/CTFd/zipball/{}".format( app.VERSION ), - u"published_at": u"Wed, 25 Oct 2017 19:39:42 -0000", - u"tag": u"{}".format(app.VERSION), - u"prerelease": False, - u"id": 6, - u"latest": True, + "published_at": "Wed, 25 Oct 2017 19:39:42 -0000", + "tag": "{}".format(app.VERSION), + "prerelease": False, + "id": 6, + "latest": True, } } update_check()