Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial tests #81

Merged
merged 59 commits into from
Jun 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
69a42d0
when `USE_INTERNAL_HEDGEDOC` is set, it means hedgedoc is local to d…
Jun 22, 2023
cab7464
[tests] added view tests for team, user + including regression test f…
Jun 23, 2023
2b7e755
merge fix hugsy/issue77
Jun 23, 2023
6e45a0b
making sure fix for #77 passes
Jun 23, 2023
395bc5d
added extra test for team registration
Jun 23, 2023
07b510e
[ci] initial ghactions for testing
Jun 23, 2023
4b8328d
[ci] added envvar
Jun 23, 2023
f5ca4a5
[ci] use own container
Jun 23, 2023
99cdbe5
Update test.yml
hugsy Jun 23, 2023
0165873
Update test.yml
hugsy Jun 23, 2023
ebb2367
[ci] mising envar
Jun 23, 2023
f737710
[ci] missing hedgedoc container
Jun 23, 2023
0a4355a
Update test.yml
hugsy Jun 23, 2023
acfa141
bad syntax
Jun 23, 2023
65f1ac3
Update test.yml
hugsy Jun 23, 2023
73a7f47
hedgedoc is not in compose
Jun 23, 2023
821b539
Update test.yml
hugsy Jun 23, 2023
9633c8a
moved helper functions into utils
Jun 24, 2023
2eba357
Merge branch 'main' into add_tests
Jun 24, 2023
cebf3d3
missing import
Jun 24, 2023
c4afd71
fixed bad merge fix
Jun 24, 2023
159dd72
complete project renaming -> ctfhub
Jun 24, 2023
f9b7b6a
cleaned last reference to ctfpad
Jun 24, 2023
c7ff34d
And migration steps in readme
Jun 24, 2023
ec0b791
Merge branch 'rename_project' into add_tests
Jun 24, 2023
bc94824
fix test and ci
Jun 24, 2023
c490d03
Merge branch 'main' into add_tests
Jun 26, 2023
53c2b43
formatting + linting
Jun 26, 2023
c4e7f1b
proper use of `login_required`
Jun 26, 2023
8807f5f
make sure all pages are authenticated
Jun 27, 2023
0ee1c16
update ci
Jun 27, 2023
9f1d783
stuff
hugsy Jun 27, 2023
31aa250
removed unused `toggle_dark_mode` function
Jun 27, 2023
0d8b55f
Merge branch 'add_tests' of https://github.com/hugsy/ctfpad into add_…
Jun 27, 2023
5ed9258
linting
Jun 27, 2023
1872bb1
[model] updated to django native types
Jun 27, 2023
c79b823
[model] using `models.TextChoices` for `Member.Country`
Jun 27, 2023
e128a32
replaced member.timezone with django native + removed package `pytz`
Jun 27, 2023
b3a6c73
alter timezone db field
Jun 27, 2023
a2852c9
aesthetic stuff
Jun 27, 2023
1841a4e
fixed broken urls, and cosmetic changes
Jun 28, 2023
1116a40
[tests] test members cannot edit settings
Jun 28, 2023
f153e57
[tests] added test_member_cannot_edit_other_member_settings
Jun 28, 2023
b1ff501
[tests] use http/403 for test_member_cannot_edit_other_member_setting…
Jun 28, 2023
2a643c4
properly handle ctftime outage, decreased http get timeout setting to…
Jun 28, 2023
c89022b
skip ctftime tests if not responding
Jun 28, 2023
3cabae8
[test] add `test_ctf_basic`
Jun 28, 2023
8d9837e
[tests] added `test_member_basic`
Jun 28, 2023
7133193
[models] add type hinting and killing bugs
Jun 29, 2023
a6af539
[models] sorted timezones
Jun 29, 2023
94398dc
deleted usless migration
Jun 29, 2023
e6210c8
and decremented the next migration
Jun 29, 2023
c04a876
and fixed migration dependency
Jun 29, 2023
55d7d8f
when exporting a hedgedoc note, do not follow redirect when authentic…
Jun 29, 2023
6d00cfa
[models] fixed notes export function
Jun 29, 2023
0da9831
Merge branch 'main' into add_tests
hugsy Jun 29, 2023
1959c50
part of #69 fixed all linting errors/warnings in ctfhub/helpers.poy
Jun 29, 2023
2bb87cc
migration merge
Jun 29, 2023
e3d2957
fixed linting models
Jun 29, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/notify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ jobs:
nodetail: true
title: ${{ github.actor }} pushed to `${{ github.ref }}`
description: |
**Commit delta**: `${{ github.event.before }}` → `${{ github.event.after }}`
---
---
**Changes**: ${{ github.event.compare }}
---
**Commits**:
Expand Down
74 changes: 74 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: "Run tests"

