Repository: MLH-Fellowship/orientation-project-python-25.SUM.a
Pull request #31: Add input validation for resume API endpoints
Author: login: AryanK1511
Created: 2025-05-23T01:25:12Z | Merged: 2025-05-23T13:27:16Z

This PR resolves #13 

Linked issue #13: Add validation that POST requests are in the correct format
When making a POST request to add an Experience, Education or Skill, the API server should check it's in the correct format, including all the required fields. If it is not, it should return an error message and the appropriate HTTP status code.

This should also include writing a test inside of test_pytest.py as part of the feature to show that it works. If you're unfamiliar with Pytest, you should take a read inside the LMS for more instructions!
Linked issue comments: user: login: AryanK1511
created_at: 2025-05-19T22:37:31Z
body: I can pick this one up
author_association: CONTRIBUTOR

Diff:
diff --git a/app.py b/app.py
index 0ac7234..285da4c 100644
--- a/app.py
+++ b/app.py
@@ -3,6 +3,7 @@
 '''
 from flask import Flask, jsonify, request
 from models import Experience, Education, Skill
+from utils import validate_data
 
 app = Flask(__name__)
 
@@ -63,22 +64,24 @@ def experience():
         return jsonify(data['experience']), 200
 
     if request.method == 'POST':
-        experience_data = request.get_json()
-        # Validate the json data
-        if not all(key in experience_data for key in ['title', 'company', 'start_date',
-                                                      'end_date', 'description', 'logo']):
-            return jsonify({"error": "Missing required fields"}), 400
-
-        new_experience = Experience(
-            experience_data['title'],
-            experience_data['company'],
-            experience_data['start_date'],
-            experience_data['end_date'],
-            experience_data['description'],
-            experience_data['logo']
-        )
-        data['experience'].append(new_experience)
-        return jsonify({"id": len(data['experience']) - 1}), 201
+        try:
+            experience_data = request.get_json()
+            is_valid, error_message = validate_data('experience', experience_data)
+            if not is_valid:
+                return jsonify({"error": error_message}), 400
+
+            new_experience = Experience(
+                experience_data['title'],
+                experience_data['company'],
+                experience_data['start_date'],
+                experience_data['end_date'],
+                experience_data['description'],
+                experience_data['logo']
+            )
+            data['experience'].append(new_experience)
+            return jsonify({"id": len(data['experience']) - 1}), 201
+        except (TypeError, ValueError, KeyError):
+            return jsonify({"error": "Invalid data format"}), 400
 
     return jsonify({"error": "Method not allowed"}), 405
 
@@ -114,7 +117,18 @@ def education():
         return jsonify(data['education']), 200
 
     if request.method == 'POST':
-        return jsonify({}), 201
+        try:
+            education_data = request.get_json()
+            is_valid, error_message = validate_data('education', education_data)
+            if not is_valid:
+                return jsonify({"error": error_message}), 400
+            # pylint: disable=fixme
+            # TODO: Create new Education object with education_data
+            # TODO: Append new education to data['education']
+            # TODO: Return jsonify({"id": len(data['education']) - 1}), 201
+            return jsonify({}), 201
+        except (TypeError, ValueError, KeyError):
+            return jsonify({"error": "Invalid data format"}), 400
 
     return jsonify({})
 
@@ -155,9 +169,20 @@ def skill():
         Returns 405 if method is not allowed.
     """
     if request.method == 'GET':
-        return jsonify({})
+        return jsonify(data['skill']), 200
 
     if request.method == 'POST':
-        return jsonify({})
+        try:
+            skill_data = request.get_json()
+            is_valid, error_message = validate_data('skill', skill_data)
+            if not is_valid:
+                return jsonify({"error": error_message}), 400
+            # pylint: disable=fixme
+            # TODO: Create new Skill object with skill_data
+            # TODO: Append new skill to data['skill']
+            # TODO: Return jsonify({"id": len(data['skill']) - 1}), 201
+            return jsonify({}), 201
+        except (TypeError, ValueError, KeyError):
+            return jsonify({"error": "Invalid data format"}), 400
 
     return jsonify({})
diff --git a/test_pytest.py b/test_pytest.py
index 39a8c3f..f386f1f 100644
--- a/test_pytest.py
+++ b/test_pytest.py
@@ -224,3 +224,52 @@ def test_skill():
 
     response = app.test_client().get('/resume/skill')
     assert response.json[item_id] == example_skill
+
+
+def test_invalid_input_validation():
+    '''
+    Test that the API properly validates input data for POST requests
+    '''
+    client = app.test_client()
+    # Test invalid experience
+    invalid_experience = {
+        "title": "Software Developer",
+        # Missing required fields
+    }
+    response = client.post('/resume/experience', json=invalid_experience)
+    assert response.status_code == 400
+    assert "error" in response.json
+    assert "Missing required fields" in response.json["error"]
+    assert "company" in response.json["error"]
+    assert "start_date" in response.json["error"]
+    assert "end_date" in response.json["error"]
+    assert "description" in response.json["error"]
+    assert "logo" in response.json["error"]
+    # Test invalid education
+    invalid_education = {
+        "course": "Computer Science",
+        # Missing required fields
+    }
+    response = client.post('/resume/education', json=invalid_education)
+    assert response.status_code == 400
+    assert "error" in response.json
+    assert "Missing required fields" in response.json["error"]
+    assert "school" in response.json["error"]
+    assert "start_date" in response.json["error"]
+    assert "end_date" in response.json["error"]
+    assert "grade" in response.json["error"]
+    assert "logo" in response.json["error"]
+    # Test invalid skill
+    invalid_skill = {
+        "name": "Python",
+        # Missing required fields
+    }
+    response = client.post('/resume/skill', json=invalid_skill)
+    assert response.status_code == 400
+    assert "error" in response.json
+    assert "Missing required fields" in response.json["error"]
+    assert "proficiency" in response.json["error"]
+    assert "logo" in response.json["error"]
+    # Test invalid data format
+    response = client.post('/resume/experience', data="not json")
+    assert response.status_code == 415
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000..c9ad5e9
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,35 @@
+'''
+Utility functions
+'''
+
+# Define required fields for each type
+REQUIRED_FIELDS = {
+    'experience': ['title', 'company', 'start_date', 'end_date', 'description', 'logo'],
+    'education': ['course', 'school', 'start_date', 'end_date', 'grade', 'logo'],
+    'skill': ['name', 'proficiency', 'logo']
+}
+
+def validate_data(data_type, data):
+    '''
+    Validates that all required fields are present in the data
+    
+    Parameters
+    ----------
+    data_type : str
+        The type of data being validated ('experience', 'education', or 'skill')
+    data : dict
+        The data to validate
+        
+    Returns
+    -------
+    tuple
+        (bool, str) - (is_valid, error_message)
+    '''
+    if data is None:
+        return False, "Invalid data format"
+    if not isinstance(data, dict):
+        return False, "Invalid data format"
+    missing_fields = [field for field in REQUIRED_FIELDS[data_type] if field not in data]
+    if missing_fields:
+        return False, f"Missing required fields: {', '.join(missing_fields)}"
+    return True, None


Reviews:
user: login: Pradyuman7
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-23T09:44:46Z
user: login: AryanK1511
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T12:05:51Z
user: login: AryanK1511
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T12:06:21Z
user: login: AryanK1511
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T12:08:05Z
user: login: Pradyuman7
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-23T12:55:03Z
user: login: Pradyuman7
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-23T12:55:13Z
user: login: Pradyuman7
body: LGTM 💯 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-23T13:13:20Z

Review comments:
diff_hunk: @@ -51,22 +52,24 @@ def experience():
         return jsonify(data['experience']), 200
 
     if request.method == 'POST':
-        experience_data = request.get_json()
-        # Validate the json data
-        if not all(key in experience_data for key in ['title', 'company', 'start_date',
-                                                      'end_date', 'description', 'logo']):
-            return jsonify({"error": "Missing required fields"}), 400
-
-        new_experience = Experience(
-            experience_data['title'],
-            experience_data['company'],
-            experience_data['start_date'],
-            experience_data['end_date'],
-            experience_data['description'],
-            experience_data['logo']
-        )
-        data['experience'].append(new_experience)
-        return jsonify({"id": len(data['experience']) - 1}), 201
+        try:
+            experience_data = request.get_json()
+            is_valid, error_message = validate_data('experience', experience_data)
+            if not is_valid:
+                return jsonify({"error": error_message}), 400
+
+            new_experience = Experience(
+                experience_data['title'],
+                experience_data['company'],
+                experience_data['start_date'],
+                experience_data['end_date'],
+                experience_data['description'],
+                experience_data['logo']
+            )
+            data['experience'].append(new_experience)
+            return jsonify({"id": len(data['experience']) - 1}), 201
+        except (TypeError, ValueError, KeyError):
+            return jsonify({"error": "Invalid data format"}), 400
path: app.py
user: login: Pradyuman7
body: Let's return bad request here as well? What do you think?
created_at: 2025-05-23T09:43:27Z
author_association: COLLABORATOR
diff_hunk: @@ -89,10 +92,17 @@ def education():
     Handles education requests
     '''
     if request.method == 'GET':
