From e43bf954cda45cd8f9e09bc3d164d037c8f246e1 Mon Sep 17 00:00:00 2001 From: Gerwoud Van den Eynden <62761483+Gerwoud@users.noreply.github.com> Date: Mon, 1 Apr 2024 13:11:33 +0200 Subject: [PATCH 1/2] Feature/backend/documentation (#94) * cooken on the projects documentation * #92 - Fixing missing /submissions * #92 - Fixing apilinter * requested changes * openapi object * changed to yaml file * small fix * grammar * json->yaml * pyyaml installed in dev.txt * linter --------- Co-authored-by: Jarne Clauw <67628242+JarneClauw@users.noreply.github.com> --- backend/dev-requirements.txt | 1 + backend/project/static/OpenAPI_Object.json | 2272 -------------------- backend/project/static/OpenAPI_Object.yaml | 1333 ++++++++++++ backend/tests.yaml | 2 +- backend/tests/endpoints/index_test.py | 10 +- 5 files changed, 1342 insertions(+), 2276 deletions(-) delete mode 100644 backend/project/static/OpenAPI_Object.json create mode 100644 backend/project/static/OpenAPI_Object.yaml diff --git a/backend/dev-requirements.txt b/backend/dev-requirements.txt index dd9b3470..fa950d3d 100644 --- a/backend/dev-requirements.txt +++ b/backend/dev-requirements.txt @@ -1,3 +1,4 @@ pytest pylint pylint-flask +pyyaml diff --git a/backend/project/static/OpenAPI_Object.json b/backend/project/static/OpenAPI_Object.json deleted file mode 100644 index ba0b5381..00000000 --- a/backend/project/static/OpenAPI_Object.json +++ /dev/null @@ -1,2272 +0,0 @@ -{ - "openapi": "3.0.1", - "info": { - "title": "Pigeonhole API", - "summary": "A project submission and grading API for University Ghent students and professors.", - "description": "The API built for the Pigeonhole application. It serves as an interface for student of University Ghent. They can submit solutions to projects created by their professors. Professors and their assistents can then review these submitions, grade them and define custom tests that automatically run on every submition. The API is built using the OpenAPI 3.1.0 specification.", - "version": "1.0.0", - "contact": { - "name": "Project discussion forum", - "url": "https://github.com/SELab-2/UGent-opgave/discussions", - "email": "Bart.Coppens@UGent.be" - }, - "x-authors": [ - { - "name": "Aron Buzogany", - "github": "https://github.com/AronBuzogany" - }, - { - "name": "Gerwoud Van den Eynden", - "github": "https://github.com/Gerwoud" - }, - { - "name": "Jarne Clauw", - "github": "https://github.com/JarneClauw" - }, - { - "name": "Siebe Vlietinck", - "github": "https://github.com/Vucis" - }, - { - "name": "Warre Provoost", - "github": "https://github.com/warreprovoost" - }, - { - "name": "Cedric Mekeirle", - "github": "https://github.com/JibrilExe" - }, - { - "name": "Matisse Sulzer", - "github": "https://github.com/Matisse-Sulzer" - } - ] - }, - "paths": { - "/projects": { - "get": { - "description": "Returns all projects from the database that the user has access to", - "responses": { - "200": { - "description": "A list of projects", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "project_id": { - "type": "integer" - }, - "description": { - "type": "string" - }, - "title": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "post": { - "description": "Upload a new project", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "assignment_file": { - "type": "string", - "format": "binary" - }, - "title": { "type": "string" }, - "description": { "type": "string" }, - "course_id": { "type": "integer" }, - "visible_for_students": { "type": "boolean" }, - "archived": { "type": "boolean" } - }, - "required": ["assignment_file", "title", "description", "course_id", "visible_for_students", "archived"] - } - } - } - }, - "responses": { - "201": { - "description": "Uploaded a new project succesfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "data": { - "type": "object" - }, - "url": { - "type": "string" - } - } - } - } - } - }, - "400": { - "description": "Bad formatted request for uploading a project", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "url": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "Something went wrong inserting model into the database", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error":{ - "type": "string" - }, - "url": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/projects/{id}": { - "get": { - "description": "Return a project with corresponding id", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "ID of the project to retrieve", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "A project with corresponding id", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "project_id": { - "type": "integer" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "assignment_file": { - "type": "string", - "format": "binary" - }, - "deadline": { - "type": "string" - }, - "course_id": { - "type": "integer" - }, - "visible_for_students": { - "type": "boolean" - }, - "archived": { - "type": "boolean" - }, - "test_path": { - "type": "string" - }, - "script_name": { - "type": "string" - }, - "regex_expressions": { - "type": "array" - } - } - } - } - } - }, - "404": { - "description": "An id that doesn't correspond to an existing project", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object" - }, - "message": { - "type": "string" - }, - "url": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "Something in the database went wrong fetching the project", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "url": { - "type": "string" - } - } - } - } - } - } - } - }, - "patch": { - "description": "Patch certain fields of a project", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "ID of the project to retrieve", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "assignment_file": { - "type": "string", - "format": "binary" - }, - "title": { "type": "string" }, - "description": { "type": "string" }, - "course_id": { "type": "integer" }, - "visible_for_students": { "type": "boolean" }, - "archived": { "type": "boolean" } - } - } - } - } - }, - "responses": { - "200": { - "description": "Patched a project succesfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object" - }, - "message": { - "type": "string" - }, - "url": { "type": "string" } - } - } - } - } - }, - "404": { - "description": "Tried to patch a project that is not present", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "url": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "Something went wrong in the database trying to patch the project", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "url": { - "type": "string" - } - } - } - } - } - } - } - }, - "delete": { - "description": "Delete a project with given id", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "ID of the project to retrieve", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Removed a project succesfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" }, - "url": { "type": "string" } - } - } - } - } - }, - "404": { - "description": "Tried to remove a project that is not present", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" }, - "url": { "type": "string" } - } - } - } - } - }, - "500": { - "description": "Something went wrong in the database trying to remove the project", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { "type": "string" }, - "url": { "type": "string" } - } - } - } - } - } - } - } - }, - "/courses": { - "get": { - "description": "Get a list of all courses.", - "responses": { - "200": { - "description": "Successfully retrieved all courses with given parameters", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "data": { - "type": "array", - "items": { - "type": "object", - "properties": { - "course_id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "ufora_id": { - "type": "string" - }, - "teacher": { - "type": "string" - }, - "url": { - "type": "string" - } - } - } - }, - "url": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "Internal server error.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - } - }, - "parameters": [ - { - "name": "name", - "in": "query", - "description": "Name of the course", - "schema": { - "type": "string" - } - }, - { - "name": "ufora_id", - "in": "query", - "description": "Ufora ID of the course", - "schema": { - "type": "string" - } - }, - { - "name": "teacher", - "in": "query", - "description": "Teacher of the course", - "schema": { - "type": "string" - } - } - ] - }, - "post": { - "description": "Create a new course.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of the course" - }, - "teacher": { - "type": "string", - "description": "Teacher of the course" - } - }, - "required": [ - "name", - "teacher" - ] - } - } - } - }, - "parameters": [ - { - "name": "uid", - "in": "query", - "description": "uid of the user sending the request", - "schema": { - "type": "string" - } - } - ], - "responses": { - "201": { - "description": "Course with name: {name} and course_id: {course_id} was successfully created", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "course_id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "teacher": { - "type": "string" - }, - "ufora_id": { - "type": "string" - } - } - }, - "url": { - "type": "string" - } - } - } - } - } - }, - "400": { - "description": "There was no uid in the request query.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "403": { - "description": "The user trying to create a course was unauthorized.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "404": { - "description": "The user trying to create a course was not found.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "Internal server error.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/courses/{course_id}": { - "get": { - "description": "Get a course by its ID.", - "parameters": [ - { - "name": "course_id", - "in": "path", - "description": "ID of the course", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Course found.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "ufora_id": { - "type": "string" - }, - "teacher": { - "type": "string" - }, - "admins": { - "type": "array", - "items": { - "type": "string" - } - }, - "students": { - "type": "array", - "items": { - "type": "string" - } - }, - "projects": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "url": { - "type": "string" - } - } - } - } - } - }, - "404": { - "description": "Course not found.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "Internal server error.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - } - } - }, - "delete": { - "description": "Delete a course by its ID.", - "parameters": [ - { - "name": "course_id", - "in": "path", - "description": "ID of the course", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "Course deleted.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string", - "examples": [ - "Successfully deleted course with course_id: {course_id}" - ] - }, - "url": { - "type": "string", - "examples": [ - "{API_URL}/courses" - ] - } - } - } - } - } - }, - "403": { - "description": "The user trying to delete the course was unauthorized.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "404": { - "description": "Course not found.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "Internal server error.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - } - } - }, - "patch": { - "description": "Update the course with given ID.", - "parameters": [ - { - "name": "course_id", - "in": "path", - "description": "ID of the course", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of the course" - }, - "teacher": { - "type": "string", - "description": "Teacher of the course" - }, - "ufora_id": { - "type": "string", - "description": "Ufora ID of the course" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Course updated.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "url": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "course_id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "teacher": { - "type": "string" - }, - "ufora_id": { - "type": "string" - } - } - } - } - } - } - } - }, - "403": { - "description": "The user trying to update the course was unauthorized.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "404": { - "description": "Course not found.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "Internal server error.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/courses/{course_id}/students": { - "get": { - "description": "Get a list of all students in a course.", - "parameters": [ - { - "name": "course_id", - "in": "path", - "description": "ID of the course", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Successfully retrieved all students of course.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "string" - } - }, - "url": { - "type": "string" - } - } - } - } - } - }, - "404": { - "description": "Course not found.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "Internal server error.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - } - } - }, - "post": { - "description": "Assign students to a course.", - "parameters": [ - { - "name": "course_id", - "in": "path", - "description": "ID of the course", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "students", - "in": "body", - "description": "list of uids of the students to be assigned to the course", - "required": true, - "schema": { - "type": "array" - } - }, - { - "name": "uid", - "in": "query", - "description": "uid of the user sending the request", - "schema": { - "type": "string" - } - } - ], - "responses": { - "201": { - "description": "Students assigned to course.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string", - "examples": [ - "User were succesfully added to the course" - ] - }, - "url": { - "type": "string", - "examples": [ - "http://api.example.com/courses/123/students" - ] - }, - "data": { - "type": "object", - "properties": { - "students": { - "type": "array", - "items": { - "type": "string", - "examples": [ - "http://api.example.com/users/123" - ] - } - } - } - } - } - } - } - } - }, - "400": { - "description": "There was no uid in the request query.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "403": { - "description": "The user trying to assign students to the course was unauthorized.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "404": { - "description": "Course not found.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "Internal server error.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - } - } - }, - "delete": { - "description": "Remove students from a course.", - "parameters": [ - { - "name": "course_id", - "in": "path", - "description": "ID of the course", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "students", - "in": "body", - "description": "list of uids of the students to be removed from the course", - "required": true, - "schema": { - "type": "array" - } - }, - { - "name": "uid", - "in": "query", - "description": "uid of the user sending the request", - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "Students removed from course.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string", - "examples": "User were succesfully removed from the course" - }, - "url": { - "type": "string", - "examples": [ - "API_URL + /courses/ + str(course_id) + /students" - ] - } - } - } - } - } - }, - "400": { - "description": "There was no uid in the request query.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "403": { - "description": "The user trying to remove students from the course was unauthorized.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "404": { - "description": "Course not found.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "Internal server error.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/courses/{course_id}/admins": { - "get": { - "description": "Get a list of all admins in a course.", - "parameters": [ - { - "name": "course_id", - "in": "path", - "description": "ID of the course", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Successfully retrieved all admins of course.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "string" - } - }, - "url": { - "type": "string" - } - } - } - } - } - }, - "404": { - "description": "Course not found.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "Internal server error.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - } - } - }, - "post": { - "description": "Assign admins to a course.", - "parameters": [ - { - "name": "course_id", - "in": "path", - "description": "ID of the course", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "admin_uid", - "in": "body", - "description": "uid of the admin to be assigned to the course", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "uid", - "in": "query", - "description": "uid of the user sending the request", - "schema": { - "type": "string" - } - } - ], - "responses": { - "201": { - "description": "User were successfully added to the course.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string", - "examples": [ - "User were successfully added to the course." - ] - }, - "url": { - "type": "string", - "examples": [ - "http://api.example.com/courses/123/students" - ] - }, - "data": { - "type": "object", - "properties": { - "students": { - "type": "array", - "items": { - "type": "string" - }, - "examples": [ - "http://api.example.com/users/1", - "http://api.example.com/users/2" - ] - } - } - } - } - } - } - } - }, - "400": { - "description": "There was no uid in the request query.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "403": { - "description": "The user trying to assign admins to the course was unauthorized.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "404": { - "description": "Course not found.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "Internal server error.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - } - } - }, - "delete": { - "description": "Remove an admin from a course.", - "parameters": [ - { - "name": "course_id", - "in": "path", - "description": "ID of the course", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "admin_uid", - "in": "body", - "description": "uid of the admin to be removed from the course", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "uid", - "in": "query", - "description": "uid of the user sending the request", - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "User was successfully removed from the course admins.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string", - "examples": [ - "User was successfully removed from the course admins." - ] - }, - "url": { - "type": "string", - "examples": [ - "API_URL + /courses/ + str(course_id) + /students" - ] - } - } - } - } - } - }, - "400": { - "description": "There was no uid in the request query.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "403": { - "description": "The user trying to remove the admin from the course was unauthorized.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "404": { - "description": "Course not found.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "Internal server error.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/users": { - "get": { - "summary": "Get all users", - "responses": { - "200": { - "description": "A list of users", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uid": { - "type": "string" - }, - "is_teacher": { - "type": "boolean" - }, - "is_admin": { - "type": "boolean" - } - }, - "required": [ - "uid", - "is_teacher", - "is_admin" - ] - } - } - } - } - } - } - }, - "post": { - "summary": "Create a new user", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "uid": { - "type": "string" - }, - "is_teacher": { - "type": "boolean" - }, - "is_admin": { - "type": "boolean" - } - }, - "required": [ - "uid", - "is_teacher", - "is_admin" - ] - } - } - } - }, - "responses": { - "201": { - "description": "User created successfully" - }, - "400": { - "description": "Invalid request data" - }, - "415": { - "description": "Unsupported Media Type. Expected JSON." - }, - "500": { - "description": "An error occurred while creating the user" - } - } - }, - "/users/{user_id}": { - "get": { - "summary": "Get a user by ID", - "parameters": [ - { - "name": "user_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "A user", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "uid": { - "type": "string" - }, - "is_teacher": { - "type": "boolean" - }, - "is_admin": { - "type": "boolean" - } - }, - "required": [ - "uid", - "is_teacher", - "is_admin" - ] - } - } - } - }, - "404": { - "description": "User not found" - } - } - }, - "patch": { - "summary": "Update a user's information", - "parameters": [ - { - "name": "user_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "is_teacher": { - "type": "boolean" - }, - "is_admin": { - "type": "boolean" - } - }, - "required": [ - "is_teacher", - "is_admin" - ] - } - } - } - }, - "responses": { - "200": { - "description": "User updated successfully" - }, - "404": { - "description": "User not found" - }, - "415": { - "description": "Unsupported Media Type. Expected JSON." - }, - "500": { - "description": "An error occurred while patching the user" - } - } - }, - "delete": { - "summary": "Delete a user", - "parameters": [ - { - "name": "user_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "User deleted successfully" - }, - "404": { - "description": "User not found" - }, - "500": { - "description": "An error occurred while deleting the user" - } - } - } - } - } - }, - "/submissions": { - "get": { - "summary": "Gets the submissions", - "parameters": [ - { - "name": "uid", - "in": "query", - "description": "User ID", - "schema": { - "type": "string" - } - }, - { - "name": "project_id", - "in": "query", - "description": "Project ID", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Successfully retrieved a list of submission URLs", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri" - }, - "message": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "submissions": "array", - "items": { - "type": "string", - "format": "uri" - } - } - } - } - } - } - } - }, - "400": { - "description": "An invalid user or project is given", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri" - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "An internal server error occurred", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - }, - "post": { - "summary": "Posts a new submission to a project", - "requestBody": { - "description": "Form data", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "uid": { - "type": "string", - "required": true - }, - "project_id": { - "type": "integer", - "required": true - }, - "files": { - "type": "array", - "items": { - "type": "file" - } - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Successfully posts the submission and retrieves its data", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri" - }, - "message": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "user": { - "type": "string", - "format": "uri" - }, - "project": { - "type": "string", - "format": "uri" - }, - "grading": { - "type": "integer" - }, - "time": { - "type": "string", - "format": "date-time" - }, - "path": { - "type": "string" - }, - "status": { - "type": "boolean" - } - } - } - } - } - } - } - }, - "400": { - "description": "An invalid user, project or list of files is given", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri" - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "An internal server error occurred", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/submissions/{submission_id}": { - "get": { - "summary": "Gets the submission", - "responses": { - "200": { - "description": "Successfully retrieved the submission", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri" - }, - "message": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "submission": { - "type": "object", - "properties": { - "submission_id": { - "type": "integer" - }, - "user": { - "type": "string", - "format": "uri" - }, - "project": { - "type": "string", - "format": "uri" - }, - "grading": { - "type": "integer", - "nullable": true - }, - "time": { - "type": "string", - "format": "date-time" - }, - "path": { - "type": "string" - }, - "status": { - "type": "boolean" - } - } - } - } - } - } - } - } - } - }, - "404": { - "description": "An invalid submission id is given", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri" - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "An internal server error occurred", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - }, - "patch": { - "summary": "Patches the submission", - "requestBody": { - "description": "The submission data", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "grading": { - "type": "integer", - "minimum": 0, - "maximum": 20 - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Successfully patches the submission and retrieves its data", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri" - }, - "message": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "user": { - "type": "string", - "format": "uri" - }, - "project": { - "type": "string", - "format": "uri" - }, - "grading": { - "type": "integer" - }, - "time": { - "type": "string", - "format": "date-time" - }, - "path": { - "type": "string" - }, - "status": { - "type": "boolean" - } - } - } - } - } - } - } - }, - "400": { - "description": "An invalid submission grading is given", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri" - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "404": { - "description": "An invalid submission id is given", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri" - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "An internal server error occurred", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - }, - "delete": { - "summary": "Deletes the submission", - "responses": { - "200": { - "description": "Successfully deletes the submission", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri" - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "404": { - "description": "An invalid submission id is given", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri" - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "An internal server error occurred", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - }, - "parameters": [ - { - "name": "submission_id", - "in": "path", - "description": "Submission ID", - "required": true, - "schema": { - "type": "integer" - } - } - ] - } -} \ No newline at end of file diff --git a/backend/project/static/OpenAPI_Object.yaml b/backend/project/static/OpenAPI_Object.yaml new file mode 100644 index 00000000..6b049c98 --- /dev/null +++ b/backend/project/static/OpenAPI_Object.yaml @@ -0,0 +1,1333 @@ +--- +openapi: 3.0.1 +info: + title: Pigeonhole API + summary: A project submission and grading API for University Ghent students and + professors. + description: The API built for the Pigeonhole application. It serves as an interface + for student of University Ghent. They can submit solutions to projects created + by their professors. Professors and their assistents can then review these submitions, + grade them and define custom tests that automatically run on every submition. + The API is built using the OpenAPI 3.1.0 specification. + version: 1.0.0 + contact: + name: Project discussion forum + url: https://github.com/SELab-2/UGent-opgave/discussions + email: Bart.Coppens@UGent.be + x-authors: + - name: Aron Buzogany + github: https://github.com/AronBuzogany + - name: Gerwoud Van den Eynden + github: https://github.com/Gerwoud + - name: Jarne Clauw + github: https://github.com/JarneClauw + - name: Siebe Vlietinck + github: https://github.com/Vucis + - name: Warre Provoost + github: https://github.com/warreprovoost + - name: Cedric Mekeirle + github: https://github.com/JibrilExe + - name: Matisse Sulzer + github: https://github.com/Matisse-Sulzer +paths: + "/projects": + get: + description: Returns all projects from the database that the user has access + to + responses: + '200': + description: A list of projects + content: + application/json: + schema: + type: array + items: + type: object + properties: + project_id: + type: integer + description: + type: string + title: + type: string + example: + - project_id: 1 + description: Project 1 description + title: Project 1 + - project_id: 2 + description: Project 2 description + title: Project 2 + '500': + description: Internal Server Error + post: + description: Upload a new project + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + assignment_file: + type: string + format: binary + title: + type: string + description: + type: string + course_id: + type: integer + visible_for_students: + type: boolean + archived: + type: boolean + required: + - assignment_file + - title + - description + - course_id + - visible_for_students + - archived + responses: + '201': + description: Project created successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + data: + type: object + url: + type: string + '400': + description: Bad formatted request for uploading a project + content: + application/json: + schema: + type: object + properties: + message: + type: string + url: + type: string + '500': + $ref: '#/components/responses/InternalError' + "/projects/{project_id}": + get: + description: Return a project with corresponding id + parameters: + - name: project_id + in: path + description: ID of the project to retrieve + required: true + schema: + type: integer + responses: + '200': + description: Project details retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + '404': + description: An id that doesn't correspond to an existing project + content: + application/json: + schema: + type: object + properties: + data: + type: object + message: + type: string + url: + type: string + '500': + $ref: '#/components/responses/InternalError' + patch: + description: Patch certain fields of a project + parameters: + - name: id + in: path + description: ID of the project to retrieve + required: true + schema: + type: integer + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + assignment_file: + type: string + format: binary + title: + type: string + description: + type: string + course_id: + type: integer + visible_for_students: + type: boolean + archived: + type: boolean + responses: + '200': + description: Patched a project succesfully + content: + application/json: + schema: + type: object + properties: + data: + type: object + message: + type: string + url: + type: string + '404': + description: Tried to patch a project that is not present + content: + application/json: + schema: + type: object + properties: + message: + type: string + url: + type: string + '500': + $ref: '#/components/responses/InternalError' + delete: + description: Delete a project with corresponding project id and all of its submissions in cascade + parameters: + - name: id + in: path + description: ID of the project to retrieve + required: true + schema: + type: integer + responses: + '200': + description: Project deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + url: + type: string + '404': + description: Tried to remove a project that is not present + content: + application/json: + schema: + type: object + properties: + error: + type: string + url: + type: string + "/projects/{id}/assignments": + get: + description: Get the assignment files of project with given id + parameters: + - name: project_id + in: path + description: ID of the project to retrieve + required: true + schema: + type: integer + responses: + '200': + description: Successfully downloaded the assignment files of the project + content: + multipart/form-data: + schema: + type: string + format: binary + '404': + description: The project of which you wanted to get the assignment files of doesn't exist + '500': + $ref: '#/components/responses/InternalError' + "/courses": + get: + description: Get a list of all courses. + responses: + '200': + description: Successfully retrieved all courses with given parameters + content: + application/json: + schema: + type: object + properties: + message: + type: string + data: + type: array + items: + type: object + properties: + course_id: + type: integer + name: + type: string + ufora_id: + type: string + teacher: + type: string + url: + type: string + url: + type: string + '500': + $ref: '#/components/responses/InternalError' + parameters: + - name: name + in: query + description: Name of the course + schema: + type: string + - name: ufora_id + in: query + description: Ufora ID of the course + schema: + type: string + - name: teacher + in: query + description: Teacher of the course + schema: + type: string + post: + description: Create a new course. + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: Name of the course + teacher: + type: string + description: Teacher of the course + required: + - name + - teacher + parameters: + - name: uid + in: query + description: uid of the user sending the request + schema: + type: string + responses: + '201': + description: 'Course with name: {name} and course_id: {course_id} was successfully + created' + content: + application/json: + schema: + type: object + properties: + message: + type: string + data: + type: object + properties: + course_id: + type: string + name: + type: string + teacher: + type: string + ufora_id: + type: string + url: + type: string + '400': + description: There was no uid in the request query. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '403': + description: The user trying to create a course was unauthorized. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '404': + description: The user trying to create a course was not found. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '500': + $ref: '#/components/responses/InternalError' + "/courses/{course_id}": + get: + description: Get a course by its ID. + parameters: + - name: course_id + in: path + description: ID of the course + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/schemas/Course' + '404': + description: Course not found. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '500': + $ref: '#/components/responses/InternalError' + delete: + description: Delete a course by its ID. + parameters: + - name: course_id + in: path + description: ID of the course + required: true + schema: + type: string + responses: + '204': + description: Course deleted. + content: + application/json: + schema: + type: object + properties: + message: + type: string + examples: + - 'Successfully deleted course with course_id: {course_id}' + url: + type: string + examples: + - "{API_URL}/courses" + '403': + description: The user trying to delete the course was unauthorized. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '404': + description: Course not found. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '500': + $ref: '#/components/responses/InternalError' + patch: + description: Update the course with given ID. + parameters: + - name: course_id + in: path + description: ID of the course + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: Name of the course + teacher: + type: string + description: Teacher of the course + ufora_id: + type: string + description: Ufora ID of the course + responses: + '200': + description: Course updated. + content: + application/json: + schema: + type: object + properties: + message: + type: string + url: + type: string + data: + type: object + properties: + course_id: + type: string + name: + type: string + teacher: + type: string + ufora_id: + type: string + '403': + description: The user trying to update the course was unauthorized. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '404': + description: Course not found. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '500': + $ref: '#/components/responses/InternalError' + "/courses/{course_id}/students": + get: + description: Get a list of all students in a course. + parameters: + - name: course_id + in: path + description: ID of the course + required: true + schema: + type: string + responses: + '200': + description: Successfully retrieved all students of course. + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + type: string + url: + type: string + '404': + description: Course not found. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '500': + $ref: '#/components/responses/InternalError' + post: + description: Assign students to a course. + parameters: + - name: course_id + in: path + description: ID of the course + required: true + schema: + type: string + - name: students + in: body + description: List of uids of the students to be assigned to the course + required: true + schema: + type: array + - name: uid + in: query + description: Uid of the user sending the request + schema: + type: string + responses: + '201': + description: Students assigned to course. + content: + application/json: + schema: + type: object + properties: + message: + type: string + examples: + - User was succesfully added to the course + url: + type: string + examples: + - http://api.example.com/courses/123/students + data: + type: object + properties: + students: + type: array + items: + type: string + examples: + - http://api.example.com/users/123 + '400': + description: There was no uid in the request query. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '403': + description: The user trying to assign students to the course was unauthorized. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '404': + description: Course not found. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '500': + $ref: '#/components/responses/InternalError' + delete: + description: Remove students from a course. + parameters: + - name: course_id + in: path + description: ID of the course + required: true + schema: + type: string + - name: students + in: body + description: List of uids of the students to be removed from the course + required: true + schema: + type: array + - name: uid + in: query + description: Uid of the user sending the request + schema: + type: string + responses: + '204': + description: Students removed from course. + content: + application/json: + schema: + type: object + properties: + message: + type: string + examples: User was succesfully removed from the course + url: + type: string + examples: + - API_URL + /courses/ + str(course_id) + /students + '400': + description: There was no uid in the request query. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '403': + description: The user trying to remove students from the course was unauthorized. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '404': + description: Course not found. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '500': + $ref: '#/components/responses/InternalError' + "/courses/{course_id}/admins": + get: + description: Get a list of all admins in a course. + parameters: + - name: course_id + in: path + description: ID of the course + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/schemas/Course' + '404': + description: Course not found. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '500': + $ref: '#/components/responses/InternalError' + post: + description: Assign admins to a course. + parameters: + - name: course_id + in: path + description: ID of the course + required: true + schema: + type: string + - name: admin_uid + in: body + description: Uid of the admin to be assigned to the course + required: true + schema: + type: string + - name: uid + in: query + description: Uid of the user sending the request + schema: + type: string + responses: + '201': + description: User were successfully added to the course. + content: + application/json: + schema: + type: object + properties: + message: + type: string + examples: + - User was successfully added to the course. + url: + type: string + examples: + - http://api.example.com/courses/123/students + data: + type: object + properties: + students: + type: array + items: + type: string + examples: + - http://api.example.com/users/1 + - http://api.example.com/users/2 + '400': + description: There was no uid in the request query. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '403': + description: The user trying to assign admins to the course was unauthorized. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '404': + description: Course not found. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '500': + $ref: '#/components/responses/InternalError' + delete: + description: Remove an admin from a course. + parameters: + - name: course_id + in: path + description: ID of the course + required: true + schema: + type: string + - name: admin_uid + in: body + description: Uid of the admin to be removed from the course + required: true + schema: + type: string + - name: uid + in: query + description: Uid of the user sending the request + schema: + type: string + responses: + '204': + description: User was successfully removed from the course admins. + content: + application/json: + schema: + type: object + properties: + message: + type: string + examples: + - User was successfully removed from the course admins. + url: + type: string + examples: + - API_URL + /courses/ + str(course_id) + /students + '400': + description: There was no uid in the request query. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '403': + description: The user trying to remove the admin from the course was unauthorized. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '404': + description: Course not found. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '500': + $ref: '#/components/responses/InternalError' + "/users": + get: + summary: Get all users + responses: + '200': + description: A list of users + content: + application/json: + schema: + type: array + items: + type: object + properties: + uid: + type: string + is_teacher: + type: boolean + is_admin: + type: boolean + required: + - uid + - is_teacher + - is_admin + post: + summary: Create a new user + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + uid: + type: string + is_teacher: + type: boolean + is_admin: + type: boolean + required: + - uid + - is_teacher + - is_admin + responses: + '201': + description: User created successfully + '400': + description: Invalid request data + '415': + description: Unsupported Media Type. Expected JSON. + '500': + $ref: '#/components/responses/InternalError' + "/users/{user_id}": + get: + summary: Get a user by ID + parameters: + - name: user_id + in: path + required: true + schema: + type: string + responses: + '200': + description: A user + content: + application/json: + $ref: '#/components/schemas/User' + '404': + description: User not found + patch: + summary: Update a user's information + parameters: + - name: user_id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + is_teacher: + type: boolean + is_admin: + type: boolean + required: + - is_teacher + - is_admin + responses: + '200': + description: User updated successfully + '404': + description: User not found + '415': + description: Unsupported Media Type. Expected JSON. + '500': + $ref: '#/components/responses/InternalError' + delete: + summary: Delete a user + parameters: + - name: user_id + in: path + required: true + schema: + type: string + responses: + '200': + description: User deleted successfully + '404': + description: User not found + '500': + $ref: '#/components/responses/InternalError' + "/submissions": + get: + summary: Gets the submissions + parameters: + - name: uid + in: query + description: User ID + schema: + type: string + - name: project_id + in: query + description: Project ID + schema: + type: integer + responses: + '200': + description: Successfully retrieved a list of submission URLs + content: + application/json: + schema: + type: object + properties: + url: + type: string + format: uri + message: + type: string + data: + type: object + properties: + submissions: array + items: + type: string + format: uri + '400': + description: An invalid user or project is given + content: + application/json: + schema: + type: object + properties: + url: + type: string + format: uri + message: + type: string + '500': + $ref: '#/components/responses/InternalError' + post: + summary: Posts a new submission to a project + requestBody: + description: Form data + content: + application/json: + schema: + type: object + properties: + uid: + type: string + required: true + project_id: + type: integer + required: true + files: + type: array + items: + type: file + responses: + '201': + description: Successfully posts the submission and retrieves its data + content: + application/json: + schema: + type: object + properties: + url: + type: string + format: uri + message: + type: string + data: + type: object + properties: + id: + type: integer + user: + type: string + format: uri + project: + type: string + format: uri + grading: + type: integer + time: + type: string + format: date-time + path: + type: string + status: + type: boolean + '400': + description: An invalid user, project or list of files is given + content: + application/json: + schema: + type: object + properties: + url: + type: string + format: uri + message: + type: string + '500': + $ref: '#/components/responses/InternalError' + "/submissions/{submission_id}": + get: + summary: Gets the submission + responses: + '200': + description: Successfully retrieved the submission + content: + application/json: + schema: + $ref: '#/components/schemas/Submission' + '404': + description: An invalid submission id is given + content: + application/json: + schema: + type: object + properties: + url: + type: string + format: uri + message: + type: string + '500': + $ref: '#/components/responses/InternalError' + patch: + summary: Patches the submission + requestBody: + description: The submission data + content: + application/json: + schema: + type: object + properties: + grading: + type: integer + minimum: 0 + maximum: 20 + responses: + '200': + description: Successfully patches the submission and retrieves its data + content: + application/json: + schema: + type: object + properties: + url: + type: string + format: uri + message: + type: string + data: + type: object + properties: + id: + type: integer + user: + type: string + format: uri + project: + type: string + format: uri + grading: + type: integer + time: + type: string + format: date-time + path: + type: string + status: + type: boolean + '400': + description: An invalid submission grading is given + content: + application/json: + schema: + type: object + properties: + url: + type: string + format: uri + message: + type: string + '404': + description: An invalid submission id is given + content: + application/json: + schema: + type: object + properties: + url: + type: string + format: uri + message: + type: string + '500': + $ref: '#/components/responses/InternalError' + delete: + summary: Deletes the submission + responses: + '200': + description: Successfully deletes the submission + content: + application/json: + schema: + type: object + properties: + url: + type: string + format: uri + message: + type: string + '404': + description: An invalid submission id is given + content: + application/json: + schema: + type: object + properties: + url: + type: string + format: uri + message: + type: string + '500': + $ref: '#/components/responses/InternalError' + parameters: + - name: submission_id + in: path + description: Submission ID + required: true + schema: + type: integer +components: + responses: + InternalError: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + schemas: + Error: + type: object + properties: + code: + type: string + message: + type: string + required: + - code + - message + Project: + type: object + properties: + project_id: + type: integer + title: + type: string + description: + type: string + assignment_file: + type: string + format: binary + deadline: + type: string + format: date-time + course_id: + type: integer + visible_for_students: + type: boolean + archived: + type: boolean + test_path: + type: string + script_name: + type: string + regex_expressions: + type: array + items: + type: string + Course: + type: object + properties: + message: + type: string + data: + type: object + properties: + ufora_id: + type: string + teacher: + type: string + admins: + type: array + items: + type: string + students: + type: array + items: + type: string + projects: + type: array + items: + type: string + url: + type: string + User: + type: object + properties: + uid: + type: string + is_teacher: + type: boolean + is_admin: + type: boolean + required: + - uid + - is_teacher + - is_admin + Submission: + type: object + properties: + url: + type: string + format: uri + message: + type: string + data: + type: object + properties: + submission: + type: object + properties: + submission_id: + type: integer + user: + type: string + format: uri + project: + type: string + format: uri + grading: + type: integer + nullable: true + time: + type: string + format: date-time + path: + type: string + status: + type: boolean \ No newline at end of file diff --git a/backend/tests.yaml b/backend/tests.yaml index dad5289a..7270889b 100644 --- a/backend/tests.yaml +++ b/backend/tests.yaml @@ -43,7 +43,7 @@ services: API_HOST: http://api_is_here AUTHENTICATION_URL: http://auth-server:5001 # Use the service name defined in Docker Compose UPLOAD_URL: /data/assignments - DOCS_JSON_PATH: static/OpenAPI_Object.json + DOCS_JSON_PATH: static/OpenAPI_Object.yaml DOCS_URL: /docs volumes: - .:/app diff --git a/backend/tests/endpoints/index_test.py b/backend/tests/endpoints/index_test.py index 8f3a5d4e..9f1cceee 100644 --- a/backend/tests/endpoints/index_test.py +++ b/backend/tests/endpoints/index_test.py @@ -1,5 +1,7 @@ """Test the base routes of the application""" +import yaml + def test_home(client): """Test whether the index page is accesible""" @@ -10,6 +12,8 @@ def test_home(client): def test_openapi_spec(client): """Test whether the required fields of the openapi spec are present""" response = client.get("/") - response_json = response.json - assert response_json["openapi"] is not None - assert response_json["info"] is not None + response_text = response.text + response_yaml = yaml.safe_load(response_text) + + assert "openapi" in response_yaml + assert "info" in response_yaml From 0aea3a349081cc4ac5eebbd6c1e3ccd60062897b Mon Sep 17 00:00:00 2001 From: Jarne Clauw <67628242+JarneClauw@users.noreply.github.com> Date: Mon, 1 Apr 2024 13:12:38 +0200 Subject: [PATCH 2/2] Base test class to handle auth (#153) * Adding auth tokens * Setup for auth tests * Fixing tests * Cleanup * Spelling * Remove unnecessary commit --- backend/test_auth_server/__main__.py | 36 ++++++++++++++++++ backend/tests/conftest.py | 23 ++++++++++-- backend/tests/endpoints/conftest.py | 18 +++++++++ backend/tests/endpoints/endpoint.py | 56 ++++++++++++++++++++++++++++ backend/tests/models/user_test.py | 6 +-- 5 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 backend/tests/endpoints/endpoint.py diff --git a/backend/test_auth_server/__main__.py b/backend/test_auth_server/__main__.py index bf5fd576..1dc6302f 100644 --- a/backend/test_auth_server/__main__.py +++ b/backend/test_auth_server/__main__.py @@ -8,6 +8,7 @@ index_bp = Blueprint("index", __name__) index_endpoint = Api(index_bp) +# Take the key the same as the id, uid can then be used in backend token_dict = { "teacher1":{ "id":"Gunnar", @@ -44,6 +45,41 @@ "admin1":{ "id":"admin_person", "jobTitle":"admin" + }, + # Lowest authorized user to test login requirement + "login": { + "id": "login", + "jobTitle": None + }, + # Student authorization access, associated with valid_... + "student": { + "id": "student", + "jobTitle": None + }, + # Student authorization access, other + "student_other": { + "id": "student_other", + "jobTitle": None + }, + # Teacher authorization access, associated with valid_... + "teacher": { + "id": "teacher", + "jobTitle": "teacher" + }, + # Teacher authorization access, other + "teacher_other": { + "id": "teacher_other", + "jobTitle": "teacher" + }, + # Admin authorization access, associated with valid_... + "admin": { + "id": "admin", + "jobTitle": "admin" + }, + # Admin authorization access, other + "admin_other": { + "id": "admin_other", + "jobTitle": "admin" } } diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index fe9d3961..a7cc092b 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -2,7 +2,7 @@ from datetime import datetime from zoneinfo import ZoneInfo -import pytest +from pytest import fixture from project.sessionmaker import engine, Session from project.db_in import db from project.models.course import Course @@ -11,7 +11,7 @@ from project.models.course_relation import CourseStudent,CourseAdmin from project.models.submission import Submission, SubmissionStatus -@pytest.fixture +@fixture def db_session(): """Create a new database session for a test. After the test, all changes are rolled back and the session is closed.""" @@ -123,7 +123,22 @@ def submissions(session): ) ] -@pytest.fixture +### AUTHENTICATION & AUTHORIZATION ### +def auth_tokens(): + """Add the authenticated users to the database""" + + return [ + User(uid="login", role=Role.STUDENT), + User(uid="student", role=Role.STUDENT), + User(uid="student_other", role=Role.STUDENT), + User(uid="teacher", role=Role.TEACHER), + User(uid="teacher_other", role=Role.TEACHER), + User(uid="admin", role=Role.ADMIN), + User(uid="admin_other", role=Role.ADMIN) + ] + +### SESSION ### +@fixture def session(): """Create a new database session for a test. After the test, all changes are rolled back and the session is closed.""" @@ -132,6 +147,8 @@ def session(): session = Session() try: + session.add_all(auth_tokens()) + # Populate the database session.add_all(users()) session.commit() diff --git a/backend/tests/endpoints/conftest.py b/backend/tests/endpoints/conftest.py index b76e8369..fd46dd8a 100644 --- a/backend/tests/endpoints/conftest.py +++ b/backend/tests/endpoints/conftest.py @@ -5,6 +5,8 @@ from datetime import datetime from zoneinfo import ZoneInfo import pytest +from pytest import fixture, FixtureRequest +from flask.testing import FlaskClient from sqlalchemy import create_engine from sqlalchemy.exc import SQLAlchemyError from project.models.user import User,Role @@ -15,7 +17,23 @@ from project.models.submission import Submission, SubmissionStatus from project.models.project import Project +### AUTHENTICATION & AUTHORIZATION ### +@fixture +def auth_test(request: FixtureRequest, client: FlaskClient, valid_course_entry): + """Add concrete test data""" + # endpoint, parameters, method, token, status + endpoint, parameters, method, *other = request.param + d = { + "course_id": valid_course_entry.course_id + } + + for index, parameter in enumerate(parameters): + endpoint = endpoint.replace(f"@{index}", str(d[parameter])) + + return endpoint, getattr(client, method), *other + +### OTHER ### @pytest.fixture def valid_submission(valid_user_entry, valid_project_entry): """ diff --git a/backend/tests/endpoints/endpoint.py b/backend/tests/endpoints/endpoint.py new file mode 100644 index 00000000..1c468164 --- /dev/null +++ b/backend/tests/endpoints/endpoint.py @@ -0,0 +1,56 @@ +"""Base class for endpoint tests""" + +from typing import List, Tuple +from pytest import param + +def authentication_tests(tests: List[Tuple[str, List[str], List[str]]]) -> List[any]: + """Transform the format to single authentication tests""" + + single_tests = [] + for test in tests: + endpoint, parameters, methods = test + for method in methods: + single_tests.append(param( + (endpoint, parameters, method), + id = f"{endpoint} {method}" + )) + return single_tests + +def authorization_tests(tests: List[Tuple[str, List[str], str, List[str], List[str]]]) -> List[any]: + """Transform the format to single authorization tests""" + + single_tests = [] + for test in tests: + endpoint, parameters, method, allowed_tokens, disallowed_tokens = test + for token in (allowed_tokens + disallowed_tokens): + allowed = token in allowed_tokens + single_tests.append(param( + (endpoint, parameters, method, token, allowed), + id = f"{endpoint} {method} {token} {'allowed' if allowed else 'disallowed'}" + )) + return single_tests + +class TestEndpoint: + """Base class for endpoint tests""" + + def authentication(self, authentication_parameter: Tuple[str, any]): + """Test if the authentication for the given enpoint works""" + + endpoint, method = authentication_parameter + + response = method(endpoint) + assert response.status_code == 401 + + response = method(endpoint, headers = {"Authorization": "0123456789"}) + assert response.status_code == 401 + + response = method(endpoint, headers = {"Authorization": "login"}) + assert response.status_code != 401 + + def authorization(self, auth_parameter: Tuple[str, any, str, bool]): + """Test if the authorization for the given endpoint works""" + + endpoint, method, token, allowed = auth_parameter + + response = method(endpoint, headers = {"Authorization": token}) + assert allowed == (response.status_code != 403) diff --git a/backend/tests/models/user_test.py b/backend/tests/models/user_test.py index 05520b8c..d607ebad 100644 --- a/backend/tests/models/user_test.py +++ b/backend/tests/models/user_test.py @@ -14,11 +14,11 @@ def test_create_user(self, session: Session): session.add(user) session.commit() assert session.get(User, "user01") is not None - assert session.query(User).count() == 5 + assert session.query(User).count() == 12 def test_query_user(self, session: Session): """Test if a user can be queried""" - assert session.query(User).count() == 4 + assert session.query(User).count() == 11 teacher = session.query(User).filter_by(uid="brinkmann").first() assert teacher is not None assert teacher.role == Role.ADMIN @@ -36,7 +36,7 @@ def test_delete_user(self, session: Session): session.delete(user) session.commit() assert session.get(User, user.uid) is None - assert session.query(User).count() == 3 + assert session.query(User).count() == 10 @mark.parametrize("property_name", ["uid"]) def test_property_not_nullable(self, session: Session, property_name: str):