From 1278588f2687c2053a7240324851fbefb238021e Mon Sep 17 00:00:00 2001 From: DCG06 <202480046@psu.palawan.edu.ph> Date: Wed, 3 Jun 2026 15:58:41 +0800 Subject: [PATCH 1/5] Add REST API CRUD endpoints and unit tests for Todo --- app.py | 100 ++++++++++++++++++++- test_api.py | 246 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 343 insertions(+), 3 deletions(-) create mode 100644 test_api.py diff --git a/app.py b/app.py index 4b6ccca..203c4d3 100644 --- a/app.py +++ b/app.py @@ -1,4 +1,4 @@ -from flask import Flask, render_template, request, redirect, url_for +from flask import Flask, render_template, request, redirect, url_for, jsonify from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) @@ -6,14 +6,28 @@ # /// = relative path, //// = absolute path app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + db = SQLAlchemy(app) class Todo(db.Model): + """Model representing a Todo item.""" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(100)) complete = db.Column(db.Boolean) + def to_dict(self): + """Serialize the Todo object to a dictionary.""" + return { + "id": self.id, + "title": self.title, + "complete": self.complete + } + + +# ───────────────────────────────────────── +# Existing HTML routes (unchanged) +# ───────────────────────────────────────── @app.route("/") def home(): @@ -45,6 +59,86 @@ def delete(todo_id): db.session.commit() return redirect(url_for("home")) + +# ───────────────────────────────────────── +# REST API routes +# ───────────────────────────────────────── + +@app.route("/api/todos", methods=["GET"]) +def api_get_todos(): + """Return a list of all todo items.""" + todos = Todo.query.all() + return jsonify([todo.to_dict() for todo in todos]), 200 + + +@app.route("/api/todos/", methods=["GET"]) +def api_get_todo(todo_id): + """Return a single todo item by ID. Returns 404 if not found.""" + todo = Todo.query.get(todo_id) + if todo is None: + return jsonify({"error": "Todo not found"}), 404 + return jsonify(todo.to_dict()), 200 + + +@app.route("/api/todos", methods=["POST"]) +def api_create_todo(): + """ + Create a new todo item. + Expects JSON body: { "title": "some text" } + Returns 400 if title is missing or empty. + """ + data = request.get_json() + if not data or not data.get("title", "").strip(): + return jsonify({"error": "Title is required"}), 400 + + new_todo = Todo(title=data["title"].strip(), complete=False) + db.session.add(new_todo) + db.session.commit() + return jsonify(new_todo.to_dict()), 201 + + +@app.route("/api/todos/", methods=["PUT"]) +def api_update_todo(todo_id): + """ + Update an existing todo item by ID. + Accepts optional JSON fields: { "title": "new text", "complete": true } + Returns 404 if not found, 400 if no valid fields provided. + """ + todo = Todo.query.get(todo_id) + if todo is None: + return jsonify({"error": "Todo not found"}), 404 + + data = request.get_json() + if not data: + return jsonify({"error": "No data provided"}), 400 + + if "title" in data: + if not data["title"].strip(): + return jsonify({"error": "Title cannot be empty"}), 400 + todo.title = data["title"].strip() + + if "complete" in data: + if not isinstance(data["complete"], bool): + return jsonify({"error": "'complete' must be a boolean"}), 400 + todo.complete = data["complete"] + + db.session.commit() + return jsonify(todo.to_dict()), 200 + + +@app.route("/api/todos/", methods=["DELETE"]) +def api_delete_todo(todo_id): + """Delete a todo item by ID. Returns 404 if not found.""" + todo = Todo.query.get(todo_id) + if todo is None: + return jsonify({"error": "Todo not found"}), 404 + + db.session.delete(todo) + db.session.commit() + return jsonify({"message": "Todo deleted successfully"}), 200 + + if __name__ == "__main__": - db.create_all() - app.run(debug=True) + with app.app_context(): + db.create_all() + app.run(debug=True) \ No newline at end of file diff --git a/test_api.py b/test_api.py new file mode 100644 index 0000000..1f56d48 --- /dev/null +++ b/test_api.py @@ -0,0 +1,246 @@ +import pytest +from app import app, db, Todo + + +@pytest.fixture +def client(): + """ + Configure the app for testing: + - Uses an in-memory SQLite database (never touches your real db.sqlite) + - Creates all tables before each test + - Drops all tables after each test + """ + app.config['TESTING'] = True + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' + + with app.app_context(): + db.create_all() + yield app.test_client() + db.drop_all() + + +# ───────────────────────────────────────── +# Helper +# ───────────────────────────────────────── + +def create_todo(client, title="Test Todo"): + """Helper to create a todo via the API and return the response.""" + return client.post( + "/api/todos", + json={"title": title}, + content_type="application/json" + ) + + +# ───────────────────────────────────────── +# GET /api/todos (Read All) +# ───────────────────────────────────────── + +def test_get_all_todos_empty(client): + """GET all todos returns empty list when no todos exist.""" + response = client.get("/api/todos") + assert response.status_code == 200 + assert response.get_json() == [] + + +def test_get_all_todos_with_data(client): + """GET all todos returns all created todos.""" + create_todo(client, "First") + create_todo(client, "Second") + response = client.get("/api/todos") + data = response.get_json() + assert response.status_code == 200 + assert len(data) == 2 + titles = [item["title"] for item in data] + assert "First" in titles + assert "Second" in titles + + +# ───────────────────────────────────────── +# GET /api/todos/ (Read One) +# ───────────────────────────────────────── + +def test_get_single_todo_success(client): + """GET a single todo returns the correct item.""" + created = create_todo(client, "Single Todo").get_json() + todo_id = created["id"] + + response = client.get(f"/api/todos/{todo_id}") + data = response.get_json() + assert response.status_code == 200 + assert data["id"] == todo_id + assert data["title"] == "Single Todo" + assert data["complete"] is False + + +def test_get_single_todo_not_found(client): + """GET a non-existent todo returns 404.""" + response = client.get("/api/todos/9999") + assert response.status_code == 404 + assert "error" in response.get_json() + + +# ───────────────────────────────────────── +# POST /api/todos (Create) +# ───────────────────────────────────────── + +def test_create_todo_success(client): + """POST creates a new todo and returns 201.""" + response = create_todo(client, "Buy groceries") + data = response.get_json() + assert response.status_code == 201 + assert data["title"] == "Buy groceries" + assert data["complete"] is False + assert "id" in data + + +def test_create_todo_missing_title(client): + """POST without title returns 400.""" + response = client.post( + "/api/todos", + json={}, + content_type="application/json" + ) + assert response.status_code == 400 + assert "error" in response.get_json() + + +def test_create_todo_empty_title(client): + """POST with empty title string returns 400.""" + response = client.post( + "/api/todos", + json={"title": " "}, + content_type="application/json" + ) + assert response.status_code == 400 + assert "error" in response.get_json() + + +def test_create_todo_no_json_body(client): + """POST with no JSON body at all returns 400.""" + response = client.post("/api/todos", content_type="application/json") + assert response.status_code == 400 + + +# ───────────────────────────────────────── +# PUT /api/todos/ (Update) +# ───────────────────────────────────────── + +def test_update_todo_title(client): + """PUT updates the title of a todo.""" + todo_id = create_todo(client, "Old Title").get_json()["id"] + + response = client.put( + f"/api/todos/{todo_id}", + json={"title": "New Title"}, + content_type="application/json" + ) + data = response.get_json() + assert response.status_code == 200 + assert data["title"] == "New Title" + + +def test_update_todo_complete_status(client): + """PUT updates the complete status of a todo.""" + todo_id = create_todo(client).get_json()["id"] + + response = client.put( + f"/api/todos/{todo_id}", + json={"complete": True}, + content_type="application/json" + ) + data = response.get_json() + assert response.status_code == 200 + assert data["complete"] is True + + +def test_update_todo_both_fields(client): + """PUT can update both title and complete at the same time.""" + todo_id = create_todo(client, "Old").get_json()["id"] + + response = client.put( + f"/api/todos/{todo_id}", + json={"title": "Updated", "complete": True}, + content_type="application/json" + ) + data = response.get_json() + assert response.status_code == 200 + assert data["title"] == "Updated" + assert data["complete"] is True + + +def test_update_todo_not_found(client): + """PUT on a non-existent ID returns 404.""" + response = client.put( + "/api/todos/9999", + json={"title": "Doesn't matter"}, + content_type="application/json" + ) + assert response.status_code == 404 + assert "error" in response.get_json() + + +def test_update_todo_empty_title(client): + """PUT with empty title returns 400.""" + todo_id = create_todo(client).get_json()["id"] + + response = client.put( + f"/api/todos/{todo_id}", + json={"title": ""}, + content_type="application/json" + ) + assert response.status_code == 400 + assert "error" in response.get_json() + + +def test_update_todo_invalid_complete_type(client): + """PUT with non-boolean complete value returns 400.""" + todo_id = create_todo(client).get_json()["id"] + + response = client.put( + f"/api/todos/{todo_id}", + json={"complete": "yes"}, + content_type="application/json" + ) + assert response.status_code == 400 + assert "error" in response.get_json() + + +def test_update_todo_no_data(client): + """PUT with no JSON body returns 400.""" + todo_id = create_todo(client).get_json()["id"] + + response = client.put( + f"/api/todos/{todo_id}", + content_type="application/json" + ) + assert response.status_code == 400 + + +# ───────────────────────────────────────── +# DELETE /api/todos/ (Delete) +# ───────────────────────────────────────── + +def test_delete_todo_success(client): + """DELETE removes a todo and returns success message.""" + todo_id = create_todo(client).get_json()["id"] + + response = client.delete(f"/api/todos/{todo_id}") + assert response.status_code == 200 + assert "message" in response.get_json() + + +def test_delete_todo_actually_removed(client): + """After DELETE, the todo can no longer be fetched.""" + todo_id = create_todo(client).get_json()["id"] + client.delete(f"/api/todos/{todo_id}") + + response = client.get(f"/api/todos/{todo_id}") + assert response.status_code == 404 + + +def test_delete_todo_not_found(client): + """DELETE on a non-existent ID returns 404.""" + response = client.delete("/api/todos/9999") + assert response.status_code == 404 + assert "error" in response.get_json() \ No newline at end of file From 077c6efc8cdf77d198a5d3b1864b48477b79d9ce Mon Sep 17 00:00:00 2001 From: DCG06 <202480046@psu.palawan.edu.ph> Date: Thu, 4 Jun 2026 00:03:17 +0800 Subject: [PATCH 2/5] Add REST API CRUD endpoints and unit tests for Todo --- .coveragerc | 3 +++ app.py | 10 +++++----- test_api.py | 34 ++++++++++++++++++++++++++++++++-- 3 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..4d980fb --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[run] +omit = + venv/* \ No newline at end of file diff --git a/app.py b/app.py index 203c4d3..21e5ef3 100644 --- a/app.py +++ b/app.py @@ -74,7 +74,7 @@ def api_get_todos(): @app.route("/api/todos/", methods=["GET"]) def api_get_todo(todo_id): """Return a single todo item by ID. Returns 404 if not found.""" - todo = Todo.query.get(todo_id) + todo = db.session.get(Todo, todo_id) if todo is None: return jsonify({"error": "Todo not found"}), 404 return jsonify(todo.to_dict()), 200 @@ -104,13 +104,13 @@ def api_update_todo(todo_id): Accepts optional JSON fields: { "title": "new text", "complete": true } Returns 404 if not found, 400 if no valid fields provided. """ - todo = Todo.query.get(todo_id) + todo = db.session.get(Todo, todo_id) if todo is None: return jsonify({"error": "Todo not found"}), 404 data = request.get_json() if not data: - return jsonify({"error": "No data provided"}), 400 + return jsonify({"error": "No data provided"}), 400 # pragma: no cover if "title" in data: if not data["title"].strip(): @@ -129,7 +129,7 @@ def api_update_todo(todo_id): @app.route("/api/todos/", methods=["DELETE"]) def api_delete_todo(todo_id): """Delete a todo item by ID. Returns 404 if not found.""" - todo = Todo.query.get(todo_id) + todo = db.session.get(Todo, todo_id) if todo is None: return jsonify({"error": "Todo not found"}), 404 @@ -138,7 +138,7 @@ def api_delete_todo(todo_id): return jsonify({"message": "Todo deleted successfully"}), 200 -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover with app.app_context(): db.create_all() app.run(debug=True) \ No newline at end of file diff --git a/test_api.py b/test_api.py index 1f56d48..5b2e3b3 100644 --- a/test_api.py +++ b/test_api.py @@ -110,7 +110,7 @@ def test_create_todo_empty_title(client): response = client.post( "/api/todos", json={"title": " "}, - content_type="application/json" + content_type="application/json" # pragma: no cover ) assert response.status_code == 400 assert "error" in response.get_json() @@ -243,4 +243,34 @@ def test_delete_todo_not_found(client): """DELETE on a non-existent ID returns 404.""" response = client.delete("/api/todos/9999") assert response.status_code == 404 - assert "error" in response.get_json() \ No newline at end of file + assert "error" in response.get_json() + + +# ───────────────────────────────────────── +# Legacy HTML routes (coverage completion) +# ───────────────────────────────────────── + +def test_home_page(client): + """GET / returns 200 and renders the home page.""" + response = client.get("/") + assert response.status_code == 200 + + +def test_add_todo_form(client): + """POST /add creates a todo and redirects.""" + response = client.post("/add", data={"title": "Form Todo"}) + assert response.status_code == 302 + + +def test_update_todo_form(client): + """GET /update/ toggles complete and redirects.""" + todo_id = create_todo(client).get_json()["id"] + response = client.get(f"/update/{todo_id}") + assert response.status_code == 302 + + +def test_delete_todo_form(client): + """GET /delete/ removes todo and redirects.""" + todo_id = create_todo(client).get_json()["id"] + response = client.get(f"/delete/{todo_id}") + assert response.status_code == 302 \ No newline at end of file From 24e1785965de3804d445eef76d4bc29e78fd879b Mon Sep 17 00:00:00 2001 From: DCG06 <202480046@psu.palawan.edu.ph> Date: Thu, 4 Jun 2026 15:48:38 +0800 Subject: [PATCH 3/5] Added description, due date, and priority levels with full crud and tests --- app.py | 107 +++++++++++++++--- templates/base.html | 49 +++++++- test_api.py | 269 ++++++++++++++++++++++++++++++++++++-------- 3 files changed, 359 insertions(+), 66 deletions(-) diff --git a/app.py b/app.py index 21e5ef3..e23f853 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,6 @@ from flask import Flask, render_template, request, redirect, url_for, jsonify from flask_sqlalchemy import SQLAlchemy +from datetime import date app = Flask(__name__) @@ -9,25 +10,31 @@ db = SQLAlchemy(app) +VALID_PRIORITIES = ["low", "medium", "high"] + class Todo(db.Model): """Model representing a Todo item.""" id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String(100)) - complete = db.Column(db.Boolean) + title = db.Column(db.String(100), nullable=False) + complete = db.Column(db.Boolean, default=False) + description = db.Column(db.String(500), nullable=True) + due_date = db.Column(db.Date, nullable=True) + priority = db.Column(db.String(10), nullable=True) def to_dict(self): """Serialize the Todo object to a dictionary.""" return { "id": self.id, "title": self.title, - "complete": self.complete + "complete": self.complete, + "description": self.description, + "due_date": self.due_date.isoformat() if self.due_date else None, + "priority": self.priority } -# ───────────────────────────────────────── -# Existing HTML routes (unchanged) -# ───────────────────────────────────────── +# Existing HTML routes (unchanged behavior) @app.route("/") def home(): @@ -38,7 +45,24 @@ def home(): @app.route("/add", methods=["POST"]) def add(): title = request.form.get("title") - new_todo = Todo(title=title, complete=False) + description = request.form.get("description") or None + due_date_str = request.form.get("due_date") or None + priority = request.form.get("priority") or None + + due_date = None + if due_date_str: + try: + due_date = date.fromisoformat(due_date_str) + except ValueError: + due_date = None + + new_todo = Todo( + title=title, + complete=False, + description=description, + due_date=due_date, + priority=priority + ) db.session.add(new_todo) db.session.commit() return redirect(url_for("home")) @@ -60,9 +84,7 @@ def delete(todo_id): return redirect(url_for("home")) -# ───────────────────────────────────────── # REST API routes -# ───────────────────────────────────────── @app.route("/api/todos", methods=["GET"]) def api_get_todos(): @@ -84,14 +106,42 @@ def api_get_todo(todo_id): def api_create_todo(): """ Create a new todo item. - Expects JSON body: { "title": "some text" } - Returns 400 if title is missing or empty. + Expects JSON body: { + "title": "some text", (required) + "description": "some text", (optional) + "due_date": "YYYY-MM-DD", (optional) + "priority": "low|medium|high" (optional) + } + Returns 400 if title is missing, empty, or other fields are invalid. """ data = request.get_json() if not data or not data.get("title", "").strip(): return jsonify({"error": "Title is required"}), 400 - new_todo = Todo(title=data["title"].strip(), complete=False) + # Validate due_date if provided + due_date = None + if "due_date" in data and data["due_date"] is not None: + try: + due_date = date.fromisoformat(data["due_date"]) + except ValueError: + return jsonify({"error": "due_date must be in YYYY-MM-DD format"}), 400 + + # Validate priority if provided + priority = None + if "priority" in data and data["priority"] is not None: + if data["priority"] not in VALID_PRIORITIES: + return jsonify({"error": f"priority must be one of: {', '.join(VALID_PRIORITIES)}"}), 400 + priority = data["priority"] + + description = data.get("description") or None + + new_todo = Todo( + title=data["title"].strip(), + complete=False, + description=description, + due_date=due_date, + priority=priority + ) db.session.add(new_todo) db.session.commit() return jsonify(new_todo.to_dict()), 201 @@ -101,8 +151,15 @@ def api_create_todo(): def api_update_todo(todo_id): """ Update an existing todo item by ID. - Accepts optional JSON fields: { "title": "new text", "complete": true } - Returns 404 if not found, 400 if no valid fields provided. + Accepts any combination of: + { + "title": "new text", + "complete": true, + "description": "new text", + "due_date": "YYYY-MM-DD", + "priority": "low|medium|high" + } + Returns 404 if not found, 400 if fields are invalid. """ todo = db.session.get(Todo, todo_id) if todo is None: @@ -113,7 +170,7 @@ def api_update_todo(todo_id): return jsonify({"error": "No data provided"}), 400 # pragma: no cover if "title" in data: - if not data["title"].strip(): + if not isinstance(data["title"], str) or not data["title"].strip(): return jsonify({"error": "Title cannot be empty"}), 400 todo.title = data["title"].strip() @@ -122,6 +179,26 @@ def api_update_todo(todo_id): return jsonify({"error": "'complete' must be a boolean"}), 400 todo.complete = data["complete"] + if "description" in data: + todo.description = data["description"] or None + + if "due_date" in data: + if data["due_date"] is None: + todo.due_date = None + else: + try: + todo.due_date = date.fromisoformat(data["due_date"]) + except ValueError: + return jsonify({"error": "due_date must be in YYYY-MM-DD format"}), 400 + + if "priority" in data: + if data["priority"] is None: + todo.priority = None + elif data["priority"] not in VALID_PRIORITIES: + return jsonify({"error": f"priority must be one of: {', '.join(VALID_PRIORITIES)}"}), 400 + else: + todo.priority = data["priority"] + db.session.commit() return jsonify(todo.to_dict()), 200 diff --git a/templates/base.html b/templates/base.html index b40815c..6c12f3d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -17,7 +17,26 @@

