diff --git a/README.md b/README.md index 78cccd5..6a5d4d9 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,9 @@ Run the server: Your `.env` file should now look something like [example.env](https://github.com/ProgrammingBuddies/programmingbuddies-api/blob/develop/example.env) +- Optionally you can set the JWT token timeout + - `JWT_ACCESS_TOKEN_EXPIRES` - time in seconds + ### Testing - to run multiple tests just specify the directory which contains them for example `pipenv run pytest tests/` diff --git a/src/api/controllers/projectController.py b/src/api/controllers/projectController.py index c8d69f9..b957bfc 100644 --- a/src/api/controllers/projectController.py +++ b/src/api/controllers/projectController.py @@ -1,60 +1,80 @@ -from api.models import db, Project, UserHasProject, ProjectLink, ProjectFeedback +from api.models import db, User, Project, UserHasProject, ProjectLink, ProjectFeedback class ProjectController: - session = db.session() + # TODO: remove this (also from userController)? + session = db.session # Project - def create_project(self, **kwargs): + def create_project(self, user_id, **kwargs): try: + user = User.query.filter_by(id=user_id).first() + + if user == None: + return None, "User not found", 404 + project = Project(**kwargs) - self.session.add(project) + + userHasProject = UserHasProject(role=1) + userHasProject.project = project + userHasProject.user = user + + self.session.add(userHasProject) self.session.commit() - return project + return project, "OK", 201 except: - return None + self.session.rollback() + return None, "Project creation failed", 400 - def update_project(self, id, **kwargs): - project = Project.query.filter_by(id=id).first() + def update_project(self, user_id, **kwargs): + if 'user_id' in kwargs: + return None, "Failed to update project. Request body can not specify user's id.", 400 - if project == None: - return None + if 'id' not in kwargs: + return None, "Failed to update project. Request body must specify project's id.", 400 - for key, value in kwargs.items(): - if not hasattr(project, key): - return None + user = User.query.filter_by(id=user_id).first() - for key, value in kwargs.items(): - setattr(project, key, value) - - db.session.commit() - - return project + if user == None: + return None, "User not found", 404 - def update_project(self, id, **kwargs): - project = Project.query.filter_by(id=id).first() + project = Project.query.filter_by(id=kwargs["id"]).first() if project == None: - return project + return None, "Project not found", 404 + + # Note that we are updating the id too, but to the same id because we used it to query the user with it + for key, value in kwargs.items(): + if not hasattr(project, key): + return None, f"Forbidden attribute '{key}'", 400 for key, value in kwargs.items(): setattr(project, key, value) db.session.commit() - return project + return project, "OK", 200 def get_project(self, **kwargs): project = Project.query.filter_by(**kwargs).first() - return project + if project: + return project, "OK", 200 + else: + return None, "Project not found", 404 def get_all_projects(self, **kwargs): all_projects = Project.query.all() - return all_projects + return all_projects, "OK", 200 + + def delete_project(self, user_id, id): + + userHasProject = UserHasProject.query.filter_by(user_id=user_id, project_id=id).first() + # TODO: verify that the user owns the project (or has neccessary rights) + if not userHasProject: + return None, "Failed to delete project. Current user does not belong to specified project, or the project does not exist.", 404 - def delete_project(self, id): # Remove all project's links for link in ProjectLink.query.filter_by(project_id=id).all(): db.session.delete(link) @@ -66,12 +86,13 @@ def delete_project(self, id): project = Project.query.filter_by(id=id).first() if project == None: - return project + # TODO: db.session.rollback? + return None, "Project not found", 404 db.session.delete(project) db.session.commit() - return project + return project, "OK", 200 # Project Link def create_link(self, project_id, **kwargs): diff --git a/src/api/controllers/userController.py b/src/api/controllers/userController.py index da4f01e..aa165c2 100644 --- a/src/api/controllers/userController.py +++ b/src/api/controllers/userController.py @@ -4,7 +4,7 @@ from flask import jsonify class UserController: - session = db.session() + session = db.session # User def create_user(self, **kwargs): @@ -16,17 +16,17 @@ def create_user(self, **kwargs): return user, "OK", 200 except: self.session.rollback() - return None, "Forbidden Attributes", 400 + return None, "Forbidden attributes", 400 def update_user(self, id, **kwargs): user = User.query.filter_by(id=id).first() if user == None: - return None, "user not found", 404 + return None, "User not found", 404 for key, value in kwargs.items(): if not hasattr(user, key): - return None, "forbidden attribute", 400 + return None, f"Forbidden attribute '{key}'", 400 for key, value in kwargs.items(): setattr(user, key, value) @@ -39,7 +39,7 @@ def get_user(self, **kwargs): user = User.query.filter_by(**kwargs).first() if user is None: - return None, "User Not Found", 404 + return None, "User not found", 404 return user, "OK", 200 @@ -60,7 +60,7 @@ def delete_user(self, id): user = User.query.filter_by(id=id).first() if user == None: - return None, "user not found", 404 + return None, "User not found", 404 db.session.delete(user) db.session.commit() @@ -81,7 +81,7 @@ def create_link(self, user_id, **kwargs): return None, "Forbidden attributes used in request. only name and url allowed.", 400 except: self.session.rollback() - return None, "link creation failed", 500 + return None, "Link creation failed", 500 def update_link(self, user_id, **kwargs): if not 'id' in kwargs: @@ -108,7 +108,7 @@ def get_all_links(self, user_id): user = User.query.filter_by(id=user_id).first() if user is None: return None, "User not found", 404 - + return user.links, "OK", 200 def delete_link(self, user_id, **kwargs): diff --git a/src/api/views/projectView.py b/src/api/views/projectView.py index 587a6f4..ccb4bae 100644 --- a/src/api/views/projectView.py +++ b/src/api/views/projectView.py @@ -1,12 +1,17 @@ from flask import request, jsonify +from flask_jwt_extended import get_jwt_identity, jwt_required from api import app +from api.utils import wrap_response, body_required from api.controllers import projectController # Project -@app.route("/projects", methods=['POST']) +@app.route("/project", methods=['POST']) +@jwt_required +@body_required def create_project(): """ Create project + Current user creates project --- tags: - Project @@ -32,16 +37,16 @@ def create_project(): description: Description of the project languages: type: string - description: List of programming languages the project uses + description: (Optional) List of programming languages the project uses development_status: type: integer description: Development status of the project creation_date: type: string - description: Creation date of the project + description: (Optional) Creation date of the project release_date: type: string - description: Release date of the project + description: (Optional) Release date of the project repository: type: string description: Url of the project's repository @@ -65,28 +70,22 @@ def create_project(): description: Project created successfully 400: description: Failed to create project + 404: + description: User doesn't exist """ - project = projectController.create_project(**request.get_json()) - - if project == None: - return "Failed to create project.", 400 - else: - return jsonify(project.as_dict()), 201 + return wrap_response(*projectController.create_project(user_id=get_jwt_identity(), **request.get_json())) -@app.route("/projects/", methods=['PUT']) -def update_project(id): +@app.route("/project", methods=['PUT']) +@jwt_required +@body_required +def update_project(): """ Update project - Updates project with `id` using the data in request body + Updates current user's project with the data in request body --- tags: - Project parameters: - - in: path - name: id - type: integer - required: true - description: Id of project to update - in: body name: Project required: true @@ -98,18 +97,15 @@ def update_project(id): description: Project updated successfully 400: description: Failed to update project + 404: + description: Current user or requested project not found """ - if 'id' in request.get_json(): - return "Failed to update project. Request body can not specify project's id.", 501 + if "user_id" in request.get_json(): + return wrap_response(None, "Failed to update project. Request body must not contain 'user_id'.", 400) - project = projectController.update_project(id, **request.get_json()) - - if project == None: - return "Failed to update project.", 400 - else: - return jsonify(project.as_dict()), 200 + return wrap_response(*projectController.update_project(user_id=get_jwt_identity(), **request.get_json())) -@app.route("/projects/", methods=['GET']) +@app.route("/project/", methods=['GET']) def get_project(id): """ Get project @@ -129,14 +125,9 @@ def get_project(id): 404: description: Project not found """ - project = projectController.get_project(id=id) + return wrap_response(*projectController.get_project(id=id)) - if project: - return jsonify(project.as_dict()), 200 - else: - return "", 404 - -@app.route("/projects", methods=['GET']) +@app.route("/project/all", methods=['GET']) def get_all_projects(): """ Get all projects @@ -148,17 +139,14 @@ def get_all_projects(): 200: description: List of projects """ - all_projects = projectController.get_all_projects() - - projects = [ project.as_dict() for project in all_projects ] - - return jsonify(projects), 200 + return wrap_response(*projectController.get_all_projects()) -@app.route("/projects/", methods=['DELETE']) +@app.route("/project/", methods=['DELETE']) +@jwt_required def delete_project(id): """ Delete project - Deletes project with `id` + Deletes current user's project with the `id` --- tags: - Project @@ -172,14 +160,11 @@ def delete_project(id): 200: description: Project deleted successfully 400: - description: Project not found + description: Failed to delete project + 404: + description: Current user is not a member of requested project or the project was not found """ - project = projectController.delete_project(id) - - if project: - return "", 202 - else: - return "", 404 + return wrap_response(*projectController.delete_project(user_id=get_jwt_identity(), id=id)) # Project Link @app.route("/projects//links", methods=['POST']) diff --git a/src/api/views/userView.py b/src/api/views/userView.py index c21143e..c9b77e8 100644 --- a/src/api/views/userView.py +++ b/src/api/views/userView.py @@ -88,7 +88,7 @@ def delete_user(): 404: description: User the token belonged to doesn't exist anymore """ - + return wrap_response(*userController.delete_user(get_jwt_identity())) # User Link @@ -152,7 +152,7 @@ def update_user_link(): description: (optional) Name of the user link url: type: string - description: (optional)Url of the user link + description: (optional) Url of the user link responses: 200: description: User link updated successfully diff --git a/tests/__init__.py b/tests/__init__.py index 56f3d94..25d093a 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -6,6 +6,6 @@ from api import app from api.models import db -from api.models import User, Project, UserFeedback, ProjectFeedback, UserLink, ProjectLink +from api.models import User, Project, UserHasProject, UserFeedback, ProjectFeedback, UserLink, ProjectLink sys.path.insert(0, os.getcwd()+'/tests') diff --git a/tests/api/__init__.py b/tests/api/__init__.py index bd7fb7c..814b07e 100644 --- a/tests/api/__init__.py +++ b/tests/api/__init__.py @@ -11,7 +11,17 @@ """ from flask_jwt_extended import create_access_token -from tests import db, Project, User, UserLink, ProjectLink, UserFeedback +from tests import db, Project, User, UserHasProject, UserLink, ProjectLink, UserFeedback + +""" Create relationship between user and project """ +def user_join_project_for_test_cases(user, project): + userHasProject = UserHasProject(role=1) + userHasProject.project = Project.query.filter_by(id=project["id"]).first() + userHasProject.user = user + # This line is not needed for some reason + # SQLAlchemy is doing some magic and I don't know shit about fuck + #user.projects.append(userHasProject) + db.session.commit() def create_project_for_test_cases(data): new_project = Project(**data) diff --git a/tests/api/views/test_projectView.py b/tests/api/views/test_projectView.py index eb328d4..366613f 100644 --- a/tests/api/views/test_projectView.py +++ b/tests/api/views/test_projectView.py @@ -1,72 +1,93 @@ from tests.conftest import client from tests import db, Project, ProjectLink -from tests.api import create_project_for_test_cases, create_project_link_for_test_cases +from tests.api import create_project_for_test_cases, create_project_link_for_test_cases, create_access_token_for_test_cases, user_join_project_for_test_cases class TestProjectView(object): - valid_data = { + # valid data for user creation + valid_user_data = { + 'name': 'L Jone', + 'bio': 'coding...', + 'languages': 'FR', + 'interests': 'Nothing', + 'location': 'X', + 'occupation': 'cashier' + } + + valid_project_data = { 'name': 'PB api', 'description': 'A cool project', + 'development_status': 1, 'repository': 'http://github.xy.com' } def test_create_project(self, client): - response = client.post('/projects', json={"name": "Project"}) + token, _ = create_access_token_for_test_cases(self.valid_user_data) + + response = client.post('/project', headers={"Authorization": f"Bearer {token}"}, json={"name": "Project"}) assert response.status_code == 400 - response = client.post('/projects', json=self.valid_data) - assert response.status_code == 201 + response = client.post('/project', headers={"Authorization": f"Bearer {token}"}) + assert response.status_code == 400 - # response = client.post('/projects') - # assert response.status_code == 400 + response = client.post('/project') + assert response.status_code == 401 + + response = client.post('/project', headers={"Authorization": f"Bearer {token}"}, json=self.valid_project_data) + assert response.status_code == 201 def test_update_project(self, client): + token, _ = create_access_token_for_test_cases(self.valid_user_data) # project id doesn't exist - response = client.post('/projects/0', json={'name': 'Updated PB'}) - - # notice: should return 404 when doesen't exist insted of 400 + response = client.put('/project', headers={"Authorization": f"Bearer {token}"}, json={'id': 0, 'name': 'Updated PB'}) assert response.status_code == 404 - project_id = create_project_for_test_cases(self.valid_data) - response = client.post('/projects/{}'.format(project_id), json={'description': 'updated desc'}) + project = create_project_for_test_cases(self.valid_project_data) + project_id = project["id"] + + response = client.put('/project', headers={"Authorization": f"Bearer {token}"}, json={'id': project_id, 'description': 'updated desc'}) project = Project.query.filter_by(id=project_id).first() assert project.description == 'updated desc' def test_delete_project(self, client): - response = client.delete('/projects/0') + token, user = create_access_token_for_test_cases(self.valid_user_data) + + response = client.delete('/project/{}'.format(0), headers={"Authorization": f"Bearer {token}"}) assert response.status_code == 404 - project1 = create_project_for_test_cases(self.valid_data) + project1 = create_project_for_test_cases(self.valid_project_data) + user_join_project_for_test_cases(user, project1) - self.valid_data["name"] = "p2 name" - project2 = create_project_for_test_cases(self.valid_data) + self.valid_project_data["name"] = "p2 name" + project2 = create_project_for_test_cases(self.valid_project_data) + user_join_project_for_test_cases(user, project2) - response = client.delete('/projects/{}'.format(project1["id"])) - assert response.status_code == 202 + response = client.delete('/project/{}'.format(project1["id"]), headers={"Authorization": f"Bearer {token}"}) + assert response.status_code == 200 def test_get_project(self, client): - response = client.get('/projects/{}'.format(0)) + response = client.get('/project/{}'.format(0)) assert response.status_code == 404 - project = create_project_for_test_cases(self.valid_data) - response = client.get('/projects/{}'.format(project["id"])) + project = create_project_for_test_cases(self.valid_project_data) + response = client.get('/project/{}'.format(project["id"])) assert response.status_code == 200 - assert response.get_json()["name"] == project["name"] + assert response.get_json()["data"]["name"] == project["name"] def test_get_all_projects(self, client): - project1 = create_project_for_test_cases(self.valid_data) + project1 = create_project_for_test_cases(self.valid_project_data) - self.valid_data["name"] = "allproject2" - project2 = create_project_for_test_cases(self.valid_data) - response = client.get('/projects') + self.valid_project_data["name"] = "allproject2" + project2 = create_project_for_test_cases(self.valid_project_data) + response = client.get('/project/all') assert response.status_code == 200 r = response.get_json() - assert [r[0]["name"], r[1]["name"]] == [project1["name"], project2["name"]] + assert [r["data"][0]["name"], r["data"][1]["name"]] == [project1["name"], project2["name"]] def test_create_project_link(self, client): - project = create_project_for_test_cases(self.valid_data) + project = create_project_for_test_cases(self.valid_project_data) url = '/projects/{}/links'.format(project["id"]) response = client.post(url, json={"user_id": 0}) @@ -76,7 +97,7 @@ def test_create_project_link(self, client): assert response.status_code == 201 def test_update_project_link(self, client): - p1 = create_project_for_test_cases(self.valid_data) + p1 = create_project_for_test_cases(self.valid_project_data) p1_link = create_project_link_for_test_cases( { "name": "Plink", @@ -99,7 +120,7 @@ def test_update_project_link(self, client): assert response.get_json()["name"] == "Nlink" def test_get_all_project_links(self, client): - p1 = create_project_for_test_cases(self.valid_data) + p1 = create_project_for_test_cases(self.valid_project_data) p_link1 = create_project_link_for_test_cases( { "name": "Plink", @@ -123,7 +144,7 @@ def test_delete_project_link(self, client): response = client.delete(url.format(0, 0)) assert response.status_code == 404 - p1 = create_project_for_test_cases(self.valid_data) + p1 = create_project_for_test_cases(self.valid_project_data) p_link1 = create_project_link_for_test_cases( { "name": "Plink", diff --git a/tests/runtests.py b/tests/runtests.py index 82a6c8b..70d7f78 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -1,6 +1,6 @@ response = """ -Use: pipenv run pytest src/tests/ +Usage: pipenv run pytest tests/ """