Skip to content

Commit

Permalink
Start adding tests to Tin (#28)
Browse files Browse the repository at this point in the history
Co-authored-by: Krishnan Shankar <[email protected]>
  • Loading branch information
JasonGrace2282 and krishnans2006 authored May 26, 2024
1 parent ba21058 commit 7774f9c
Show file tree
Hide file tree
Showing 15 changed files with 653 additions and 97 deletions.
18 changes: 10 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ defaults:
shell: bash

jobs:
format:
test:
runs-on: ubuntu-latest

strategy:
Expand All @@ -26,14 +26,16 @@ jobs:
python-version: ${{ matrix.python-version }}
cache: pipenv

- name: Install dependencies
run: |
set -e
pip install pipenv
- name: Install pipenv
run: pip install pipenv

# we have to check this first because
# installing autofixes the lock
- name: Check Pipfile.lock
run: pipenv verify

# Since we don't install dependencies, we have to have this
- name: Make dir for virtualenv
run: mkdir -p $HOME/.local/share/virtualenvs
- name: Install dependencies
run: pipenv install --deploy

- name: Run Tests
run: pipenv run python3 manage.py test
2 changes: 2 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ django-extensions = "~=3.2"
ipython = "~=8.12.3" # IPython follows NEP 29, so v8.13 does not support Python 3.8
mosspy = "~=1.0"
django-debug-toolbar = "~=4.3"
pytest-django = "~=4.8"

[dev-packages]
django-stubs = "*"
pre-commit = "*"
typing-extensions = "*"

[requires]
python_version = "3.8"
167 changes: 110 additions & 57 deletions Pipfile.lock

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import pytest

from django.utils.translation import activate

pytest_plugins = [
"tin.tests.fixtures",
]

@pytest.fixture(autouse=True)
def set_default_language():
activate('en')

@pytest.fixture(autouse=True)
def use_db_on_all_test(db):
pass
32 changes: 3 additions & 29 deletions create_debug_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,10 @@
)
exit()

from tin.apps.users.models import User

password = input("Enter password for all users: ")

# fmt: off
users = [
#[username, id, full_name, first_name, last_name, email, is_teacher, is_student, is_staff, is_superuser]
["2020jbeutner", 33538, "John Beutner", "John", "Beutner", "[email protected]", False, True, False, False],
["2020fouzhins", 33797, "Theo Ouzhinski", "Theo", "Ouzhinski", "[email protected]", False, True, False, False],
["2024kshankar", 1000891, "Krishnan Shankar", "Krishnan", "Shankar", "[email protected]", False, True, False, False],
["pcgabor", None, "Peter Gabor", "Peter", "Gabor", "[email protected]", True, False, False, False],
["admin", None, "Admin", "Admin", "Admin", "[email protected]", False, False, True, True],
]
# fmt: on
from tin.tests.create_users import add_users_to_database

for user_info in users:
print(f"Creating user {user_info[0]}")
if user_info[1] is None:
user = User.objects.get_or_create(username=user_info[0])[0]
else:
user = User.objects.get_or_create(id=user_info[1])[0]
user.username = user_info[0]
password = input("Enter password for all users: ")

user.full_name = user_info[2]
user.first_name = user_info[3]
user.last_name = user_info[4]
user.email = user_info[5]
user.is_teacher = user_info[6]
user.is_student = user_info[7]
user.is_staff = user_info[8]
user.is_superuser = user_info[9]
user.set_password(password)
user.save()
add_users_to_database(password=password, verbose=True)
26 changes: 26 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE="tin.settings"
python_files="tests.py test_*.py *_tests.py"
norecursedirs = ["media"]