-        return jsonify({})
+        return jsonify(data['education']), 200
 
     if request.method == 'POST':
-        return jsonify({})
+        try:
+            education_data = request.get_json()
+            is_valid, error_message = validate_data('education', education_data)
+            if not is_valid:
+                return jsonify({"error": error_message}), 400
+            return jsonify({}), 201
path: app.py
user: login: Pradyuman7
body: Let's return the id here
created_at: 2025-05-23T09:44:26Z
author_association: COLLABORATOR
diff_hunk: @@ -125,9 +135,16 @@ def skill():
     Handles Skill requests
     '''
     if request.method == 'GET':
-        return jsonify({})
+        return jsonify(data['skill']), 200
 
     if request.method == 'POST':
-        return jsonify({})
+        try:
+            skill_data = request.get_json()
+            is_valid, error_message = validate_data('skill', skill_data)
+            if not is_valid:
+                return jsonify({"error": error_message}), 400
+            return jsonify({}), 201
path: app.py
user: login: Pradyuman7
body: Let's return the id here
created_at: 2025-05-23T09:44:41Z
author_association: COLLABORATOR
diff_hunk: @@ -89,10 +92,17 @@ def education():
     Handles education requests
     '''
     if request.method == 'GET':
-        return jsonify({})
+        return jsonify(data['education']), 200
 
     if request.method == 'POST':
