-
Notifications
You must be signed in to change notification settings - Fork 0
build: configure pytest-django and add dev dependencies #40
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
base: dev
Are you sure you want to change the base?
Changes from all commits
1149dab
8d3ed21
a3fc806
54059f0
5459ae8
cbd6269
3ab511e
bcc4148
6439b8f
6a44f11
706f8c5
656cb5d
7a3cf02
2cff119
74cb780
088539c
bdf5b2e
8997bda
38da811
bc0334e
23d6f05
a314ed8
97460ba
8d3f870
08bd759
82b7d1f
5aec605
fa1f27d
5a7a0b1
a7cbe24
2ad2872
88ba7f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # Generated by Django 5.2.8 on 2026-01-19 02:11 | ||
|
|
||
| from django.conf import settings | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ("web", "0011_remove_course_difficulty_score_and_more"), | ||
| migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddIndex( | ||
| model_name="vote", | ||
| index=models.Index( | ||
| fields=["course", "category", "value"], | ||
| name="web_vote_course__b117a9_idx", | ||
| ), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| import pytest | ||
| from django.conf import settings | ||
| from django.urls import reverse | ||
| from rest_framework.test import APIClient | ||
| from apps.web.tests import factories | ||
|
|
||
| # ------------------------------------------------------------------------- | ||
| # 1. Clients & Authentication | ||
| # ------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def base_client(): | ||
| """Returns an unauthenticated API client.""" | ||
| return APIClient() | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def user(db): | ||
| """Returns a saved user instance.""" | ||
| return factories.UserFactory() | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def auth_client(user, base_client): | ||
| """Returns an API client authenticated as the 'user' fixture.""" | ||
| base_client.force_authenticate(user=user) | ||
| return base_client | ||
|
|
||
|
|
||
| # ------------------------------------------------------------------------- | ||
| # 2. Data Fixtures (Models) | ||
| # ------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def course(db): | ||
| """Returns a saved course instance.""" | ||
| return factories.CourseFactory() | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def course_batch(db): | ||
| """Returns a batch of 3 general courses.""" | ||
| return factories.CourseFactory.create_batch(3) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def department_mixed_courses(db): | ||
| """Returns a mixed set of courses for filtering/sorting tests.""" | ||
| # Note: Using course_title to match current course model field | ||
| return [ | ||
| factories.CourseFactory( | ||
| department="MATH", | ||
| course_title="Honors Calculus II", | ||
| course_code="MATH1560J", | ||
| ), | ||
| factories.CourseFactory( | ||
| department="MATH", course_title="Calculus II", course_code="MATH1160J" | ||
| ), | ||
| factories.CourseFactory( | ||
| department="CHEM", course_title="Chemistry", course_code="CHEM2100J" | ||
| ), | ||
| ] | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def review(db, course, user, min_len): | ||
| """Returns a saved review instance belonging to 'user'.""" | ||
| return factories.ReviewFactory(course=course, user=user, comments="a" * min_len) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def other_review(db): | ||
| """Returns a review belonging to a different user for security testing.""" | ||
| from apps.web.tests.factories import UserFactory, ReviewFactory | ||
|
|
||
| return ReviewFactory(user=UserFactory()) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def course_factory(db): | ||
| """Access the factory class directly for custom batch creation.""" | ||
| return factories.CourseFactory | ||
|
|
||
|
|
||
| # ------------------------------------------------------------------------- | ||
| # 3. Validation & Payloads | ||
| # ------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def min_len(): | ||
| """Retrieves the minimum comment length from project settings.""" | ||
| return settings.WEB["REVIEW"]["COMMENT_MIN_LENGTH"] | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def valid_review_data(min_len): | ||
| """Generates a valid payload for review creation/update tests.""" | ||
| return { | ||
| "term": "23F", | ||
| "professor": "Dr. Testing", | ||
| "comments": "a" * min_len, | ||
| } | ||
|
|
||
|
|
||
| # ------------------------------------------------------------------------- | ||
| # 4. URL Fixtures (Routing) | ||
| # ------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def course_reviews_url(course): | ||
| """URL for listing/posting reviews for a specific course.""" | ||
| return reverse("course_review_api", kwargs={"course_id": course.id}) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def personal_reviews_list_url(): | ||
| """URL for the current user's personal review list.""" | ||
| return reverse("user_reviews_api") | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def personal_review_detail_url(review): | ||
| """URL for GET/PUT/DELETE a specific review owned by the user.""" | ||
| return reverse("user_review_api", kwargs={"review_id": review.id}) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def other_review_detail_url(other_review): | ||
| """URL for a review NOT owned by the current user (used for 404/Security).""" | ||
| return reverse("user_review_api", kwargs={"review_id": other_review.id}) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,88 +1,79 @@ | ||
| import factory | ||
| import factory.fuzzy | ||
| from django.contrib.auth.models import User | ||
|
|
||
| from apps.web import models | ||
| from lib import constants | ||
| # Import models from their individual files | ||
| from apps.web.models.course import Course | ||
| from apps.web.models.review import Review | ||
| from apps.web.models.student import Student | ||
| from apps.web.models.course_offering import CourseOffering | ||
|
|
||
|
|
||
| class UserFactory(factory.django.DjangoModelFactory): | ||
| class Meta: | ||
| model = User | ||
|
|
||
| username = factory.Faker("first_name") | ||
| email = factory.Faker("email") | ||
| first_name = factory.Faker("first_name") | ||
| last_name = factory.Faker("last_name") | ||
| is_active = True | ||
| username = factory.Sequence(lambda n: f"user_{n}") | ||
| email = factory.LazyAttribute(lambda o: f"{o.username}@example.com") | ||
|
|
||
| @classmethod | ||
| def _prepare(cls, create, **kwargs): | ||
| # thanks: https://gist.github.com/mbrochh/2433411 | ||
| password = factory.Faker("password") | ||
| if "password" in kwargs: | ||
| password = kwargs.pop("password") | ||
| user = super(UserFactory, cls)._prepare(create, **kwargs) | ||
| user.set_password(password) | ||
| if create: | ||
| user.save() | ||
| return user | ||
| def _create(cls, model_class, *args, **kwargs): | ||
| """Ensure password is hashed correctly so auth_client can log in""" | ||
| password = kwargs.pop("password", "password123") | ||
| obj = model_class(*args, **kwargs) | ||
| obj.set_password(password) | ||
| obj.save() | ||
| return obj | ||
|
|
||
|
|
||
| class CourseFactory(factory.django.DjangoModelFactory): | ||
| class Meta: | ||
| model = models.Course | ||
| model = Course | ||
|
|
||
| title = factory.Faker("words") | ||
| department = "COSC" | ||
| number = factory.Faker("random_number") | ||
| url = factory.Faker("url") | ||
| description = factory.Faker("text") | ||
| course_title = factory.Faker("sentence", nb_words=3) | ||
| department = factory.fuzzy.FuzzyChoice(["MATH", "PHYS", "EECS"]) | ||
| number = factory.Sequence(lambda n: 100 + n) | ||
|
|
||
| @factory.lazy_attribute | ||
| def course_code(self): | ||
| """Generates unique MATH100, PHYS101, etc.""" | ||
| return f"{self.department}{str(self.number):<04}J" | ||
|
|
||
| description = factory.Faker("paragraph") | ||
|
|
||
|
|
||
| class CourseOfferingFactory(factory.django.DjangoModelFactory): | ||
| class Meta: | ||
| model = models.CourseOffering | ||
| model = CourseOffering | ||
|
|
||
| course = factory.SubFactory(CourseFactory) | ||
|
|
||
| term = constants.CURRENT_TERM | ||
| section = factory.Faker("random_number") | ||
| term = "23F" | ||
| section = factory.Sequence(lambda n: n) | ||
| period = "2A" | ||
|
|
||
|
|
||
| class ReviewFactory(factory.django.DjangoModelFactory): | ||
| class Meta: | ||
| model = models.Review | ||
| model = Review | ||
|
|
||
| course = factory.SubFactory(CourseFactory) | ||
| user = factory.SubFactory(UserFactory) | ||
|
|
||
| term = "23F" | ||
| professor = factory.Faker("name") | ||
| term = constants.CURRENT_TERM | ||
| comments = factory.Faker("paragraph") | ||
|
|
||
|
|
||
| class DistributiveRequirementFactory(factory.django.DjangoModelFactory): | ||
| class Meta: | ||
| model = models.DistributiveRequirement | ||
|
|
||
| name = "ART" | ||
| distributive_type = models.DistributiveRequirement.DISTRIBUTIVE | ||
|
|
||
|
|
||
| class StudentFactory(factory.django.DjangoModelFactory): | ||
| class Meta: | ||
| model = models.Student | ||
| model = Student | ||
|
|
||
| user = factory.SubFactory(UserFactory) | ||
| confirmation_link = User.objects.make_random_password(length=16) | ||
|
|
||
|
|
||
| class VoteFactory(factory.django.DjangoModelFactory): | ||
| class DistributiveRequirementFactory(factory.django.DjangoModelFactory): | ||
| class Meta: | ||
| model = models.Vote | ||
| # Using string reference for potential distributive requirements model | ||
| model = "web.DistributiveRequirement" | ||
|
|
||
| value = 0 | ||
| course = factory.SubFactory(CourseFactory) | ||
| user = factory.SubFactory(UserFactory) | ||
| category = models.Vote.CATEGORIES.QUALITY | ||
| name = factory.Sequence(lambda n: f"Dist{n}") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,12 @@ def test_term_regex_allows_for_lower_and_upper_terms(self): | |
| and term_data.group("year") == "16" | ||
| and term_data.group("term") == "w" | ||
| ) | ||
| term_data = terms.term_regex.match("16F") | ||
| self.assertTrue( | ||
| term_data | ||
| and term_data.group("year") == "16" | ||
| and term_data.group("term") == "F" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed the term regex test to check 16W, 16w, and 16F separately in apps/web/tests/lib_tests/test_terms.py. Evidence: .venv/bin/pytest apps/web/tests -> 52 passed.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added concrete diff for the term assertion fix: term_data = terms.term_regex.match("16w")
self.assertTrue(
term_data
and term_data.group("year") == "16"
- and term_data.group("term") == "w"
- and term_data.group("term") == "F"
+ and term_data.group("term") == "w"
)
+term_data = terms.term_regex.match("16F")
+self.assertTrue(
+ term_data
+ and term_data.group("year") == "16"
+ and term_data.group("term") == "F"
+)This removes the contradictory condition and tests both cases explicitly. |
||
| ) | ||
|
|
||
| def test_term_regex_allows_for_current_term(self): | ||
| term_data = terms.term_regex.match(constants.CURRENT_TERM) | ||
|
|
@@ -52,7 +58,7 @@ def test_numeric_value_of_term_returns_0_if_bad_term(self): | |
| self.assertEqual(terms.numeric_value_of_term("fall"), 0) | ||
|
|
||
| def test_numeric_value_of_term_ranks_terms_in_correct_order(self): | ||
| correct_order = ["", "09w", "09S", "09X", "12F", "14x", "15W", "16S", "20x"] | ||
| correct_order = ["", "09w", "09S", "09X", "12F", "14x", "15w", "16S", "20x"] | ||
| shuffled_data = list(correct_order) | ||
| while correct_order == shuffled_data: | ||
| random.shuffle(shuffled_data) | ||
|
|
@@ -66,9 +72,10 @@ def test_numeric_value_of_term_gives_expected_numeric_value(self): | |
| self.assertEqual(terms.numeric_value_of_term("16W"), 161) | ||
|
|
||
| def test_is_valid_term_returns_false_if_in_future(self): | ||
| next_year = ( | ||
| int(terms.term_regex.match(constants.CURRENT_TERM).group("year")) + 1 | ||
| ) | ||
| term_data = terms.term_regex.match(constants.CURRENT_TERM) | ||
| if term_data is None: | ||
| raise AssertionError("CURRENT_TERM did not match term_regex") | ||
| next_year = int(term_data.group("year")) + 1 | ||
| self.assertFalse(terms.is_valid_term("{}f".format(next_year))) | ||
|
|
||
| def test_is_valid_term_returns_false_if_no_term(self): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import pytest | ||
| from django.urls import reverse | ||
|
|
||
| from apps.web.tests.factories import ReviewFactory | ||
|
|
||
|
|
||
| @pytest.mark.django_db | ||
| class TestUserStatusAPI: | ||
| def test_user_status_anonymous(self, base_client): | ||
| """Test that unauthenticated users get isAuthenticated=False""" | ||
| url = reverse("user_status") | ||
| response = base_client.get(url) | ||
|
|
||
| assert response.status_code == 200 | ||
| assert response.data["isAuthenticated"] is False | ||
| assert "username" not in response.data | ||
|
|
||
| def test_user_status_authenticated(self, auth_client, user): | ||
| """Test that authenticated users get isAuthenticated=True and their username""" | ||
| url = reverse("user_status") | ||
| # auth_client is already logged in via the fixture in conftest.py | ||
| response = auth_client.get(url) | ||
|
|
||
| assert response.status_code == 200 | ||
| assert response.data["isAuthenticated"] is True | ||
| assert response.data["username"] == user.username | ||
|
|
||
|
|
||
| @pytest.mark.django_db | ||
| class TestLandingPageAPI: | ||
| def test_landing_page_review_count(self, base_client, review): | ||
| """Verify landing page shows correct review statistics.""" | ||
| url = reverse("landing_api") | ||
| response = base_client.get(url) | ||
| assert response.status_code == 200 | ||
| # Should be at least 1 due to the 'review' fixture | ||
| assert response.data["review_count"] == 1 | ||
|
|
||
| def test_landing_page_review_count_empty(self, base_client, db): | ||
| """Verify review count is 0 when no reviews exist in the database.""" | ||
| url = reverse("landing_api") | ||
| response = base_client.get(url) | ||
|
|
||
| assert response.status_code == 200 | ||
| assert response.data["review_count"] == 0 | ||
|
|
||
| def test_landing_page_review_count_multiple(self, base_client, db): | ||
| """Verify review count returns the correct total when multiple reviews exist.""" | ||
| # Create 5 reviews across different courses/users | ||
| ReviewFactory.create_batch(5) | ||
|
|
||
| url = reverse("landing_api") | ||
| response = base_client.get(url) | ||
|
|
||
| assert response.status_code == 200 | ||
| assert response.data["review_count"] == 5 |
Uh oh!
There was an error while loading. Please reload this page.