[tool.black]
line-length = 100
exclude = '''
Expand Down Expand Up @@ -64,6 +69,8 @@ select = [
"N",
# Pylint
"PL",
# Pytest
"PT",
# pygrep hooks
"PGH",
# ruff
Expand All @@ -78,6 +85,8 @@ ignore = [
"PLR09",
# magic number comparison
"PLR2004",
# fixtures not returning anything should have leading underscore
"PT004",
# mutable class attrs annotated as typing.ClassVar
"RUF012",
# as recommended by https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
Expand All @@ -87,6 +96,23 @@ ignore = [
"E501",
]

[tool.ruff.lint.flake8-pytest-style]
fixture-parentheses = false
mark-parentheses = false
parametrize-names-type = "tuple"
parametrize-values-type = "tuple"

[tool.ruff.lint.pep8-naming]
extend-ignore-names = [
"User",
]

[tool.ruff.lint.per-file-ignores]
"__init__.py" = [
"F401",
"F403",
]

[tool.ruff.format]
docstring-code-format = true
line-ending = "lf"
Expand Down
44 changes: 44 additions & 0 deletions tin/apps/assignments/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import pytest

from django.urls import reverse

from tin.tests import is_redirect, teacher


@teacher
def test_create_folder(client, course) -> None:
response = client.post(
reverse("assignments:add_folder", args=[course.id]), {"name": "Fragment Shader"}
)
assert is_redirect(response)
assert course.folders.exists()


@teacher
@pytest.mark.parametrize("is_quiz", (-1, 0, 1, 2))
def test_create_assignment(client, course, is_quiz) -> None:
data = {
"name": "Write a Vertex Shader",
"description": "See https://learnopengl.com/Getting-started/Shaders",
"language": "P",
"is_quiz": is_quiz,
"filename": "vertex.glsl",
"points_possible": "300",
"due": "04/16/2025",
"grader_timeout": "300",
"submission_limit_count": "90",
"submission_limit_interval": "30",
"submission_limit_cooldown": "30",
}
response = client.post(
reverse("assignments:add", args=[course.id]),
data,
)
assert is_redirect(response)
assignment_set = course.assignments.filter(name__exact=data["name"])
assert assignment_set.count() == 1
assignment = assignment_set.get()
if is_quiz != -1:
assert assignment.quiz.action == str(is_quiz)
else:
assert not hasattr(assignment, "quiz")
46 changes: 46 additions & 0 deletions tin/apps/courses/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from django.urls import reverse

from tin.tests import is_login_redirect, is_redirect, teacher

from .models import Course


@teacher
def test_create_course(client, teacher) -> None:
course_name = "Foundations of CS"
response = client.post(
reverse("courses:create"),
{
"name": [course_name],
"teacher": [f"{teacher.id}"],
"sort_assignments_by": ["due_date"],
},
)
assert is_redirect(response)
filter_ = Course.objects.filter(name__exact=course_name)
assert filter_.count() == 1
course = filter_.get()
assert course.name == course_name


@teacher
def test_edit_course(client, course, teacher) -> None:
old_name = course.name
response = client.post(
reverse("courses:edit", args=[course.id]),
{
"name": [f"{old_name} and Bezier Curves"],
"teacher": [f"{teacher.id}"],
"sort_assignments_by": ["due_date"],
},
)

course.refresh_from_db()
assert is_redirect(response)
assert course.name == f"{old_name} and Bezier Curves"


def test_redirect(client) -> None:
response = client.get(reverse("courses:index"))

assert is_login_redirect(response)
6 changes: 3 additions & 3 deletions tin/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

TEST_RUNNER = "tin.tests.runner.PytestRunner"


# Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
Expand Down Expand Up @@ -193,8 +195,6 @@

USE_I18N = True

USE_L10N = True

USE_TZ = True


Expand Down Expand Up @@ -321,6 +321,6 @@
IMGBB_API_KEY = ""

try:
from .secret import * # noqa: F403
from .secret import *
except ImportError:
pass
2 changes: 2 additions & 0 deletions tin/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .assertions import *
from .utils import *
73 changes: 73 additions & 0 deletions tin/tests/assertions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""
A module containing frequently used
checks throughout the tests
"""

from __future__ import annotations

from typing import Literal

from django.http import HttpResponse, HttpResponseRedirect

__all__ = (
"is_redirect",
"not_redirect",
"is_login_redirect",
"not_login_redirect",
)


def is_redirect(
response: HttpResponse, url: str | None = None, part: Literal["base", "full", "end"] = "full"
) -> bool:
"""
Checks if ``response`` is a redirect.
Parameters
----------
url
Checks if redirect url is the same as url
part
Which part of the url to check. Can be base, full, or end.
"""
if not (isinstance(response, HttpResponseRedirect) and response.status_code == 302):
return False

if url is None:
return True

if part == "base":
return response.url.startswith(url)
if part == "full":
return response.url == url
if part == "end":
return part.endswith(url)
raise ValueError(f"Didn't recognize argument {part=}")


def not_redirect(response: HttpResponse) -> bool:
"""
Inverse of :meth:`is_redirect`
"""
return not is_redirect(response)


def is_login_redirect(response: HttpResponse, next: str | None = None) -> bool:
"""Checks if a response is a redirect to a login page
If the parameter ``next`` is passed, checks if the success_url
of the login is that url
"""
login_success_url = "/login/"
part = "base"
if next is not None and isinstance(next, str):
login_success_url += f"?next={next}"
part = "full"

return is_redirect(response, url=login_success_url, part=part)


def not_login_redirect(response: HttpResponse, **kwargs: str | None) -> bool:
"""Inverse of :meth:`is_login_redirect`"""
return not is_login_redirect(response, **kwargs)
45 changes: 45 additions & 0 deletions tin/tests/create_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from django.contrib.auth import get_user_model

__all__ = ("user_data", "add_users_to_database")

# fmt: off
user_data = [
#[username, is_teacher, is_student, is_staff, is_superuser]
["student", False, True, False, False],
["teacher", True, False, False, False],
["admin", False, False, True, True],
]
# fmt: on


def add_users_to_database(password: str, *, verbose: bool = True) -> None:
User = get_user_model()

for (
username,
is_teacher,
is_student,
is_staff,
is_superuser,
) in user_data:
user, created = User.objects.get_or_create(username=username)

if not created:
if verbose:
print(f"User {username} already exists, skipping...")
continue

if verbose:
print(f"Creating user {username}...")

name = username.capitalize()
user.full_name = name
user.first_name = name
user.last_name = name
user.email = f"{username}@example.com"
user.is_teacher = is_teacher
user.is_student = is_student
user.is_staff = is_staff
user.is_superuser = is_superuser
user.set_password(password)
user.save()
Loading

0 comments on commit 7774f9c

Please sign in to comment.