Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 28 additions & 12 deletions app/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import copy

from bson.objectid import ObjectId
from flask import jsonify, request
from pymongo.errors import ConnectionFailure
from werkzeug.exceptions import HTTPException, NotFound
Expand Down Expand Up @@ -88,7 +89,6 @@ def add_book():
host = request.host_url
# Send the host and new book_id to the helper function to generate links
final_book_for_api = append_hostname(book_from_db, host)
print("final_book_for_api", final_book_for_api)

# Transform _id to id and remove the internal _id
final_book_for_api["id"] = str(final_book_for_api["_id"])
Expand Down Expand Up @@ -141,20 +141,36 @@ def get_book(book_id):
"""
Retrieve a specific book by its unique ID.
"""
if not books:
return jsonify({"error": "Book collection not initialized"}), 500
# get the collection
collection = get_book_collection()

# extract host from the request
if collection is None:
return jsonify({"error": "Book collection not found"}), 500

# sanity check book_id
if not ObjectId.is_valid(book_id):
return jsonify({"error": "Invalid book ID format"}), 400
obj_id = ObjectId(book_id)

# Query db for a non-deleted book
query = {"_id": obj_id, "state": {"$ne": "deleted"}}
# look it up in MongoDB
book = collection.find_one(query)
Copy link

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding exception handling around the MongoDB query. Database operations can raise exceptions (ConnectionFailure, ServerSelectionTimeoutError) that should be caught and converted to appropriate HTTP error responses.

Copilot uses AI. Check for mistakes.
# also equivalent to Key version
# book = raw_books.find_one(_id=obj_id, state={"$ne": "deleted"})
Comment on lines +159 to +160
Copy link

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The comment 'also equivalent to Key version' is unclear and doesn't add value. Consider removing it or making it more descriptive.

Suggested change
# also equivalent to Key version
# book = raw_books.find_one(_id=obj_id, state={"$ne": "deleted"})
# Alternative query to fetch a non-deleted book by its ID
# book = collection.find_one({"_id": obj_id, "state": {"$ne": "deleted"}})

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commented-out code should be removed as it serves no purpose and adds confusion. The variable name 'raw_books' doesn't match the actual collection variable name.

Suggested change
# book = raw_books.find_one(_id=obj_id, state={"$ne": "deleted"})

Copilot uses AI. Check for mistakes.

if book is None:
return jsonify({"error": "Book not found"}), 404

# Format for API response
host = request.host_url
formatted_book = append_hostname(book, host)

for book in books:
if book.get("id") == book_id and book.get("state") != "deleted":
# copy the book
book_copy = copy.deepcopy(book)
book_copy.pop("state", None)
# Add the hostname to the book_copy object and return it
return jsonify(append_hostname(book_copy, host)), 200
return jsonify({"error": "Book not found"}), 404
formatted_book["id"] = str(formatted_book["_id"])
formatted_book.pop("state", None)
formatted_book.pop("_id", None)

return jsonify(formatted_book), 200

# ----------- DELETE section ------------------
@app.route("/books/<string:book_id>", methods=["DELETE"])
Expand Down
37 changes: 34 additions & 3 deletions openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ components:
required:
- error

SimpleError:
type: object
properties:
error:
type: string
description: A brief error message.
required:
- error

# API Error: Reusable responses for common errors
responses:
BadRequest:
Expand Down Expand Up @@ -263,10 +272,32 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/BookOutput'
'400':
description: |-
Bad Request. The provided book_id is not a valid 24-character hexadecimal string.
content:
application/json:
schema:
$ref: '#/components/schemas/SimpleError'
example:
error: "Invalid book ID format"
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
description: A book with the specified ID was not found.
content:
application/json:
schema:
$ref: '#/components/schemas/SimpleError'
example:
error: "Book not found"
'500':
description: |-
Service Unavailable. The server cannot connect to the database.
content:
application/json:
schema:
$ref: '#/components/schemas/SimpleError'
example:
error: "Book collection not found"
# --------------------------------------------

put:
Expand Down
21 changes: 21 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import pytest

from app import create_app
from app.datastore.mongo_db import get_book_collection


@pytest.fixture(name="_insert_book_to_db")
Expand Down Expand Up @@ -84,3 +85,23 @@ def test_app():
def client(test_app): # pylint: disable=redefined-outer-name
"""A test client for the app."""
return test_app.test_client()


@pytest.fixture(scope="function")
def db_setup(test_app): # pylint: disable=redefined-outer-name
"""
Sets up and tears down the database for a test.
Scope is "function" to ensure a clean DB for each test.
"""
# Use app_context to access the database
with test_app.app_context():
collection = get_book_collection()

collection.delete_many({})
# Pass control to the test function
yield

# Teardown: code runs after the test is finished
with test_app.app_context():
collection = get_book_collection()
collection.delete_many({})
Loading