From c578e14d1813911daa35f745de31747ed85e7141 Mon Sep 17 00:00:00 2001 From: Jarne Clauw <67628242+JarneClauw@users.noreply.github.com> Date: Fri, 17 May 2024 17:49:51 +0200 Subject: [PATCH 01/10] Auth tests --- backend/tests/endpoints/conftest.py | 27 ++++++++- .../tests/endpoints/course/courses_test.py | 3 +- backend/tests/endpoints/project_test.py | 60 +++++++++++++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/backend/tests/endpoints/conftest.py b/backend/tests/endpoints/conftest.py index de82cf72..db5041d3 100644 --- a/backend/tests/endpoints/conftest.py +++ b/backend/tests/endpoints/conftest.py @@ -15,14 +15,15 @@ from project.models.course_relation import CourseStudent, CourseAdmin from project.models.course_share_code import CourseShareCode from project.models.submission import Submission, SubmissionStatus -from project.models.project import Project +from project.models.project import Project, Runner ### AUTHENTICATION & AUTHORIZATION ### @fixture -def data_map(course: Course) -> dict[str, Any]: +def data_map(course: Course, project: Project) -> dict[str, Any]: """Map an id to data""" return { - "@course_id": course.course_id + "@course_id": course.course_id, + "@project_id": project.project_id } @fixture @@ -123,6 +124,26 @@ def course(session: Session, student: User, teacher: User, admin: User) -> Cours +### PROJECTS ### +@fixture +def project(session: Session, course: Course): + """Return a project entry""" + project = Project( + title="Test project", + description="Test project", + deadlines=[{"deadline":"2024-05-23T21:59:59", "description":"Final deadline"}], + course_id=course.course_id, + visible_for_students=True, + archived=False, + runner=Runner.GENERAL, + regex_expressions=[".*.pdf"] + ) + session.add(project) + session.commit() + return project + + + ### OTHER ### @pytest.fixture def valid_submission(valid_user_entry, valid_project_entry): diff --git a/backend/tests/endpoints/course/courses_test.py b/backend/tests/endpoints/course/courses_test.py index 99ee348e..ba18b8c9 100644 --- a/backend/tests/endpoints/course/courses_test.py +++ b/backend/tests/endpoints/course/courses_test.py @@ -24,7 +24,8 @@ class TestCourseEndpoint(TestEndpoint): authentication_tests("/courses", ["get", "post"]) + \ authentication_tests("/courses/@course_id", ["get", "patch", "delete"]) + \ authentication_tests("/courses/@course_id/students", ["get", "post", "delete"]) + \ - authentication_tests("/courses/@course_id/admins", ["get", "post", "delete"]) + authentication_tests("/courses/@course_id/admins", ["get", "post", "delete"]) + \ + authentication_tests("/courses/", []) @mark.parametrize("auth_test", authentication_tests, indirect=True) def test_authentication(self, auth_test: tuple[str, Any, str, bool]): diff --git a/backend/tests/endpoints/project_test.py b/backend/tests/endpoints/project_test.py index 0884145a..11bf314e 100644 --- a/backend/tests/endpoints/project_test.py +++ b/backend/tests/endpoints/project_test.py @@ -1,8 +1,68 @@ """Tests for project endpoints.""" import json +from typing import Any + +from pytest import mark from tests.utils.auth_login import get_csrf_from_login +from tests.endpoints.endpoint import ( + TestEndpoint, + authentication_tests, + authorization_tests +) + +class TestProjectsEndpoint(TestEndpoint): + """Class to test the projects API endpoint""" + + ### AUTHENTICATION ### + # Where is login required + authentication_tests = \ + authentication_tests("/projects", ["get", "post"]) + \ + authentication_tests("/projects/@project_id", ["get", "patch", "delete"]) + \ + authentication_tests("/projects/@project_id/assignment", ["get"]) + \ + authentication_tests("/projects/@project_id/submissions-download", ["get"]) + \ + authentication_tests("/projects/@project_id/latest-per-user", ["get"]) + + @mark.parametrize("auth_test", authentication_tests, indirect=True) + def test_authentication(self, auth_test: tuple[str, Any, str, bool]): + """Test the authentication""" + super().authentication(auth_test) + + + + ### AUTHORIZATION ### + # Who can access what + authorization_tests = \ + authorization_tests("/projects", "get", + ["student", "student_other", "teacher", "teacher_other", "admin", "admin_other"], + []) + \ + authorization_tests("/projects", "post", + ["teacher"], + ["student", "student_other", "teacher_other", "admin", "admin_other"]) + \ + authorization_tests("/projects/@project_id", "get", + ["student", "teacher", "admin"], + ["student_other", "teacher_other", "admin_other"]) + \ + authorization_tests("/projects/@project_id", "patch", + ["teacher", "admin"], + ["student", "student_other", "teacher_other", "admin_other"]) + \ + authorization_tests("/projects/@project_id", "delete", + ["teacher"], + ["student", "student_other", "teacher_other", "admin", "admin_other"]) + \ + authorization_tests("/projects/@project_id/assignment", "get", + ["student", "teacher", "admin"], + ["student_other", "teacher_other", "admin_other"]) + \ + authorization_tests("/projects/@project_id/submissions-download", "get", + ["teacher", "admin"], + ["student", "student_other", "teacher_other", "admin_other"]) + \ + authorization_tests("/projects/@project_id/latest-per-user", "get", + ["student", "teacher", "admin"], + ["student_other", "teacher_other", "admin_other"]) + + @mark.parametrize("auth_test", authorization_tests, indirect=True) + def test_authorization(self, auth_test: tuple[str, Any, str, bool]): + """Test the authorization""" + super().authorization(auth_test) def test_assignment_download(client, valid_project): """ From 91693f439f51625bddcdcfafbb0a784c49e8fb4e Mon Sep 17 00:00:00 2001 From: Jarne Clauw <67628242+JarneClauw@users.noreply.github.com> Date: Fri, 17 May 2024 17:58:55 +0200 Subject: [PATCH 02/10] Fixing the legacy warning --- .../project/endpoints/projects/project_submissions_download.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/project/endpoints/projects/project_submissions_download.py b/backend/project/endpoints/projects/project_submissions_download.py index 6ba93e93..c6bdcb4b 100644 --- a/backend/project/endpoints/projects/project_submissions_download.py +++ b/backend/project/endpoints/projects/project_submissions_download.py @@ -24,7 +24,7 @@ def get_last_submissions_per_user(project_id): Get the last submissions per user for a given project """ try: - project = Project.query.get(project_id) + project = db.session.get(Project, project_id) except SQLAlchemyError: return {"message": "Internal server error"}, 500 From b47ad3e88ac51bbea064bfcbb42434532f7277f9 Mon Sep 17 00:00:00 2001 From: Jarne Clauw <67628242+JarneClauw@users.noreply.github.com> Date: Sat, 18 May 2024 12:16:21 +0200 Subject: [PATCH 03/10] Adding auth decorators to the project submission download enpoints --- backend/project/endpoints/projects/project_last_submission.py | 2 ++ .../project/endpoints/projects/project_submissions_download.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/backend/project/endpoints/projects/project_last_submission.py b/backend/project/endpoints/projects/project_last_submission.py index 5b998c25..6cd4e719 100644 --- a/backend/project/endpoints/projects/project_last_submission.py +++ b/backend/project/endpoints/projects/project_last_submission.py @@ -6,6 +6,7 @@ from urllib.parse import urljoin from flask_restful import Resource from project.endpoints.projects.project_submissions_download import get_last_submissions_per_user +from project.utils.authentication import authorize_teacher_or_project_admin API_HOST = getenv("API_HOST") UPLOAD_FOLDER = getenv("UPLOAD_FOLDER") @@ -16,6 +17,7 @@ class SubmissionPerUser(Resource): Recourse to get all the submissions for users """ + @authorize_teacher_or_project_admin def get(self, project_id: int): """ Download all submissions for a project as a zip file. diff --git a/backend/project/endpoints/projects/project_submissions_download.py b/backend/project/endpoints/projects/project_submissions_download.py index c6bdcb4b..31d8e80a 100644 --- a/backend/project/endpoints/projects/project_submissions_download.py +++ b/backend/project/endpoints/projects/project_submissions_download.py @@ -14,6 +14,7 @@ from project.models.project import Project from project.models.submission import Submission from project.db_in import db +from project.utils.authentication import authorize_teacher_or_project_admin API_HOST = getenv("API_HOST") UPLOAD_FOLDER = getenv("UPLOAD_FOLDER") @@ -57,6 +58,8 @@ class SubmissionDownload(Resource): """ Resource to download all submissions for a project. """ + + @authorize_teacher_or_project_admin def get(self, project_id: int): """ Download all submissions for a project as a zip file. From a0c3efede45c6140f6f16c75780fce64cfbeae5e Mon Sep 17 00:00:00 2001 From: Jarne Clauw <67628242+JarneClauw@users.noreply.github.com> Date: Sat, 18 May 2024 14:56:01 +0200 Subject: [PATCH 04/10] Fixing auth tests --- backend/project/endpoints/projects/projects.py | 2 +- backend/tests/endpoints/project_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index e19538ad..0a7b4ced 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -82,7 +82,7 @@ def post(self, teacher_id=None): using flask_restfull parse lib """ project_json = parse_project_params() - + print(project_json) if not is_teacher_of_course(teacher_id, project_json["course_id"]): return {"message":"You are not the teacher of this course"}, 403 diff --git a/backend/tests/endpoints/project_test.py b/backend/tests/endpoints/project_test.py index 11bf314e..ecedc7a2 100644 --- a/backend/tests/endpoints/project_test.py +++ b/backend/tests/endpoints/project_test.py @@ -56,7 +56,7 @@ def test_authentication(self, auth_test: tuple[str, Any, str, bool]): ["teacher", "admin"], ["student", "student_other", "teacher_other", "admin_other"]) + \ authorization_tests("/projects/@project_id/latest-per-user", "get", - ["student", "teacher", "admin"], + ["teacher", "admin"], ["student_other", "teacher_other", "admin_other"]) @mark.parametrize("auth_test", authorization_tests, indirect=True) From 215d8cb6e25e1fec9069797eebcb64074a69d543 Mon Sep 17 00:00:00 2001 From: Jarne Clauw <67628242+JarneClauw@users.noreply.github.com> Date: Sat, 18 May 2024 20:28:06 +0200 Subject: [PATCH 05/10] Broken data field and query parameter tests --- backend/tests/endpoints/project_test.py | 51 ++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/backend/tests/endpoints/project_test.py b/backend/tests/endpoints/project_test.py index ecedc7a2..4a55eb65 100644 --- a/backend/tests/endpoints/project_test.py +++ b/backend/tests/endpoints/project_test.py @@ -5,11 +5,14 @@ from pytest import mark +from project.models.project import Runner from tests.utils.auth_login import get_csrf_from_login from tests.endpoints.endpoint import ( TestEndpoint, authentication_tests, - authorization_tests + authorization_tests, + data_field_type_tests, + query_parameter_tests ) class TestProjectsEndpoint(TestEndpoint): @@ -64,6 +67,52 @@ def test_authorization(self, auth_test: tuple[str, Any, str, bool]): """Test the authorization""" super().authorization(auth_test) + ### DATA FIELD TYPE ### + # Test a data field by passing a list of values for which it should return a bad request + # project_data_minimal = { + # "title": "A title", + # "description": "A description", + # "deadlines": [], + # "course_id": "@course_id", + # "visible_for_students": True, + # "archived": False, + # "runner": Runner.GENERAL, + # "regex_expressions": [], + # "assignment_file": "@assignment_file" + # } + # project_data_test = { + # "title": [None], + # "description": [None], + # "deadlines": [None, {"description": "deadline 1", "deadline": None}], + # "course_id": [None, "zero", 0], + # "visible_for_students": [None], + # "runner": [None, "general"], + # "regex_expressions": [None], + # "assignment_file": [None] + # } + # data_field_type_tests = \ + # data_field_type_tests("/projects", "post", "teacher", + # project_data_minimal, project_data_test) + \ + # data_field_type_tests("/projects/@project_id", "patch", "teacher", + # project_data_minimal, project_data_test) + + # @mark.parametrize("data_field_type_test", data_field_type_tests, indirect=True) + # def test_data_fields(self, data_field_type_test: tuple[str, Any, str, dict[str, Any]]): + # """Test a data field typing""" + # super().data_field_type(data_field_type_test) + + ### QUERY PARAMETER ### + # Test a query parameter, should return [] for wrong values + query_parameter_tests = \ + query_parameter_tests("/projects", "get", "student", ["project_id", "title", "course_id"]) + + @mark.parametrize("query_parameter_test", query_parameter_tests, indirect=True) + def test_query_parameters(self, query_parameter_test: tuple[str, Any, str, bool]): + """Test a query parameter""" + super().query_parameter(query_parameter_test) + + + def test_assignment_download(client, valid_project): """ Method for assignment download From dbf8ee526480abc9f48bf5191763c8144aa60142 Mon Sep 17 00:00:00 2001 From: Jarne Clauw <67628242+JarneClauw@users.noreply.github.com> Date: Sat, 18 May 2024 20:32:13 +0200 Subject: [PATCH 06/10] Fixing query parameter tests --- backend/project/endpoints/projects/projects.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index e19538ad..7c364d24 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -52,6 +52,10 @@ def get(self, uid=None): # Filter the projects based on the query parameters filters = dict(request.args) + invalid_filters = set(filters.keys()) - {"project_id", "title", "course_id"} + if invalid_filters: + data["message"] = f"Invalid query parameters {invalid_filters}" + return data, 400 conditions = [] for key, value in filters.items(): conditions.append(getattr(Project, key) == value) From 1c66ef5adb3051aa018b698cce787e254944b0fa Mon Sep 17 00:00:00 2001 From: Jarne Clauw <67628242+JarneClauw@users.noreply.github.com> Date: Sun, 19 May 2024 11:19:44 +0200 Subject: [PATCH 07/10] More tests but first fixing some issues --- .../project/endpoints/projects/projects.py | 2 +- backend/tests/endpoints/conftest.py | 73 +++- backend/tests/endpoints/project_test.py | 322 +++++++++++++----- 3 files changed, 310 insertions(+), 87 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 148c624a..7c364d24 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -86,7 +86,7 @@ def post(self, teacher_id=None): using flask_restfull parse lib """ project_json = parse_project_params() - print(project_json) + if not is_teacher_of_course(teacher_id, project_json["course_id"]): return {"message":"You are not the teacher of this course"}, 403 diff --git a/backend/tests/endpoints/conftest.py b/backend/tests/endpoints/conftest.py index 3f7717d7..64e44f0d 100644 --- a/backend/tests/endpoints/conftest.py +++ b/backend/tests/endpoints/conftest.py @@ -4,6 +4,9 @@ from datetime import datetime from zoneinfo import ZoneInfo from typing import Any +from zipfile import ZipFile +import os + import pytest from pytest import fixture, FixtureRequest from flask.testing import FlaskClient @@ -125,12 +128,13 @@ def course(session: Session, student: User, teacher: User, admin: User) -> Cours return course + ### PROJECTS ### @fixture def project(session: Session, course: Course): """Return a project entry""" project = Project( - title="Test project", + title="project", description="Test project", deadlines=[{"deadline":"2024-05-23T21:59:59", "description":"Final deadline"}], course_id=course.course_id, @@ -143,6 +147,45 @@ def project(session: Session, course: Course): session.commit() return project +@fixture +def project_invisible(session: Session, course: Course): + """Return a project entry that is not visible for the student""" + project = Project( + title="invisible project", + description="Test project", + deadlines=[{"deadline":"2024-05-23T21:59:59", "description":"Final deadline"}], + course_id=course.course_id, + visible_for_students=False, + archived=False, + runner=Runner.GENERAL, + regex_expressions=[".*.pdf"] + ) + session.add(project) + session.commit() + return project + +@fixture +def project_archived(session: Session, course: Course): + """Return a project entry that is not visible for the student""" + project = Project( + title="archived project", + description="Test project", + deadlines=[{"deadline":"2024-05-23T21:59:59", "description":"Final deadline"}], + course_id=course.course_id, + visible_for_students=True, + archived=True, + runner=Runner.GENERAL, + regex_expressions=[".*.pdf"] + ) + session.add(project) + session.commit() + return project + +@fixture +def projects(project: Project, project_invisible: Project, project_archived: Project): + """Return a list of project entries""" + return [project, project_invisible, project_archived] + ### SUBMISSIONS ### @@ -161,13 +204,6 @@ def submission(session: Session, student: User, project: Project): return submission ### FILES ### -@fixture -def file_empty(): - """Return an empty file""" - descriptor, name = tempfile.mkstemp() - with open(descriptor, "rb") as temp: - yield temp, name - @fixture def file_no_name(): """Return a file with no name""" @@ -176,15 +212,34 @@ def file_no_name(): temp.write("This is a test file.") with open(name, "rb") as temp: yield temp, "" + os.remove(name) @fixture def files(): """Return a temporary file""" - name = "/tmp/test.pdf" + name = "test.pdf" with open(name, "w", encoding="UTF-8") as file: file.write("This is a test file.") with open(name, "rb") as file: yield [(file, name)] + os.remove(name) + +@fixture +def file_assignment(): + """Return an assignment file for a project""" + assignment_file = "assignment.md" + assignment_content = "# Assignment" + with open(assignment_file, "w", encoding="UTF-8") as file: + file.write(assignment_content) + + zip_file = "project.zip" + with ZipFile(zip_file, "w") as zipf: + zipf.write(assignment_file) + + yield (zipf, zip_file) + + os.remove(assignment_file) + os.remove(zip_file) diff --git a/backend/tests/endpoints/project_test.py b/backend/tests/endpoints/project_test.py index 4a55eb65..b38b55ab 100644 --- a/backend/tests/endpoints/project_test.py +++ b/backend/tests/endpoints/project_test.py @@ -1,11 +1,12 @@ """Tests for project endpoints.""" -import json from typing import Any from pytest import mark +from flask.testing import FlaskClient -from project.models.project import Runner +from project.models.course import Course +from project.models.project import Project, Runner from tests.utils.auth_login import get_csrf_from_login from tests.endpoints.endpoint import ( TestEndpoint, @@ -67,6 +68,8 @@ def test_authorization(self, auth_test: tuple[str, Any, str, bool]): """Test the authorization""" super().authorization(auth_test) + + ### DATA FIELD TYPE ### # Test a data field by passing a list of values for which it should return a bad request # project_data_minimal = { @@ -101,6 +104,8 @@ def test_authorization(self, auth_test: tuple[str, Any, str, bool]): # """Test a data field typing""" # super().data_field_type(data_field_type_test) + + ### QUERY PARAMETER ### # Test a query parameter, should return [] for wrong values query_parameter_tests = \ @@ -113,97 +118,260 @@ def test_query_parameters(self, query_parameter_test: tuple[str, Any, str, bool] -def test_assignment_download(client, valid_project): - """ - Method for assignment download - """ + ### PROJECTS ### + def test_get_projects(self, client: FlaskClient, projects: list[Project]): + """Test getting all projects""" + response = client.get( + "/projects", + headers = {"X-CSRF-TOKEN":get_csrf_from_login(client, "student")} + ) + assert response.status_code == 200 + data = response.json["data"] + assert [project["title"] in ["project", "archived project"] for project in data] + + def test_get_projects_project_id( + self, client: FlaskClient, api_host: str, project: Project, projects: list[Project] + ): + """Test getting all projects for a given project_id""" + response = client.get( + f"/projects?project_id={project.project_id}", + headers = {"X-CSRF-TOKEN":get_csrf_from_login(client, "teacher")} + ) + assert response.status_code == 200 + data = response.json["data"] + assert len(data) == 1 + assert data[0]["project_id"] == f"{api_host}/projects/{project.project_id}" + + def test_get_projects_title( + self, client: FlaskClient, project: Project, projects: list[Project] + ): + """Test getting all projects for a given title""" + response = client.get( + f"/projects?title={project.title}", + headers = {"X-CSRF-TOKEN":get_csrf_from_login(client, "teacher")} + ) + assert response.status_code == 200 + data = response.json["data"] + assert len(data) == 1 + assert data[0]["title"] == project.title + + def test_get_projects_course_id( + self, client: FlaskClient, project: Project, projects: list[Project] + ): + """Test getting all projects for a given course_id""" + response = client.get( + f"/projects?course_id={project.course_id}", + headers = {"X-CSRF-TOKEN":get_csrf_from_login(client, "teacher")} + ) + assert response.status_code == 200 + assert len(response.json["data"]) == len(projects) - valid_project["deadlines"] = json.dumps(valid_project["deadlines"]) - with open("tests/resources/testzip.zip", "rb") as zip_file: - valid_project["assignment_file"] = zip_file - # post the project + def test_post_projects( + self, client: FlaskClient, course: Course, file_assignment + ): + """Test posting a new project""" csrf = get_csrf_from_login(client, "teacher") response = client.post( "/projects", headers = {"X-CSRF-TOKEN":csrf}, - data=valid_project, - content_type='multipart/form-data', + data = { + "title": "Test", + "description": "A test project", + "deadlines": [], + "course_id": course.course_id, + "visible_for_students": True, + "archived": False, + "runner": Runner.PYTHON, + "regex_expressions": [], + "assignment_file": file_assignment + } ) assert response.status_code == 201 - project_id = response.json["data"]["project_id"] - response = client.get(f"/projects/{project_id}/assignment", headers = {"X-CSRF-TOKEN":csrf}) - # 404 because the file is not found, no assignment.md in zip file - assert response.status_code == 404 + project_id = response.json["data"]["project_id"].split("/")[-1] + response = client.get( + f"/projects/{project_id}", + headers = {"X-CSRF-TOKEN":csrf} + ) + assert response.status_code == 200 + data = response.json["data"] + assert data["title"] == "Test" + assert data["course_id"] == course.course_id -def test_not_found_download(client): - """ - Test a not present project download - """ - csrf = get_csrf_from_login(client, "teacher2") - response = client.get("/projects", headers = {"X-CSRF-TOKEN":csrf}) - # get an index that doesnt exist - response = client.get("/projects/-1/assignments", headers = {"X-CSRF-TOKEN":csrf}) - assert response.status_code == 404 + ### PROJECT ### + def test_get_project(self, client: FlaskClient, api_host: str, project: Project): + """Test getting a project""" + response = client.get( + f"/projects/{project.project_id}", + headers = {"X-CSRF-TOKEN":get_csrf_from_login(client, "student")} + ) + assert response.status_code == 200 + assert response.json["data"]["project_id"] == f"{api_host}/projects/{project.project_id}" -def test_projects_home(client): - """Test home project endpoint.""" - csrf = get_csrf_from_login(client, "teacher1") - response = client.get("/projects", headers = {"X-CSRF-TOKEN":csrf}) - assert response.status_code == 200 + def test_patch_project(self, client: FlaskClient, project: Project): + """Test patching a project""" + csrf = get_csrf_from_login(client, "teacher") + response = client.patch( + f"/projects/{project.project_id}", + headers = {"X-CSRF-TOKEN":csrf}, + data = { + "title": "A new title" + } + ) + assert response.status_code == 200 + response = client.get( + f"/projects/{project.project_id}", + headers = {"X-CSRF-TOKEN":csrf} + ) + assert response.status_code == 200 + data = response.json["data"] + assert data["title"] == "A new title" + def test_delete_project(self, client: FlaskClient, project: Project): + """Test deleting a project""" + csrf = get_csrf_from_login(client, "teacher") + response = client.delete( + f"/projects/{project.project_id}", + headers = {"X-CSRF-TOKEN":csrf} + ) + assert response.status_code == 200 + response = client.get( + f"/projects/{project.project_id}", + headers = {"X-CSRF-TOKEN":csrf} + ) + assert response.status_code == 404 -def test_getting_all_projects(client): - """Test getting all projects""" - csrf = get_csrf_from_login(client, "teacher1") - response = client.get("/projects", headers = {"X-CSRF-TOKEN":csrf}) - assert response.status_code == 200 - assert isinstance(response.json['data'], list) -def test_post_project(client, valid_project): - """Test posting a project to the database and testing if it's present""" - valid_project["deadlines"] = json.dumps(valid_project["deadlines"]) - csrf = get_csrf_from_login(client, "teacher") - with open("tests/resources/testzip.zip", "rb") as zip_file: - valid_project["assignment_file"] = zip_file - # post the project + ### PROJECT ASSIGNMENT ### + def test_get_project_assignment(self, client: FlaskClient, course: Course, file_assignment): + """Test downloading the assignment of the project""" + csrf = get_csrf_from_login(client, "teacher") response = client.post( "/projects", - data=valid_project, - content_type='multipart/form-data', headers = {"X-CSRF-TOKEN":csrf} + headers = {"X-CSRF-TOKEN":csrf}, + data = { + "title": "Test", + "description": "A test project", + "course_id": course.course_id, + "visible_for_students": True, + "archived": False, + "runner": Runner.GENERAL, + "assignment_file": file_assignment + } ) + assert response.status_code == 201 + project_id = response.json["data"]["project_id"].split("/")[-1] + response = client.get( + f"/projects/{project_id}/assignment", + headers = {"X-CSRF-TOKEN":csrf} + ) + assert response.status_code == 200 + - assert response.status_code == 201 - - # check if the project with the id is present - project_id = response.json["data"]["project_id"] - response = client.get(f"/projects/{project_id}", headers = {"X-CSRF-TOKEN":csrf}) - - assert response.status_code == 200 - -def test_remove_project(client, valid_project_entry): - """Test removing a project to the datab and fetching it, testing if it's not present anymore""" - csrf = get_csrf_from_login(client, "teacher") - project_id = valid_project_entry.project_id - response = client.delete(f"/projects/{project_id}", headers = {"X-CSRF-TOKEN":csrf}) - assert response.status_code == 200 - - # check if the project isn't present anymore and the delete indeed went through - response = client.get(f"/projects/{project_id}", headers = {"X-CSRF-TOKEN":csrf}) - assert response.status_code == 404 - -def test_patch_project(client, valid_project_entry): - """Test functionality of the PATCH method for projects""" - csrf = get_csrf_from_login(client, "teacher") - project_id = valid_project_entry.project_id - - new_title = valid_project_entry.title + "hallo" - new_archived = not valid_project_entry.archived - - response = client.patch(f"/projects/{project_id}", json={ - "title": new_title, "archived": new_archived - }, headers = {"X-CSRF-TOKEN":csrf}) - - assert response.status_code == 200 + + ### PROJECT SUBMISSIONS DOWNLOAD ### + def test_get_project_submissions(self, client: FlaskClient, project: Project): + """Test downloading all the submissions for this project""" + + def test_get_project_latest_submissions(self, client: FlaskClient, project: Project): + """Test downloading the latest submissions for this project""" + + + + +# def test_assignment_download(client, valid_project): +# """ +# Method for assignment download +# """ + +# valid_project["deadlines"] = json.dumps(valid_project["deadlines"]) +# with open("tests/resources/testzip.zip", "rb") as zip_file: +# valid_project["assignment_file"] = zip_file +# # post the project +# csrf = get_csrf_from_login(client, "teacher") +# response = client.post( +# "/projects", +# headers = {"X-CSRF-TOKEN":csrf}, +# data=valid_project, +# content_type='multipart/form-data', +# ) +# assert response.status_code == 201 +# project_id = response.json["data"]["project_id"] +# response = client.get(f"/projects/{project_id}/assignment", headers = {"X-CSRF-TOKEN":csrf}) +# # 404 because the file is not found, no assignment.md in zip file +# assert response.status_code == 404 + + +# def test_not_found_download(client): +# """ +# Test a not present project download +# """ +# csrf = get_csrf_from_login(client, "teacher2") +# response = client.get("/projects", headers = {"X-CSRF-TOKEN":csrf}) +# # get an index that doesnt exist +# response = client.get("/projects/-1/assignments", headers = {"X-CSRF-TOKEN":csrf}) +# assert response.status_code == 404 + + +# def test_projects_home(client): +# """Test home project endpoint.""" +# csrf = get_csrf_from_login(client, "teacher1") +# response = client.get("/projects", headers = {"X-CSRF-TOKEN":csrf}) +# assert response.status_code == 200 + + +# def test_getting_all_projects(client): +# """Test getting all projects""" +# csrf = get_csrf_from_login(client, "teacher1") +# response = client.get("/projects", headers = {"X-CSRF-TOKEN":csrf}) +# assert response.status_code == 200 +# assert isinstance(response.json['data'], list) + + +# def test_post_project(client, valid_project): +# """Test posting a project to the database and testing if it's present""" +# valid_project["deadlines"] = json.dumps(valid_project["deadlines"]) +# csrf = get_csrf_from_login(client, "teacher") +# with open("tests/resources/testzip.zip", "rb") as zip_file: +# valid_project["assignment_file"] = zip_file +# # post the project +# response = client.post( +# "/projects", +# data=valid_project, +# content_type='multipart/form-data', headers = {"X-CSRF-TOKEN":csrf} +# ) + +# assert response.status_code == 201 + +# # check if the project with the id is present +# project_id = response.json["data"]["project_id"] +# response = client.get(f"/projects/{project_id}", headers = {"X-CSRF-TOKEN":csrf}) + +# assert response.status_code == 200 + +# def test_remove_project(client, valid_project_entry): +# """Test removing a project to the datab and fetching it, testing if it's not present anymore""" +# csrf = get_csrf_from_login(client, "teacher") +# project_id = valid_project_entry.project_id +# response = client.delete(f"/projects/{project_id}", headers = {"X-CSRF-TOKEN":csrf}) +# assert response.status_code == 200 + +# # check if the project isn't present anymore and the delete indeed went through +# response = client.get(f"/projects/{project_id}", headers = {"X-CSRF-TOKEN":csrf}) +# assert response.status_code == 404 + +# def test_patch_project(client, valid_project_entry): +# """Test functionality of the PATCH method for projects""" +# csrf = get_csrf_from_login(client, "teacher") +# project_id = valid_project_entry.project_id + +# new_title = valid_project_entry.title + "hallo" +# new_archived = not valid_project_entry.archived + +# response = client.patch(f"/projects/{project_id}", json={ +# "title": new_title, "archived": new_archived +# }, headers = {"X-CSRF-TOKEN":csrf}) + +# assert response.status_code == 200 From ae6f6a57bb6b47b3f64c66937fbedba4dd30607c Mon Sep 17 00:00:00 2001 From: Jarne Clauw <67628242+JarneClauw@users.noreply.github.com> Date: Thu, 23 May 2024 15:58:27 +0200 Subject: [PATCH 08/10] working tests --- backend/tests/endpoints/project_test.py | 271 +++++++----------------- 1 file changed, 77 insertions(+), 194 deletions(-) diff --git a/backend/tests/endpoints/project_test.py b/backend/tests/endpoints/project_test.py index b38b55ab..2c4f56ab 100644 --- a/backend/tests/endpoints/project_test.py +++ b/backend/tests/endpoints/project_test.py @@ -1,18 +1,17 @@ """Tests for project endpoints.""" from typing import Any +import json from pytest import mark from flask.testing import FlaskClient -from project.models.course import Course -from project.models.project import Project, Runner +from project.models.project import Project from tests.utils.auth_login import get_csrf_from_login from tests.endpoints.endpoint import ( TestEndpoint, authentication_tests, authorization_tests, - data_field_type_tests, query_parameter_tests ) @@ -70,42 +69,6 @@ def test_authorization(self, auth_test: tuple[str, Any, str, bool]): - ### DATA FIELD TYPE ### - # Test a data field by passing a list of values for which it should return a bad request - # project_data_minimal = { - # "title": "A title", - # "description": "A description", - # "deadlines": [], - # "course_id": "@course_id", - # "visible_for_students": True, - # "archived": False, - # "runner": Runner.GENERAL, - # "regex_expressions": [], - # "assignment_file": "@assignment_file" - # } - # project_data_test = { - # "title": [None], - # "description": [None], - # "deadlines": [None, {"description": "deadline 1", "deadline": None}], - # "course_id": [None, "zero", 0], - # "visible_for_students": [None], - # "runner": [None, "general"], - # "regex_expressions": [None], - # "assignment_file": [None] - # } - # data_field_type_tests = \ - # data_field_type_tests("/projects", "post", "teacher", - # project_data_minimal, project_data_test) + \ - # data_field_type_tests("/projects/@project_id", "patch", "teacher", - # project_data_minimal, project_data_test) - - # @mark.parametrize("data_field_type_test", data_field_type_tests, indirect=True) - # def test_data_fields(self, data_field_type_test: tuple[str, Any, str, dict[str, Any]]): - # """Test a data field typing""" - # super().data_field_type(data_field_type_test) - - - ### QUERY PARAMETER ### # Test a query parameter, should return [] for wrong values query_parameter_tests = \ @@ -166,49 +129,9 @@ def test_get_projects_course_id( assert response.status_code == 200 assert len(response.json["data"]) == len(projects) - def test_post_projects( - self, client: FlaskClient, course: Course, file_assignment - ): - """Test posting a new project""" - csrf = get_csrf_from_login(client, "teacher") - response = client.post( - "/projects", - headers = {"X-CSRF-TOKEN":csrf}, - data = { - "title": "Test", - "description": "A test project", - "deadlines": [], - "course_id": course.course_id, - "visible_for_students": True, - "archived": False, - "runner": Runner.PYTHON, - "regex_expressions": [], - "assignment_file": file_assignment - } - ) - assert response.status_code == 201 - project_id = response.json["data"]["project_id"].split("/")[-1] - response = client.get( - f"/projects/{project_id}", - headers = {"X-CSRF-TOKEN":csrf} - ) - assert response.status_code == 200 - data = response.json["data"] - assert data["title"] == "Test" - assert data["course_id"] == course.course_id - ### PROJECT ### - def test_get_project(self, client: FlaskClient, api_host: str, project: Project): - """Test getting a project""" - response = client.get( - f"/projects/{project.project_id}", - headers = {"X-CSRF-TOKEN":get_csrf_from_login(client, "student")} - ) - assert response.status_code == 200 - assert response.json["data"]["project_id"] == f"{api_host}/projects/{project.project_id}" - def test_patch_project(self, client: FlaskClient, project: Project): """Test patching a project""" csrf = get_csrf_from_login(client, "teacher") @@ -244,134 +167,94 @@ def test_delete_project(self, client: FlaskClient, project: Project): - ### PROJECT ASSIGNMENT ### - def test_get_project_assignment(self, client: FlaskClient, course: Course, file_assignment): - """Test downloading the assignment of the project""" +### OLD TESTS ### +def test_assignment_download(client, valid_project): + """ + Method for assignment download + """ + + valid_project["deadlines"] = json.dumps(valid_project["deadlines"]) + with open("tests/resources/testzip.zip", "rb") as zip_file: + valid_project["assignment_file"] = zip_file + # post the project csrf = get_csrf_from_login(client, "teacher") response = client.post( "/projects", headers = {"X-CSRF-TOKEN":csrf}, - data = { - "title": "Test", - "description": "A test project", - "course_id": course.course_id, - "visible_for_students": True, - "archived": False, - "runner": Runner.GENERAL, - "assignment_file": file_assignment - } + data=valid_project, + content_type='multipart/form-data', ) assert response.status_code == 201 - project_id = response.json["data"]["project_id"].split("/")[-1] - response = client.get( - f"/projects/{project_id}/assignment", - headers = {"X-CSRF-TOKEN":csrf} - ) - assert response.status_code == 200 - - - - ### PROJECT SUBMISSIONS DOWNLOAD ### - def test_get_project_submissions(self, client: FlaskClient, project: Project): - """Test downloading all the submissions for this project""" - - def test_get_project_latest_submissions(self, client: FlaskClient, project: Project): - """Test downloading the latest submissions for this project""" - - - - -# def test_assignment_download(client, valid_project): -# """ -# Method for assignment download -# """ - -# valid_project["deadlines"] = json.dumps(valid_project["deadlines"]) -# with open("tests/resources/testzip.zip", "rb") as zip_file: -# valid_project["assignment_file"] = zip_file -# # post the project -# csrf = get_csrf_from_login(client, "teacher") -# response = client.post( -# "/projects", -# headers = {"X-CSRF-TOKEN":csrf}, -# data=valid_project, -# content_type='multipart/form-data', -# ) -# assert response.status_code == 201 -# project_id = response.json["data"]["project_id"] -# response = client.get(f"/projects/{project_id}/assignment", headers = {"X-CSRF-TOKEN":csrf}) -# # 404 because the file is not found, no assignment.md in zip file -# assert response.status_code == 404 - - -# def test_not_found_download(client): -# """ -# Test a not present project download -# """ -# csrf = get_csrf_from_login(client, "teacher2") -# response = client.get("/projects", headers = {"X-CSRF-TOKEN":csrf}) -# # get an index that doesnt exist -# response = client.get("/projects/-1/assignments", headers = {"X-CSRF-TOKEN":csrf}) -# assert response.status_code == 404 - - -# def test_projects_home(client): -# """Test home project endpoint.""" -# csrf = get_csrf_from_login(client, "teacher1") -# response = client.get("/projects", headers = {"X-CSRF-TOKEN":csrf}) -# assert response.status_code == 200 - + project_id = response.json["data"]["project_id"] + response = client.get(f"/projects/{project_id}/assignment", headers = {"X-CSRF-TOKEN":csrf}) + # 404 because the file is not found, no assignment.md in zip file + assert response.status_code == 404 -# def test_getting_all_projects(client): -# """Test getting all projects""" -# csrf = get_csrf_from_login(client, "teacher1") -# response = client.get("/projects", headers = {"X-CSRF-TOKEN":csrf}) -# assert response.status_code == 200 -# assert isinstance(response.json['data'], list) +def test_not_found_download(client): + """ + Test a not present project download + """ + csrf = get_csrf_from_login(client, "teacher2") + response = client.get("/projects", headers = {"X-CSRF-TOKEN":csrf}) + # get an index that doesnt exist + response = client.get("/projects/-1/assignments", headers = {"X-CSRF-TOKEN":csrf}) + assert response.status_code == 404 + +def test_projects_home(client): + """Test home project endpoint.""" + csrf = get_csrf_from_login(client, "teacher1") + response = client.get("/projects", headers = {"X-CSRF-TOKEN":csrf}) + assert response.status_code == 200 + +def test_getting_all_projects(client): + """Test getting all projects""" + csrf = get_csrf_from_login(client, "teacher1") + response = client.get("/projects", headers = {"X-CSRF-TOKEN":csrf}) + assert response.status_code == 200 + assert isinstance(response.json['data'], list) + +def test_post_project(client, valid_project): + """Test posting a project to the database and testing if it's present""" + valid_project["deadlines"] = json.dumps(valid_project["deadlines"]) + csrf = get_csrf_from_login(client, "teacher") + with open("tests/resources/testzip.zip", "rb") as zip_file: + valid_project["assignment_file"] = zip_file + # post the project + response = client.post( + "/projects", + data=valid_project, + content_type='multipart/form-data', headers = {"X-CSRF-TOKEN":csrf} + ) + assert response.status_code == 201 -# def test_post_project(client, valid_project): -# """Test posting a project to the database and testing if it's present""" -# valid_project["deadlines"] = json.dumps(valid_project["deadlines"]) -# csrf = get_csrf_from_login(client, "teacher") -# with open("tests/resources/testzip.zip", "rb") as zip_file: -# valid_project["assignment_file"] = zip_file -# # post the project -# response = client.post( -# "/projects", -# data=valid_project, -# content_type='multipart/form-data', headers = {"X-CSRF-TOKEN":csrf} -# ) + # check if the project with the id is present + project_id = response.json["data"]["project_id"] + response = client.get(f"/projects/{project_id}", headers = {"X-CSRF-TOKEN":csrf}) -# assert response.status_code == 201 + assert response.status_code == 200 -# # check if the project with the id is present -# project_id = response.json["data"]["project_id"] -# response = client.get(f"/projects/{project_id}", headers = {"X-CSRF-TOKEN":csrf}) +def test_remove_project(client, valid_project_entry): + """Test removing a project to the datab and fetching it, testing if it's not present anymore""" + csrf = get_csrf_from_login(client, "teacher") + project_id = valid_project_entry.project_id + response = client.delete(f"/projects/{project_id}", headers = {"X-CSRF-TOKEN":csrf}) + assert response.status_code == 200 -# assert response.status_code == 200 + # check if the project isn't present anymore and the delete indeed went through + response = client.get(f"/projects/{project_id}", headers = {"X-CSRF-TOKEN":csrf}) + assert response.status_code == 404 -# def test_remove_project(client, valid_project_entry): -# """Test removing a project to the datab and fetching it, testing if it's not present anymore""" -# csrf = get_csrf_from_login(client, "teacher") -# project_id = valid_project_entry.project_id -# response = client.delete(f"/projects/{project_id}", headers = {"X-CSRF-TOKEN":csrf}) -# assert response.status_code == 200 +def test_patch_project(client, valid_project_entry): + """Test functionality of the PATCH method for projects""" + csrf = get_csrf_from_login(client, "teacher") + project_id = valid_project_entry.project_id -# # check if the project isn't present anymore and the delete indeed went through -# response = client.get(f"/projects/{project_id}", headers = {"X-CSRF-TOKEN":csrf}) -# assert response.status_code == 404 + new_title = valid_project_entry.title + "hallo" + new_archived = not valid_project_entry.archived -# def test_patch_project(client, valid_project_entry): -# """Test functionality of the PATCH method for projects""" -# csrf = get_csrf_from_login(client, "teacher") -# project_id = valid_project_entry.project_id + response = client.patch(f"/projects/{project_id}", json={ + "title": new_title, "archived": new_archived + }, headers = {"X-CSRF-TOKEN":csrf}) -# new_title = valid_project_entry.title + "hallo" -# new_archived = not valid_project_entry.archived - -# response = client.patch(f"/projects/{project_id}", json={ -# "title": new_title, "archived": new_archived -# }, headers = {"X-CSRF-TOKEN":csrf}) - -# assert response.status_code == 200 + assert response.status_code == 200 From d89057ed0f5c1aca4324a53851debad6908a7453 Mon Sep 17 00:00:00 2001 From: Jarne Clauw <67628242+JarneClauw@users.noreply.github.com> Date: Thu, 23 May 2024 16:28:28 +0200 Subject: [PATCH 09/10] remove bad param filter --- backend/project/endpoints/projects/projects.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 12360301..94813bbd 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -52,10 +52,6 @@ def get(self, uid=None): courses_student = [c[0] for c in courses_student] # Filter the projects based on the query parameters filters = dict(request.args) - invalid_filters = set(filters.keys()) - {"project_id", "title", "course_id"} - if invalid_filters: - data["message"] = f"Invalid query parameters {invalid_filters}" - return data, 400 conditions = [] for key, value in filters.items(): if key in Project.__table__.columns: From 908bce5fcbffa5e0a3b94a2c3eab118b1974325a Mon Sep 17 00:00:00 2001 From: Jarne Clauw <67628242+JarneClauw@users.noreply.github.com> Date: Thu, 23 May 2024 17:17:57 +0200 Subject: [PATCH 10/10] stuff --- backend/tests/endpoints/course/courses_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/tests/endpoints/course/courses_test.py b/backend/tests/endpoints/course/courses_test.py index 7d854b3f..f2e20012 100644 --- a/backend/tests/endpoints/course/courses_test.py +++ b/backend/tests/endpoints/course/courses_test.py @@ -24,8 +24,7 @@ class TestCourseEndpoint(TestEndpoint): authentication_tests("/courses", ["get", "post"]) + \ authentication_tests("/courses/@course_id", ["get", "patch", "delete"]) + \ authentication_tests("/courses/@course_id/students", ["get", "post", "delete"]) + \ - authentication_tests("/courses/@course_id/admins", ["get", "post", "delete"]) + \ - authentication_tests("/courses/", []) + authentication_tests("/courses/@course_id/admins", ["get", "post", "delete"]) @mark.parametrize("auth_test", authentication_tests, indirect=True) def test_authentication(self, auth_test: tuple[str, Any, str, bool, dict[str, Any]]):