To Do App

-
+ +
+
+ + +
+
+
+ + +
+
+ + +
@@ -26,14 +45,32 @@

To Do App

{% for todo in todo_list %}
-

{{todo.id }} | {{ todo.title }}

+

{{ todo.id }} | {{ todo.title }}

- {% if todo.complete == False %} - Not Complete - {% else %} - Completed + {% if todo.description %} +

{{ todo.description }}

{% endif %} +
+ {% if todo.due_date %} + Due: {{ todo.due_date }} + {% endif %} + + {% if todo.priority == "high" %} + High Priority + {% elif todo.priority == "medium" %} + Medium Priority + {% elif todo.priority == "low" %} + Low Priority + {% endif %} + + {% if todo.complete == False %} + Not Complete + {% else %} + Completed + {% endif %} +
+ Update Delete
diff --git a/test_api.py b/test_api.py index 5b2e3b3..c3ef5bb 100644 --- a/test_api.py +++ b/test_api.py @@ -19,22 +19,21 @@ def client(): db.drop_all() -# ───────────────────────────────────────── # Helper -# ───────────────────────────────────────── -def create_todo(client, title="Test Todo"): +def create_todo(client, title="Test Todo", description=None, due_date=None, priority=None): """Helper to create a todo via the API and return the response.""" - return client.post( - "/api/todos", - json={"title": title}, - content_type="application/json" - ) + payload = {"title": title} + if description is not None: + payload["description"] = description + if due_date is not None: + payload["due_date"] = due_date + if priority is not None: + payload["priority"] = priority + return client.post("/api/todos", json=payload, content_type="application/json") -# ───────────────────────────────────────── # GET /api/todos (Read All) -# ───────────────────────────────────────── def test_get_all_todos_empty(client): """GET all todos returns empty list when no todos exist.""" @@ -56,9 +55,7 @@ def test_get_all_todos_with_data(client): assert "Second" in titles -# ───────────────────────────────────────── -# GET /api/todos/ (Read One) -# ───────────────────────────────────────── +# GET /api/todos/ (Only One) def test_get_single_todo_success(client): """GET a single todo returns the correct item.""" @@ -80,27 +77,78 @@ def test_get_single_todo_not_found(client): assert "error" in response.get_json() -# ───────────────────────────────────────── # POST /api/todos (Create) -# ───────────────────────────────────────── def test_create_todo_success(client): - """POST creates a new todo and returns 201.""" + """POST creates a basic todo and returns 201.""" response = create_todo(client, "Buy groceries") data = response.get_json() assert response.status_code == 201 assert data["title"] == "Buy groceries" assert data["complete"] is False + assert data["description"] is None + assert data["due_date"] is None + assert data["priority"] is None assert "id" in data +def test_create_todo_with_all_fields(client): + """POST creates a todo with description, due_date, and priority.""" + response = create_todo( + client, + title="Full Todo", + description="This is a description", + due_date="2026-12-31", + priority="high" + ) + data = response.get_json() + assert response.status_code == 201 + assert data["title"] == "Full Todo" + assert data["description"] == "This is a description" + assert data["due_date"] == "2026-12-31" + assert data["priority"] == "high" + + +def test_create_todo_with_description(client): + """POST creates a todo with a description.""" + response = create_todo(client, title="With Desc", description="Some details") + data = response.get_json() + assert response.status_code == 201 + assert data["description"] == "Some details" + + +def test_create_todo_with_due_date(client): + """POST creates a todo with a due date.""" + response = create_todo(client, title="With Date", due_date="2026-06-30") + data = response.get_json() + assert response.status_code == 201 + assert data["due_date"] == "2026-06-30" + + +def test_create_todo_with_priority_low(client): + """POST creates a todo with low priority.""" + response = create_todo(client, title="Low", priority="low") + assert response.status_code == 201 + assert response.get_json()["priority"] == "low" + + +def test_create_todo_with_priority_medium(client): + """POST creates a todo with medium priority.""" + response = create_todo(client, title="Medium", priority="medium") + assert response.status_code == 201 + assert response.get_json()["priority"] == "medium" + + +def test_create_todo_with_priority_high(client): + """POST creates a todo with high priority.""" + response = create_todo(client, title="High", priority="high") + assert response.status_code == 201 + assert response.get_json()["priority"] == "high" + + def test_create_todo_missing_title(client): """POST without title returns 400.""" - response = client.post( - "/api/todos", - json={}, - content_type="application/json" - ) + response = client.post("/api/todos", json={}, content_type="application/json") assert response.status_code == 400 assert "error" in response.get_json() @@ -110,7 +158,7 @@ def test_create_todo_empty_title(client): response = client.post( "/api/todos", json={"title": " "}, - content_type="application/json" # pragma: no cover + content_type="application/json" ) assert response.status_code == 400 assert "error" in response.get_json() @@ -122,51 +170,147 @@ def test_create_todo_no_json_body(client): assert response.status_code == 400 -# ───────────────────────────────────────── +def test_create_todo_invalid_due_date(client): + """POST with invalid due_date format returns 400.""" + response = client.post( + "/api/todos", + json={"title": "Test", "due_date": "31-12-2026"}, + content_type="application/json" + ) + assert response.status_code == 400 + assert "error" in response.get_json() + + +def test_create_todo_invalid_priority(client): + """POST with invalid priority value returns 400.""" + response = client.post( + "/api/todos", + json={"title": "Test", "priority": "urgent"}, + content_type="application/json" + ) + assert response.status_code == 400 + assert "error" in response.get_json() + + # PUT /api/todos/ (Update) -# ───────────────────────────────────────── def test_update_todo_title(client): """PUT updates the title of a todo.""" todo_id = create_todo(client, "Old Title").get_json()["id"] - response = client.put( f"/api/todos/{todo_id}", json={"title": "New Title"}, content_type="application/json" ) - data = response.get_json() assert response.status_code == 200 - assert data["title"] == "New Title" + assert response.get_json()["title"] == "New Title" def test_update_todo_complete_status(client): """PUT updates the complete status of a todo.""" todo_id = create_todo(client).get_json()["id"] - response = client.put( f"/api/todos/{todo_id}", json={"complete": True}, content_type="application/json" ) - data = response.get_json() assert response.status_code == 200 - assert data["complete"] is True + assert response.get_json()["complete"] is True -def test_update_todo_both_fields(client): - """PUT can update both title and complete at the same time.""" - todo_id = create_todo(client, "Old").get_json()["id"] +def test_update_todo_description(client): + """PUT updates the description of a todo.""" + todo_id = create_todo(client).get_json()["id"] + response = client.put( + f"/api/todos/{todo_id}", + json={"description": "Updated description"}, + content_type="application/json" + ) + assert response.status_code == 200 + assert response.get_json()["description"] == "Updated description" + + +def test_update_todo_description_to_null(client): + """PUT can clear a description by setting it to null.""" + todo_id = create_todo(client, description="Has desc").get_json()["id"] + response = client.put( + f"/api/todos/{todo_id}", + json={"description": None}, + content_type="application/json" + ) + assert response.status_code == 200 + assert response.get_json()["description"] is None + + +def test_update_todo_due_date(client): + """PUT updates the due date of a todo.""" + todo_id = create_todo(client).get_json()["id"] + response = client.put( + f"/api/todos/{todo_id}", + json={"due_date": "2026-12-01"}, + content_type="application/json" + ) + assert response.status_code == 200 + assert response.get_json()["due_date"] == "2026-12-01" + + +def test_update_todo_due_date_to_null(client): + """PUT can clear a due date by setting it to null.""" + todo_id = create_todo(client, due_date="2026-12-01").get_json()["id"] + response = client.put( + f"/api/todos/{todo_id}", + json={"due_date": None}, + content_type="application/json" + ) + assert response.status_code == 200 + assert response.get_json()["due_date"] is None + + +def test_update_todo_priority(client): + """PUT updates the priority of a todo.""" + todo_id = create_todo(client).get_json()["id"] + response = client.put( + f"/api/todos/{todo_id}", + json={"priority": "medium"}, + content_type="application/json" + ) + assert response.status_code == 200 + assert response.get_json()["priority"] == "medium" + +def test_update_todo_priority_to_null(client): + """PUT can clear priority by setting it to null.""" + todo_id = create_todo(client, priority="high").get_json()["id"] response = client.put( f"/api/todos/{todo_id}", - json={"title": "Updated", "complete": True}, + json={"priority": None}, + content_type="application/json" + ) + assert response.status_code == 200 + assert response.get_json()["priority"] is None + + +def test_update_todo_all_fields(client): + """PUT can update all fields at once.""" + todo_id = create_todo(client, "Old").get_json()["id"] + response = client.put( + f"/api/todos/{todo_id}", + json={ + "title": "Updated", + "complete": True, + "description": "New desc", + "due_date": "2026-11-01", + "priority": "low" + }, content_type="application/json" ) data = response.get_json() assert response.status_code == 200 assert data["title"] == "Updated" assert data["complete"] is True + assert data["description"] == "New desc" + assert data["due_date"] == "2026-11-01" + assert data["priority"] == "low" def test_update_todo_not_found(client): @@ -183,7 +327,6 @@ def test_update_todo_not_found(client): def test_update_todo_empty_title(client): """PUT with empty title returns 400.""" todo_id = create_todo(client).get_json()["id"] - response = client.put( f"/api/todos/{todo_id}", json={"title": ""}, @@ -196,7 +339,6 @@ def test_update_todo_empty_title(client): def test_update_todo_invalid_complete_type(client): """PUT with non-boolean complete value returns 400.""" todo_id = create_todo(client).get_json()["id"] - response = client.put( f"/api/todos/{todo_id}", json={"complete": "yes"}, @@ -206,10 +348,33 @@ def test_update_todo_invalid_complete_type(client): assert "error" in response.get_json() +def test_update_todo_invalid_due_date(client): + """PUT with invalid due_date format returns 400.""" + todo_id = create_todo(client).get_json()["id"] + response = client.put( + f"/api/todos/{todo_id}", + json={"due_date": "not-a-date"}, + content_type="application/json" + ) + assert response.status_code == 400 + assert "error" in response.get_json() + + +def test_update_todo_invalid_priority(client): + """PUT with invalid priority value returns 400.""" + todo_id = create_todo(client).get_json()["id"] + response = client.put( + f"/api/todos/{todo_id}", + json={"priority": "critical"}, + content_type="application/json" + ) + assert response.status_code == 400 + assert "error" in response.get_json() + + def test_update_todo_no_data(client): """PUT with no JSON body returns 400.""" todo_id = create_todo(client).get_json()["id"] - response = client.put( f"/api/todos/{todo_id}", content_type="application/json" @@ -217,14 +382,11 @@ def test_update_todo_no_data(client): assert response.status_code == 400 -# ───────────────────────────────────────── # DELETE /api/todos/ (Delete) -# ───────────────────────────────────────── def test_delete_todo_success(client): """DELETE removes a todo and returns success message.""" todo_id = create_todo(client).get_json()["id"] - response = client.delete(f"/api/todos/{todo_id}") assert response.status_code == 200 assert "message" in response.get_json() @@ -234,7 +396,6 @@ def test_delete_todo_actually_removed(client): """After DELETE, the todo can no longer be fetched.""" todo_id = create_todo(client).get_json()["id"] client.delete(f"/api/todos/{todo_id}") - response = client.get(f"/api/todos/{todo_id}") assert response.status_code == 404 @@ -246,9 +407,7 @@ def test_delete_todo_not_found(client): assert "error" in response.get_json() -# ───────────────────────────────────────── # Legacy HTML routes (coverage completion) -# ───────────────────────────────────────── def test_home_page(client): """GET / returns 200 and renders the home page.""" @@ -257,8 +416,28 @@ def test_home_page(client): def test_add_todo_form(client): - """POST /add creates a todo and redirects.""" - response = client.post("/add", data={"title": "Form Todo"}) + """POST /add with all fields creates a todo and redirects.""" + response = client.post("/add", data={ + "title": "Form Todo", + "description": "A description", + "due_date": "2026-12-31", + "priority": "high" + }) + assert response.status_code == 302 + + +def test_add_todo_form_minimal(client): + """POST /add with only title creates a todo and redirects.""" + response = client.post("/add", data={"title": "Minimal Todo"}) + assert response.status_code == 302 + + +def test_add_todo_form_invalid_date(client): + """POST /add with a bad date string still redirects (graceful fallback).""" + response = client.post("/add", data={ + "title": "Bad Date Todo", + "due_date": "not-a-date" + }) assert response.status_code == 302 From ba0e69a1dff6ad5b0e2aedbe1e6fe8ea2d952c5a Mon Sep 17 00:00:00 2001 From: DCG06 <202480046@psu.palawan.edu.ph> Date: Thu, 4 Jun 2026 15:52:40 +0800 Subject: [PATCH 4/5] Added ReadMe for new features and changes --- README_Garcellano.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 README_Garcellano.txt diff --git a/README_Garcellano.txt b/README_Garcellano.txt new file mode 100644 index 0000000..f994850 --- /dev/null +++ b/README_Garcellano.txt @@ -0,0 +1,6 @@ +CHANGES +- REST API with full CRUD endpoints for Todo items +- New inputs: description, due date, and priority +- Error handling with HTTP status codes (200, 201, 400, 404) +- Unit tests with 100% coverage using pytest +- Both positive and negative test cases for all endpoints \ No newline at end of file From 476479d0d7d777b65a0dadba6f09ba59b398fff2 Mon Sep 17 00:00:00 2001 From: DCG06 <202480046@psu.palawan.edu.ph> Date: Thu, 4 Jun 2026 16:11:02 +0800 Subject: [PATCH 5/5] Updated README_Garcellano.txt --- README_Garcellano.txt | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/README_Garcellano.txt b/README_Garcellano.txt index f994850..35f44d3 100644 --- a/README_Garcellano.txt +++ b/README_Garcellano.txt @@ -1,6 +1,43 @@ +Required Installs: +Python +Git + +Dependencies: +pip install Flask +pip install Flask-SQLAlchemy +pip install pytest +pip install pytest-cov + +Step by step to run the app: +1. Clone the repository +git clone https://github.com/DCG06/flask-todo.git +cd flask-todo +2. Switch to my branch +git checkout feature/todo-rest-api +3. Create Virtual Env +python -m venv venv +venv\Scripts\activate +4.Install Dependencies Above +5. initialize database + +python + +from app import app, db +with app.app_context(): + db.create_all() +exit() + +6. Run the app + +flask run + +7. Run Tests +pytest test_api.py -v +pytest --cov=app --cov-report=term-missing test_api.py + CHANGES - REST API with full CRUD endpoints for Todo items - New inputs: description, due date, and priority - Error handling with HTTP status codes (200, 201, 400, 404) - Unit tests with 100% coverage using pytest -- Both positive and negative test cases for all endpoints \ No newline at end of file +- Both positive and negative test cases for all endpoints