-        return jsonify({})
+        try:
+            education_data = request.get_json()
+            is_valid, error_message = validate_data('education', education_data)
+            if not is_valid:
+                return jsonify({"error": error_message}), 400
+            return jsonify({}), 201
path: app.py
user: login: AryanK1511
body: I can't return an ID here since that route has not been implemented yet and someone else has picked up the issue to do so. This here is just the input validation.
created_at: 2025-05-23T12:05:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,9 +135,16 @@ def skill():
     Handles Skill requests
     '''
     if request.method == 'GET':
-        return jsonify({})
+        return jsonify(data['skill']), 200
 
     if request.method == 'POST':
-        return jsonify({})
+        try:
+            skill_data = request.get_json()
+            is_valid, error_message = validate_data('skill', skill_data)
+            if not is_valid:
+                return jsonify({"error": error_message}), 400
+            return jsonify({}), 201
path: app.py
user: login: AryanK1511
body: I can't return an ID here since that route has not been implemented yet and someone else has picked up the issue to do so. This here is just the input validation.
created_at: 2025-05-23T12:06:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -51,22 +52,24 @@ def experience():
         return jsonify(data['experience']), 200
 
     if request.method == 'POST':
-        experience_data = request.get_json()
-        # Validate the json data
-        if not all(key in experience_data for key in ['title', 'company', 'start_date',
-                                                      'end_date', 'description', 'logo']):
-            return jsonify({"error": "Missing required fields"}), 400
-
-        new_experience = Experience(
-            experience_data['title'],
-            experience_data['company'],
-            experience_data['start_date'],
-            experience_data['end_date'],
-            experience_data['description'],
-            experience_data['logo']
-        )
-        data['experience'].append(new_experience)
-        return jsonify({"id": len(data['experience']) - 1}), 201
+        try:
+            experience_data = request.get_json()
+            is_valid, error_message = validate_data('experience', experience_data)
+            if not is_valid:
+                return jsonify({"error": error_message}), 400
+
+            new_experience = Experience(
+                experience_data['title'],
+                experience_data['company'],
+                experience_data['start_date'],
+                experience_data['end_date'],
+                experience_data['description'],
+                experience_data['logo']
+            )
+            data['experience'].append(new_experience)
+            return jsonify({"id": len(data['experience']) - 1}), 201
+        except (TypeError, ValueError, KeyError):
+            return jsonify({"error": "Invalid data format"}), 400
path: app.py
user: login: AryanK1511
body: Those exceptions usually mean bad input from the client, so 400 Bad Request makes sense here.
created_at: 2025-05-23T12:08:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,10 +92,17 @@ def education():
     Handles education requests
     '''
     if request.method == 'GET':
-        return jsonify({})
+        return jsonify(data['education']), 200
 
     if request.method == 'POST':
-        return jsonify({})
+        try:
+            education_data = request.get_json()
+            is_valid, error_message = validate_data('education', education_data)
+            if not is_valid:
+                return jsonify({"error": error_message}), 400
+            return jsonify({}), 201
path: app.py
user: login: Pradyuman7
body: Ah, makes sense. Can you add a todo here then
created_at: 2025-05-23T12:55:03Z
author_association: COLLABORATOR
diff_hunk: @@ -125,9 +135,16 @@ def skill():
     Handles Skill requests
     '''
     if request.method == 'GET':
-        return jsonify({})
+        return jsonify(data['skill']), 200
 
     if request.method == 'POST':
-        return jsonify({})
+        try:
+            skill_data = request.get_json()
+            is_valid, error_message = validate_data('skill', skill_data)
+            if not is_valid:
+                return jsonify({"error": error_message}), 400
+            return jsonify({}), 201
path: app.py
user: login: Pradyuman7
body: Same here, let's just add a todo
created_at: 2025-05-23T12:55:12Z
author_association: COLLABORATOR

PR comments:
user: login: AryanK1511
created_at: 2025-05-23T01:30:18Z
body: Note: Tests fail since some routes have not been implemented yet
author_association: CONTRIBUTOR
user: login: AryanK1511
created_at: 2025-05-23T13:09:45Z
body: @Pradyuman7 I have pushed the fixes for this

author_association: CONTRIBUTOR
user: login: Pradyuman7
created_at: 2025-05-23T13:13:46Z
body: Can you fix the pylint as well, I can merge it then
author_association: COLLABORATOR
user: login: AryanK1511
created_at: 2025-05-23T13:21:48Z
body: Fixed!
author_association: CONTRIBUTOR