on:
workflow_dispatch:
push:


jobs:
test:
name: "Run tests"
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_DB: ctfhub
POSTGRES_USER: ctfhub
POSTGRES_PASSWORD: ctfhub
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432

hedgedoc:
image: quay.io/hedgedoc/hedgedoc:alpine
env:
CMD_DB_URL: postgres://ctfhub:ctfhub@postgres:5432/ctfhub
CMD_ALLOW_ANONYMOUS: false
CMD_ALLOW_FREEURL: true
CMD_IMAGE_UPLOAD_TYPE: filesystem
CMD_DOMAIN: localhost
CMD_PORT: 3000
CMD_URL_ADDPORT: true
CMD_PROTOCOL_USESSL: false
ports:
- 3000:3000

steps:
- name: Checkout
uses: actions/checkout@v3

- name: Setup environment vars
run: |
echo CTFPAD_PROTOCOL=http >> $GITHUB_ENV
echo CTFPAD_DOMAIN=localhost >> $GITHUB_ENV
echo CTFPAD_PORT=8000 >> $GITHUB_ENV
echo CTFPAD_DB_NAME=ctfhub >> $GITHUB_ENV
echo CTFPAD_DB_USER=ctfhub >> $GITHUB_ENV
echo CTFPAD_DB_PASSWORD=ctfhub >> $GITHUB_ENV
echo CTFPAD_DB_HOST=localhost >> $GITHUB_ENV
echo CTFPAD_DB_PORT=5432 >> $GITHUB_ENV
echo CTFPAD_HEDGEDOC_URL=http://localhost:3000 >> $GITHUB_ENV
echo CTFPAD_HEDGEDOC_IS_INTERNAL=0 >> $GITHUB_ENV

- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'

- name: Python pre-requisites
run: |
python -m pip install pip --user --upgrade
python -m pip install -r requirements.txt --user --upgrade

- name: Migrate DB
run: |
python manage.py migrate

- name: Execute Tests
run: |
py.test
18 changes: 7 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
# https://hub.docker.com/_/python?tab=description
FROM python:3.11-buster
FROM python:3.10-buster

ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
ENV DEBUG 0

RUN \
apt-get update && \
apt-get upgrade -y && \
apt-get install -y libpq-dev python3-dev postgresql-client && \
apt-get autoclean && \
apt-get autoremove
apt-get update && apt-get upgrade -y && \
apt-get install -y libpq-dev python3-dev postgresql-client && \
apt-get autoclean && apt-get autoremove

WORKDIR /code
COPY requirements.txt .

COPY requirements.txt .
RUN \
python3 -m pip install --upgrade pip && \
python3 -m pip install -r requirements.txt --no-cache-dir

COPY . .
python3 -m pip install --upgrade pip && \
python3 -m pip install -r requirements.txt --no-cache-dir

ENTRYPOINT ["bash", "/code/docker-entrypoint.sh"]
2 changes: 0 additions & 2 deletions ctfhub/admin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
from django.contrib import admin

# Register your models here.
2 changes: 1 addition & 1 deletion ctfhub/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ class CtfhubConfig(AppConfig):
verbose_name = _("ctfhub")

def ready(self):
import ctfhub.signals
pass
28 changes: 27 additions & 1 deletion ctfhub/context_processors.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,35 @@
import django.http
from django.conf import settings

from ctfhub.models import Member

def add_debug_context(request: django.http.HttpRequest) -> dict:

