From f3865209c26a4e2f5dada2c5461617ddab8a4958 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:38:29 +0000 Subject: [PATCH] Add unit tests for book_review.py and app.py - 13 tests for book_review.py covering get_all_records, get_record_id, update_record, and add_record with mocked Airtable API - 12 tests for Flask API endpoints (uppercase, records, add-record) covering success paths, error handling, and edge cases - All tests use unittest.mock to avoid external dependencies Co-Authored-By: David Greenstein --- tests/__init__.py | 1 + tests/test_app.py | 137 ++++++++++++++++++++++++++++++++++++ tests/test_book_review.py | 144 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 282 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/test_app.py create mode 100644 tests/test_book_review.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..f2fd8e9 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,137 @@ +from unittest.mock import patch, MagicMock +import json +import pytest + + +@pytest.fixture +def client(): + """Create a Flask test client with mocked book_review module.""" + with patch("book_review.Api"), \ + patch("book_review.api"), \ + patch("book_review.table"): + from app import app + app.config["TESTING"] = True + with app.test_client() as client: + yield client + + +class TestUppercaseEndpoint: + def test_converts_text_to_uppercase(self, client): + response = client.get("/uppercase?text=hello") + data = response.get_json() + + assert response.status_code == 200 + assert data["text"] == "HELLO" + + def test_converts_mixed_case(self, client): + response = client.get("/uppercase?text=Hello+World") + data = response.get_json() + + assert response.status_code == 200 + assert data["text"] == "HELLO WORLD" + + def test_already_uppercase(self, client): + response = client.get("/uppercase?text=ALREADY") + data = response.get_json() + + assert response.status_code == 200 + assert data["text"] == "ALREADY" + + def test_empty_string(self, client): + response = client.get("/uppercase?text=") + data = response.get_json() + + assert response.status_code == 200 + assert data["text"] == "" + + def test_special_characters(self, client): + response = client.get("/uppercase?text=hello%21%40%23") + data = response.get_json() + + assert response.status_code == 200 + assert data["text"] == "HELLO!@#" + + +class TestRecordsEndpoint: + @patch("book_review.get_all_records") + def test_returns_books(self, mock_get_all, client): + mock_get_all.return_value = [ + {"id": "rec1", "fields": {"Book": "Book A", "Rating": 8}}, + ] + + response = client.get("/records") + data = response.get_json() + + assert response.status_code == 200 + assert "books" in data + assert len(data["books"]) == 1 + + @patch("book_review.get_all_records") + def test_passes_count_and_sort(self, mock_get_all, client): + mock_get_all.return_value = [] + + client.get("/records?count=5&sort=ASC") + + mock_get_all.assert_called_once_with(count="5", sort="ASC") + + @patch("book_review.get_all_records") + def test_returns_empty_list(self, mock_get_all, client): + mock_get_all.return_value = [] + + response = client.get("/records") + data = response.get_json() + + assert response.status_code == 200 + assert data["books"] == [] + + +class TestAddRecordEndpoint: + @patch("book_review.add_record") + def test_adds_record_successfully(self, mock_add, client): + mock_add.return_value = True + + response = client.post( + "/add-record", + data=json.dumps({"Book": "Test Book", "Rating": 8}), + content_type="application/json", + ) + data = response.get_json() + + assert response.status_code == 200 + assert data["message"] == "Record added successfully" + + @patch("book_review.add_record") + def test_returns_400_when_book_missing(self, mock_add, client): + response = client.post( + "/add-record", + data=json.dumps({"Rating": 8}), + content_type="application/json", + ) + data = response.get_json() + + assert response.status_code == 400 + + @patch("book_review.add_record") + def test_returns_400_when_rating_missing(self, mock_add, client): + response = client.post( + "/add-record", + data=json.dumps({"Book": "Test Book"}), + content_type="application/json", + ) + data = response.get_json() + + assert response.status_code == 400 + + @patch("book_review.add_record") + def test_returns_500_when_add_fails(self, mock_add, client): + mock_add.return_value = False + + response = client.post( + "/add-record", + data=json.dumps({"Book": "Test Book", "Rating": 8}), + content_type="application/json", + ) + data = response.get_json() + + assert response.status_code == 500 + assert data["message"] == "Failed to add record" diff --git a/tests/test_book_review.py b/tests/test_book_review.py new file mode 100644 index 0000000..bf769f3 --- /dev/null +++ b/tests/test_book_review.py @@ -0,0 +1,144 @@ +from unittest.mock import patch, MagicMock +import pytest + + +class TestGetAllRecords: + @patch("book_review.table") + def test_returns_all_records_no_params(self, mock_table): + import book_review + + mock_table.all.return_value = [ + {"id": "rec1", "fields": {"Book": "Book A", "Rating": 8}}, + {"id": "rec2", "fields": {"Book": "Book B", "Rating": 5}}, + ] + + result = book_review.get_all_records() + + mock_table.all.assert_called_once_with(max_records=None, sort=[]) + assert len(result) == 2 + + @patch("book_review.table") + def test_returns_limited_records_with_count(self, mock_table): + import book_review + + mock_table.all.return_value = [ + {"id": "rec1", "fields": {"Book": "Book A", "Rating": 8}}, + ] + + result = book_review.get_all_records(count=1) + + mock_table.all.assert_called_once_with(max_records=1, sort=[]) + assert len(result) == 1 + + @patch("book_review.table") + def test_sorts_ascending(self, mock_table): + import book_review + + mock_table.all.return_value = [] + + book_review.get_all_records(sort="ASC") + + mock_table.all.assert_called_once_with(max_records=None, sort=["Rating"]) + + @patch("book_review.table") + def test_sorts_descending(self, mock_table): + import book_review + + mock_table.all.return_value = [] + + book_review.get_all_records(sort="DESC") + + mock_table.all.assert_called_once_with(max_records=None, sort=["-Rating"]) + + @patch("book_review.table") + def test_sort_case_insensitive(self, mock_table): + import book_review + + mock_table.all.return_value = [] + + book_review.get_all_records(sort="desc") + + mock_table.all.assert_called_once_with(max_records=None, sort=["-Rating"]) + + @patch("book_review.table") + def test_invalid_sort_ignored(self, mock_table): + import book_review + + mock_table.all.return_value = [] + + book_review.get_all_records(sort="INVALID") + + mock_table.all.assert_called_once_with(max_records=None, sort=[]) + + @patch("book_review.table") + def test_count_and_sort_together(self, mock_table): + import book_review + + mock_table.all.return_value = [] + + book_review.get_all_records(count=5, sort="ASC") + + mock_table.all.assert_called_once_with(max_records=5, sort=["Rating"]) + + +class TestGetRecordId: + @patch("book_review.table") + def test_returns_record_id(self, mock_table): + import book_review + + mock_table.first.return_value = {"id": "rec123", "fields": {"Book": "Test"}} + + result = book_review.get_record_id("Test") + + mock_table.first.assert_called_once_with(formula="Book='Test'") + assert result == "rec123" + + +class TestUpdateRecord: + @patch("book_review.table") + def test_updates_and_returns_true(self, mock_table): + import book_review + + result = book_review.update_record("rec123", {"Rating": 9}) + + mock_table.update.assert_called_once_with("rec123", {"Rating": 9}) + assert result is True + + +class TestAddRecord: + @patch("book_review.table") + def test_adds_valid_record(self, mock_table): + import book_review + + data = {"Book": "New Book", "Rating": 7} + result = book_review.add_record(data) + + mock_table.create.assert_called_once_with(data) + assert result is True + + @patch("book_review.table") + def test_rejects_missing_book(self, mock_table): + import book_review + + result = book_review.add_record({"Rating": 7}) + + mock_table.create.assert_not_called() + assert result is False + + @patch("book_review.table") + def test_rejects_missing_rating(self, mock_table): + import book_review + + result = book_review.add_record({"Book": "Some Book"}) + + mock_table.create.assert_not_called() + assert result is False + + @patch("book_review.table") + def test_rejects_empty_dict(self, mock_table): + import book_review + + result = book_review.add_record({}) + + mock_table.create.assert_not_called() + assert result is False