def add_debug_context(request: django.http.HttpRequest) -> dict[str, str]:
"""Adds some CTFHub environment information to every context

Args:
request (django.http.HttpRequest): _description_

Returns:
dict[str, str]: _description_
"""
return {
"DEBUG": settings.DEBUG,
"VERSION": settings.VERSION,
}


def add_timezone_context(request: django.http.HttpRequest) -> dict[str, str]:
"""Add the client timezone information to the HTTP context

Args:
request (django.http.HttpRequest): _description_

Returns:
dict: _description_
"""
try:
member = Member.objects.get(user=request.user)
return {"TZ": member.timezone}
except Exception:
return {"TZ": "UTC"}
8 changes: 8 additions & 0 deletions ctfhub/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class ExternalError(Exception):
"""Used for reporting exceptions from non-builtin components, for instance hedgedoc

Args:
Exception (_type_): _description_
"""

pass
15 changes: 9 additions & 6 deletions ctfhub/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.forms import widgets


class UserUpdateForm(UserChangeForm):
Expand Down Expand Up @@ -87,9 +86,13 @@ class Meta:
has_superpowers = forms.BooleanField(required=False, label="Has Super-Powers?")

def clean(self):
status = self.cleaned_data["status"].strip().lower()
if status == "guest" and not self.cleaned_data["selected_ctf"]:
raise ValidationError("Guests MUST have a selected_ctf")
if "status" in self.cleaned_data:
status = self.cleaned_data["status"]
if (
status == Member.StatusType.GUEST.value
and not self.cleaned_data["selected_ctf"]
):
raise ValidationError("Guests MUST have a selected_ctf")
return super(MemberUpdateForm, self).clean()


Expand All @@ -111,8 +114,8 @@ class Meta:
"visibility",
]

weight = forms.FloatField(min_value=0.0)
rating = forms.FloatField(min_value=0.0, required=False)
weight = forms.FloatField(min_value=1.0, required=True)
rating = forms.FloatField(min_value=0.0, required=True)


class ChallengeCreateForm(forms.ModelForm):
Expand Down
73 changes: 50 additions & 23 deletions ctfhub/helpers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import io
import os
import pathlib
import smtplib
import time
import uuid
from datetime import datetime
from functools import lru_cache
from typing import Any, TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Union

import django.core.mail
import django.utils.crypto
Expand All @@ -14,7 +15,6 @@
from django.conf import settings
from django.core.files.storage import get_storage_class


from ctfhub_project.settings import (
CTFHUB_ACCEPTED_IMAGE_EXTENSIONS,
CTFHUB_DEFAULT_CTF_LOGO,
Expand All @@ -33,13 +33,12 @@
EXCALIDRAW_ROOM_KEY_CHARSET,
EXCALIDRAW_ROOM_KEY_LENGTH,
HEDGEDOC_URL,
STATIC_URL,
IMAGE_URL,
USE_INTERNAL_HEDGEDOC,
)


if TYPE_CHECKING:
from ctfhub.models import Challenge
from ctfhub.models import ChallengeFile


@lru_cache(maxsize=1)
Expand Down Expand Up @@ -110,24 +109,39 @@ def check_note_id(id: str) -> bool:
return res.status_code == requests.codes.found


def get_file_magic(challenge_file: io.BufferedReader) -> str:
def get_file_magic(
challenge_file: Union[io.BufferedReader, pathlib.Path], use_mime: bool = False
) -> str:
"""
Returns the file description from its magic number (ex. 'PE32+ executable (console) x86-64, for MS Windows' )
Returns the file description from its magic number (ex. 'PE32+ executable (console) x86-64, for MS Windows' ), or a
MIME type if `use_mime` is True

Args:
challenge_file: File-like object
challenge_file: File-like object or a pathlib.Path
use_mime: specifies whether to get the output string as a MIME type

Raises:
TypeError if `challenge_file` has an invalid type

Returns:
str: the file description, or "" if the file doesn't exist on FS
"""

if isinstance(challenge_file, io.BufferedReader):
challenge_file.seek(0)
challenge_file_data = challenge_file.read()
elif isinstance(challenge_file, pathlib.Path):
challenge_file_data = challenge_file.open("rb").read()
else:
raise TypeError("Invalid type for `challenge_file`")

try:
challenge_file.seek(0) # Ensure file is read from beginning
return magic.from_buffer(challenge_file.read())
return magic.from_buffer(challenge_file_data, mime=use_mime)
except Exception:
return "Data"
return "Data" if not use_mime else "application/octet-stream"


def get_file_mime(challenge_file: io.BufferedReader) -> str:
def get_file_mime(challenge_file: Union[io.BufferedReader, pathlib.Path]) -> str:
"""
Returns the mime type associated to the file (ex. 'appication/pdf')

Expand All @@ -137,11 +151,7 @@ def get_file_mime(challenge_file: io.BufferedReader) -> str:
Returns:
str: the file mime type, or "application/octet-stream" if the file doesn't exist on FS
"""
try:
challenge_file.seek(0) # Ensure file is read from beginning
return magic.from_buffer(challenge_file.read(), mime=True)
except Exception:
return "application/octet-stream"
return get_file_magic(challenge_file, True)


def ctftime_parse_date(date: str) -> datetime:
Expand Down Expand Up @@ -189,6 +199,7 @@ def ctftime_fetch_ctfs(limit=100) -> list:
res = requests.get(
f"{CTFTIME_API_EVENTS_URL}?limit={limit}&start={start:.0f}&finish={end:.0f}",
headers={"user-agent": CTFTIME_USER_AGENT},
timeout=CTFHUB_HTTP_REQUEST_DEFAULT_TIMEOUT,
)
if res.status_code != requests.codes.ok:
raise RuntimeError(
Expand Down Expand Up @@ -216,7 +227,11 @@ def ctftime_get_ctf_info(ctftime_id: int) -> dict:
dict: JSON output from CTFTime
"""
url = f"{CTFTIME_API_EVENTS_URL}{ctftime_id}/"
res = requests.get(url, headers={"user-agent": CTFTIME_USER_AGENT})
res = requests.get(
url,
headers={"user-agent": CTFTIME_USER_AGENT},
timeout=CTFHUB_HTTP_REQUEST_DEFAULT_TIMEOUT,
)
if res.status_code != requests.codes.ok:
raise RuntimeError(
f"CTFTime service returned HTTP code {res.status_code} (expected {requests.codes.ok}): {res.reason}"
Expand All @@ -235,7 +250,7 @@ def ctftime_get_ctf_logo_url(ctftime_id: int) -> str:
Returns:
str: [description]
"""
default_logo = f"{STATIC_URL}images/{CTFHUB_DEFAULT_CTF_LOGO}"
default_logo = f"{IMAGE_URL}/{CTFHUB_DEFAULT_CTF_LOGO}"
if ctftime_id != 0:
try:
ctf_info = ctftime_get_ctf_info(ctftime_id)
Expand Down Expand Up @@ -374,19 +389,22 @@ def export_challenge_note(member, note_id: uuid.UUID) -> str:
str: The body of the note if successful; an empty string otherwise
"""
result = ""
url = which_hedgedoc()
print(url)
with requests.Session() as session:
h = session.post(
f"{HEDGEDOC_URL}/login",
f"{url}/login",
data={
"email": member.hedgedoc_username,
"password": member.hedgedoc_password,
},
allow_redirects=False,
)
if h.status_code == requests.codes.ok:
h2 = session.get(f"{HEDGEDOC_URL}{note_id}/download")
h2 = session.get(f"{url}{note_id}/download")
if h2.status_code == requests.codes.ok:
result = h2.text
session.post(f"{HEDGEDOC_URL}/logout")
session.post(f"{url}/logout")
return result


Expand All @@ -396,5 +414,14 @@ def get_named_storage(name: str) -> Any:
return storage_class(**config["OPTIONS"])


def get_challenge_upload_path(instance: "Challenge", filename: str) -> str:
def get_challenge_upload_path(instance: "ChallengeFile", filename: str) -> str:
"""Custom helper to retrieve the upload path for a given challenge file.

Args:
instance (ChallengeFile): _description_
filename (str): _description_

Returns:
str: _description_
"""
return f"files/{instance.challenge.id}/{filename}"
Loading