From 18afa2e7ea856eba0627e644510612f415965cbe Mon Sep 17 00:00:00 2001 From: codesungrape Date: Mon, 11 Aug 2025 14:15:25 +0100 Subject: [PATCH 01/19] Move test files for scripts to tests/scripts/ --- tests/{ => scripts}/test_create_books.py | 0 tests/{ => scripts}/test_delete_books.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/{ => scripts}/test_create_books.py (100%) rename tests/{ => scripts}/test_delete_books.py (100%) diff --git a/tests/test_create_books.py b/tests/scripts/test_create_books.py similarity index 100% rename from tests/test_create_books.py rename to tests/scripts/test_create_books.py diff --git a/tests/test_delete_books.py b/tests/scripts/test_delete_books.py similarity index 100% rename from tests/test_delete_books.py rename to tests/scripts/test_delete_books.py From 10d2553a2b4e003fe4f60f6f935b3d2995503bf6 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Mon, 11 Aug 2025 14:59:49 +0100 Subject: [PATCH 02/19] Refactor test_create_books.py to use `with` blocks for mocks Using context managers for mocks fixes the mock leak that caused KeyError failures in test_app.py and test_integration.py. Confirmed by successful run of test_script_entry_point_calls_main. --- tests/scripts/test_create_books.py | 178 +++++++++++++++-------------- 1 file changed, 91 insertions(+), 87 deletions(-) diff --git a/tests/scripts/test_create_books.py b/tests/scripts/test_create_books.py index 5238590..5fb9d77 100644 --- a/tests/scripts/test_create_books.py +++ b/tests/scripts/test_create_books.py @@ -166,68 +166,66 @@ def test_main_creates_app_context_and_calls_run_population( assert captured.out == expected_output -@patch("app.datastore.mongo_helper.upsert_book_from_file") -@patch("utils.db_helpers.load_books_json") # PATCHING AT THE SOURCE -@patch("app.datastore.mongo_db.get_book_collection") # PATCHING AT THE SOURCE -def test_script_entry_point_calls_main( - mock_get_collection, mock_load_json, mock_upsert_book, sample_book_data -): +def test_script_entry_point_calls_main(sample_book_data, test_app): # Arrange test_book sample and return values of MOCKS test_books = sample_book_data - mock_load_json.return_value = test_books - # Mock mongodb collection object - mock_collection_obj = MagicMock() - mock_get_collection.return_value = mock_collection_obj - # Act: Run the script's main entry point. - run_create_books_script_cleanup() + with test_app.app_context(): + with patch("app.datastore.mongo_db.get_book_collection") as mock_get_collection, \ + patch("utils.db_helpers.load_books_json") as mock_load_json, \ + patch("app.datastore.mongo_helper.upsert_book_from_file") as mock_upsert_book: - # Assert: Verify mocked dependencies were called correctly. - mock_get_collection.assert_called_once() - mock_load_json.assert_called_once() + mock_load_json.return_value = test_books + # Mock mongodb collection object + mock_collection_obj = MagicMock() + mock_get_collection.return_value = mock_collection_obj + + # Act: Run the script's main entry point. + run_create_books_script_cleanup() - assert mock_upsert_book.call_count == len(test_books) - mock_upsert_book.assert_any_call(test_books[0], mock_collection_obj) - mock_upsert_book.assert_any_call(test_books[1], mock_collection_obj) + # Assert: Verify mocked dependencies were called correctly. + mock_get_collection.assert_called_once() + mock_load_json.assert_called_once() + assert mock_upsert_book.call_count == len(test_books) + mock_upsert_book.assert_any_call(test_books[0], mock_collection_obj) + mock_upsert_book.assert_any_call(test_books[1], mock_collection_obj) def test_run_population_should_insert_new_book_when_id_does_not_exist( - monkeypatch, mock_books_collection, sample_book_data + mock_books_collection, sample_book_data, test_app ): # Arrange - mock_db_collection_func = MagicMock(return_value=mock_books_collection) - monkeypatch.setattr( - "scripts.create_books.get_book_collection", mock_db_collection_func - ) + assert mock_books_collection.count_documents({}) == 0 - mock_load_data = MagicMock(return_value=sample_book_data) - monkeypatch.setattr("scripts.create_books.load_books_json", mock_load_data) + with test_app.app_context(): + # Use 'with patch' to safely contain the mocks + with patch("scripts.create_books.get_book_collection") as mock_get_collection, \ + patch("scripts.create_books.load_books_json") as mock_load_data: - assert mock_books_collection.count_documents({}) == 0 + # Configure mocks inside the 'with' block + mock_get_collection.return_value = mock_books_collection + mock_load_data.return_value = sample_book_data - # Act - # Call the function with the new list_of_books and the MONGODB COLLECTION instance - result_message = run_population() + # Act + result_message = run_population() - # Assert - mock_db_collection_func.assert_called_once() - mock_load_data.assert_called_once() + # Assert + mock_get_collection.assert_called_once() + mock_load_data.assert_called_once() - # Check for specific book to be sure the data is right - book_a_from_db = mock_books_collection.find_one( - {"id": "550e8400-e29b-41d4-a716-446655440000"} - ) - assert book_a_from_db is not None - assert book_a_from_db["title"] == "To Kill a Mockingbird" + # Check for specific book to be sure the data is right + book_a_from_db = mock_books_collection.find_one( + {"id": "550e8400-e29b-41d4-a716-446655440000"} + ) + assert book_a_from_db is not None + assert book_a_from_db["title"] == "To Kill a Mockingbird" - # Verify that the function returned the correct status message - assert result_message == "Inserted 2 books" + # Verify that the function returned the correct status message + assert result_message == "Inserted 2 books" -def test_run_population_correctly_upserts_a_batch_of_books( - mock_books_collection, monkeypatch -): +def test_run_population_correctly_upserts_a_batch_of_books(mock_books_collection, test_app): """ BEHAVIORAL TEST: Verifies that run_population correctly handles a mix of new and existing books, resulting in a fully updated collection. @@ -281,44 +279,45 @@ def test_run_population_correctly_upserts_a_batch_of_books( }, ] - # Monkeypatch the helper functions to isolate test - # - make get_book_collection return our mockDB - # - make load_books_json return our hard-coded new data - monkeypatch.setattr( - "scripts.create_books.get_book_collection", lambda: mock_books_collection - ) - monkeypatch.setattr( - "scripts.create_books.load_books_json", lambda: new_book_data_from_file - ) - # Sanity check: confim the database starts with exactly one document assert mock_books_collection.count_documents({}) == 1 - # Act - run_population() + with test_app.app_context(): + # Replace monkeypatch with the robust 'with patch' context manager + with patch("scripts.create_books.get_book_collection") as mock_get_collection, \ + patch("scripts.create_books.load_books_json") as mock_load_json: - # Assert - # Check final state of database, total count == 2 - assert ( - mock_books_collection.count_documents({}) == 2 - ), "The total document count should be 2" + # --- ARRANGE (Mock Setup) --- + # Configure mocks inside the 'with' block + mock_get_collection.return_value = mock_books_collection + mock_load_json.return_value = new_book_data_from_file + + # Act + run_population() + + # Assert + mock_get_collection.assert_called_once() + mock_load_json.assert_called_once() + assert ( + mock_books_collection.count_documents({}) == 2 + ), "The total document count should be 2" - # Retrieve the book we expected to be replaced and verify its contents - updated_book = mock_books_collection.find_one({"id": common_id}) + # Retrieve the book we expected to be replaced and verify its contents + updated_book = mock_books_collection.find_one({"id": common_id}) - assert updated_book is not None, "The updated book was not found in the database" - assert updated_book["title"] == "The Age of Surveillance Capitalism" - assert updated_book["author"] == "Shoshana Zuboff" - assert "version" not in updated_book + assert updated_book is not None, "The updated book was not found in the database" + assert updated_book["title"] == "The Age of Surveillance Capitalism" + assert updated_book["author"] == "Shoshana Zuboff" + assert "version" not in updated_book - # Retrieve the book we expected to be INSERTED and verify it exists. - inserted_book = mock_books_collection.find_one({"id": new_book_id}) - assert inserted_book is not None - assert inserted_book["title"] == "Brave New World" + # Retrieve the book we expected to be INSERTED and verify it exists. + inserted_book = mock_books_collection.find_one({"id": new_book_id}) + assert inserted_book is not None + assert inserted_book["title"] == "Brave New World" def test_upsert_book_to_mongo_replaces_document_when_id_exists( - mock_books_collection, monkeypatch + mock_books_collection, test_app ): # --- ARRANGE --- common_id = "550e8400-e29b-41d4-a716-446655440000" @@ -340,7 +339,7 @@ def test_upsert_book_to_mongo_replaces_document_when_id_exists( mock_books_collection.insert_one(old_book_version) # Define new version of book - new_book = [ + new_book_data = [ { "id": common_id, "title": "The Age of Surveillance Capitalism", @@ -355,24 +354,29 @@ def test_upsert_book_to_mongo_replaces_document_when_id_exists( } ] - # Monkeypatch the helper functions to isolate test - monkeypatch.setattr( - "scripts.create_books.get_book_collection", lambda: mock_books_collection - ) - monkeypatch.setattr("scripts.create_books.load_books_json", lambda: new_book) - # Sanity check: confim the database starts with exactly one document assert mock_books_collection.count_documents({}) == 1 - # Act - run_population() + with test_app.app_context(): + with patch("scripts.create_books.get_book_collection") as mock_get_collection, \ + patch("scripts.create_books.load_books_json") as mock_load_json: - # ASSERT - assert mock_books_collection.count_documents({}) == 1 + # Arrange + # Configure the mocks inside the 'with' block using .return_value + mock_get_collection.return_value = mock_books_collection + mock_load_json.return_value = new_book_data + + # Act + run_population() + + # ASSERT + mock_get_collection.assert_called_once() + mock_load_json.assert_called_once() + assert mock_books_collection.count_documents({}) == 1 - # Fetch the document and verify its contents are new - updated_book = mock_books_collection.find_one({"id": common_id}) + # Fetch the document and verify its contents are new + updated_book = mock_books_collection.find_one({"id": common_id}) - assert updated_book is not None, "The updated book was not found in the database" - assert updated_book["title"] == "The Age of Surveillance Capitalism" - assert updated_book["author"] == "Shoshana Zuboff" + assert updated_book is not None, "The updated book was not found in the database" + assert updated_book["title"] == "The Age of Surveillance Capitalism" + assert updated_book["author"] == "Shoshana Zuboff" From 1b15b3f198c96f531fdefde42c113ca073c12d47 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Mon, 11 Aug 2025 17:36:30 +0100 Subject: [PATCH 03/19] Remove runpy test and helper to fix mock leak --- tests/scripts/test_create_books.py | 86 +++++++++--------------------- 1 file changed, 25 insertions(+), 61 deletions(-) diff --git a/tests/scripts/test_create_books.py b/tests/scripts/test_create_books.py index 5fb9d77..be860fb 100644 --- a/tests/scripts/test_create_books.py +++ b/tests/scripts/test_create_books.py @@ -1,23 +1,10 @@ # pylint: disable=missing-docstring,line-too-long, too-many-arguments, too-many-positional-arguments -import runpy -import sys + from unittest.mock import MagicMock, patch from scripts.create_books import main, populate_books, run_population -# --------------------- Helper functions ------------------------------ -def run_create_books_script_cleanup(): - """ - Safely re-runs the 'scripts.create_books' module as a script. - - Removes 'scripts.create_books' from sys.modules to avoid re-import conflicts, - then executes it using runpy as if run from the command line (__main__ context). - """ - sys.modules.pop("scripts.create_books", None) - runpy.run_module("scripts.create_books", run_name="__main__") - - # ------------------------- Test Suite ------------------------------- @@ -137,60 +124,37 @@ def test_run_population_handles_no_inserted_list_books( ) -@patch("scripts.create_books.create_app") -@patch("scripts.create_books.run_population") -def test_main_creates_app_context_and_calls_run_population( - mock_run_population, mock_create_app, capsys -): - - # Arrange - # Mock Flask app object that has a working app_context manager - mock_app = MagicMock() - mock_create_app.return_value = mock_app - mock_run_population.return_value = "Success from mock" - expected_output = "Success from mock\n" - - # Act - main() - captured = capsys.readouterr() - - # Assert - # 1. Did we set up the environment correctly? - mock_create_app.assert_called_once() - mock_app.app_context.return_value.__enter__.assert_called_once() - - # 2. Did we call the core logic? - mock_run_population.assert_called_once() - - # 3. Did we print the result? - assert captured.out == expected_output - +def test_main_orchestrates_and_outputs(capsys): + """ + Verifies that main() correctly: + 1. Creates the Flask app. + 2. Enters the app context. + 3. Calls run_population(). + 4. Prints the result returned by run_population(). + """ -def test_script_entry_point_calls_main(sample_book_data, test_app): - # Arrange test_book sample and return values of MOCKS - test_books = sample_book_data + with patch("scripts.create_books.create_app") as mock_create_app, \ + patch("scripts.create_books.run_population") as mock_run_population: + # Arrange: mock the app and its context manager + mock_app = MagicMock() + mock_create_app.return_value = mock_app + mock_run_population.return_value = "Success from mock" - with test_app.app_context(): - with patch("app.datastore.mongo_db.get_book_collection") as mock_get_collection, \ - patch("utils.db_helpers.load_books_json") as mock_load_json, \ - patch("app.datastore.mongo_helper.upsert_book_from_file") as mock_upsert_book: + expected_output = "Success from mock\n" - mock_load_json.return_value = test_books - # Mock mongodb collection object - mock_collection_obj = MagicMock() - mock_get_collection.return_value = mock_collection_obj + # Act + main() + captured = capsys.readouterr() - # Act: Run the script's main entry point. - run_create_books_script_cleanup() + # Assert orchestration + mock_create_app.assert_called_once() + mock_app.app_context.return_value.__enter__.assert_called_once() + mock_run_population.assert_called_once() - # Assert: Verify mocked dependencies were called correctly. - mock_get_collection.assert_called_once() - mock_load_json.assert_called_once() + # Assert output + assert captured.out == expected_output - assert mock_upsert_book.call_count == len(test_books) - mock_upsert_book.assert_any_call(test_books[0], mock_collection_obj) - mock_upsert_book.assert_any_call(test_books[1], mock_collection_obj) def test_run_population_should_insert_new_book_when_id_does_not_exist( mock_books_collection, sample_book_data, test_app From 20530601fc871fd939363372a6897f2e4bb93aeb Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 12 Aug 2025 09:58:25 +0100 Subject: [PATCH 04/19] Add TDD failing test for seed_users function --- tests/scripts/test_seed_users.py | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/scripts/test_seed_users.py diff --git a/tests/scripts/test_seed_users.py b/tests/scripts/test_seed_users.py new file mode 100644 index 0000000..8f9a6e5 --- /dev/null +++ b/tests/scripts/test_seed_users.py @@ -0,0 +1,40 @@ +""" Test file for seeing database with user data""" +import pytest +import bcrypt +from app.extensions import mongo + + +def test_seed_users_successfully(test_app): + """ + GIVEN an empty database and a list of user data + WHEN the seed_users fucntion is called + THEN the users should be created in the database with hashed passwords. + """ + # Arrrange + # define the user data we want to seed the database with + sample_users = [ + {"email": "test.admin@example.com", "password": "AdminPassword123"}, + {"email": "test.user@example.com", "password": "UserPassword456"}, + ] + + # Ensure the database is clean beofre the test + with test_app.app_context(): + mongo.db.users.delete_many({}) + + # Act: call the function we are testing + result_message = seed_users(sample_users) + + # Assert + assert "Successfully seeded 2 users" in result_message + # Check the database state directly + with test_app.app_context(): + assert mongo.db.user.count_documents({}) == 2 + admin_user = mongo.db.users.find_one({"email": "test.admin@example.com"}) + assert admin_user is not None + + # Verify the password was hashed + assert admin_user["password_hash"] != "AdminPassword123" + assert bcrypt.checkpw( + b"AdminPassword123", + admin_user["password_hash"].encode("utf-8") + ) From b0b7fc08fb1706f782efc3606a2c3c4cca383326 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 12 Aug 2025 10:00:25 +0100 Subject: [PATCH 05/19] Add scripts/test_data/sample_user_data.json + empty scripts/seed_user.py --- scripts/seed_users.py | 0 scripts/test_data/sample_user_data.json | 5 +++++ 2 files changed, 5 insertions(+) create mode 100644 scripts/seed_users.py create mode 100644 scripts/test_data/sample_user_data.json diff --git a/scripts/seed_users.py b/scripts/seed_users.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/test_data/sample_user_data.json b/scripts/test_data/sample_user_data.json new file mode 100644 index 0000000..a32e40f --- /dev/null +++ b/scripts/test_data/sample_user_data.json @@ -0,0 +1,5 @@ +[ + {"email": "test.admin@example.com", "password": "AdminPassword123"}, + {"email": "test.user@example.com", "password": "UserPassword456"}, + {"email": "miss.s.rai@outlook.com", "password": "DAYSend1991"} +] \ No newline at end of file From 657441fabb6541e72ba6264b54f803dc29253488 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 12 Aug 2025 12:56:04 +0100 Subject: [PATCH 06/19] Fix context manager and typo in test --- tests/scripts/test_seed_users.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/scripts/test_seed_users.py b/tests/scripts/test_seed_users.py index 8f9a6e5..50aa272 100644 --- a/tests/scripts/test_seed_users.py +++ b/tests/scripts/test_seed_users.py @@ -1,13 +1,14 @@ """ Test file for seeing database with user data""" -import pytest + import bcrypt from app.extensions import mongo +from scripts.seed_users import seed_users def test_seed_users_successfully(test_app): """ GIVEN an empty database and a list of user data - WHEN the seed_users fucntion is called + WHEN the seed_users function is called THEN the users should be created in the database with hashed passwords. """ # Arrrange @@ -17,18 +18,16 @@ def test_seed_users_successfully(test_app): {"email": "test.user@example.com", "password": "UserPassword456"}, ] + # Enter application context and # Ensure the database is clean beofre the test with test_app.app_context(): mongo.db.users.delete_many({}) - # Act: call the function we are testing - result_message = seed_users(sample_users) + # Act: call the function we are testing + result_message = seed_users(sample_users) - # Assert - assert "Successfully seeded 2 users" in result_message - # Check the database state directly - with test_app.app_context(): - assert mongo.db.user.count_documents({}) == 2 + # Check the database state directly + assert mongo.db.users.count_documents({}) == 2 admin_user = mongo.db.users.find_one({"email": "test.admin@example.com"}) assert admin_user is not None @@ -38,3 +37,4 @@ def test_seed_users_successfully(test_app): b"AdminPassword123", admin_user["password_hash"].encode("utf-8") ) + assert "Successfully seeded 2 users" in result_message From 04f06e30053b7e88d019d2e8001459ae5333385c Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 12 Aug 2025 13:50:17 +0100 Subject: [PATCH 07/19] Add test to check skipping if exiting user functionality --- tests/scripts/test_seed_users.py | 38 ++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/tests/scripts/test_seed_users.py b/tests/scripts/test_seed_users.py index 50aa272..512ba02 100644 --- a/tests/scripts/test_seed_users.py +++ b/tests/scripts/test_seed_users.py @@ -4,14 +4,13 @@ from app.extensions import mongo from scripts.seed_users import seed_users - def test_seed_users_successfully(test_app): """ GIVEN an empty database and a list of user data WHEN the seed_users function is called THEN the users should be created in the database with hashed passwords. """ - # Arrrange + # Arrange # define the user data we want to seed the database with sample_users = [ {"email": "test.admin@example.com", "password": "AdminPassword123"}, @@ -38,3 +37,38 @@ def test_seed_users_successfully(test_app): admin_user["password_hash"].encode("utf-8") ) assert "Successfully seeded 2 users" in result_message + + +def test_seed_users_skips_if_user_already_exists(test_app, capsys): + """ + GIVEN a database that already contains one user + WHEN the seed_users function is called with a list containing that existing user and a new one + THEN it should skip the existing user, insert the new one, and print a skip message. + """ + # Arrange + users_to_attempt_seeding = [ + {"email": "existing.user@example.com", "password": "Password123"}, + {"email": "new.user@example.com", "password": "Password456"}, + ] + + with test_app.app_context(): + # start with a clean state + mongo.db.users.delete_many({}) + + mongo.db.users.insert_one({ + "email": "existing.user@example.com", + "password_hash": "some-pre-existing-hash" + }) + # ACT + result_message = seed_users(users_to_attempt_seeding) + + # Assert + final_count = mongo.db.users.count_documents({}) + assert final_count == 2 + # Check the return message from the function + assert "Successfully seeded 1 users" in result_message + + #Check the print + captured = capsys.readouterr() + assert "Skipping existing user: existing.user@example.com" in captured.out + assert "Created user: new.user@example.com" in captured.out From 4460f2a60dd80e9723e1d4a417b645b37dd164df Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 12 Aug 2025 14:21:14 +0100 Subject: [PATCH 08/19] Add logic seed_users; TDD tests pass --- scripts/seed_users.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/scripts/seed_users.py b/scripts/seed_users.py index e69de29..8d6c333 100644 --- a/scripts/seed_users.py +++ b/scripts/seed_users.py @@ -0,0 +1,41 @@ +""" Seeding user_data script""" + +import bcrypt +from app.extensions import mongo + +def seed_users(users_to_seed: list) -> str: + """ + Processes a list of user data, hashes their passwords, + and inserts them into the database. Skips existing users. + + Args: + users_to_seed: A list of dicts, each with 'email' and 'password'. + + Returns: + A string summarizing the result. + """ + count = 0 + + for user_data in users_to_seed: + email = user_data["email"] + + # Check if data already exists + if mongo.db.users.find_one({"email": email}): + print(f"Skipping existing user: {email}") + continue + + # hash the password + hashed_password = bcrypt.hashpw( + user_data["password"].encode("utf-8"), + bcrypt.gensalt() + ) + + # insert to new user + mongo.db.users.insert_one({ + "email": email, + "password_hash": hashed_password.decode("utf-8") + }) + count += 1 + print(f"Created user: {email}") + + return f"Successfully seeded {count} users" From 911289dca8951eee5b0a0cb263076c6451a66a61 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 12 Aug 2025 16:38:54 +0100 Subject: [PATCH 09/19] Refactor: move script execution to main(); fix pylint --- scripts/seed_users.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/scripts/seed_users.py b/scripts/seed_users.py index 8d6c333..f269ce2 100644 --- a/scripts/seed_users.py +++ b/scripts/seed_users.py @@ -1,12 +1,16 @@ """ Seeding user_data script""" +import json import bcrypt +from app import create_app from app.extensions import mongo def seed_users(users_to_seed: list) -> str: """ Processes a list of user data, hashes their passwords, and inserts them into the database. Skips existing users. + + This function MUST be run within an active Flask application context. Args: users_to_seed: A list of dicts, each with 'email' and 'password'. @@ -39,3 +43,34 @@ def seed_users(users_to_seed: list) -> str: print(f"Created user: {email}") return f"Successfully seeded {count} users" + + +def main(): + """ + Main execution fucntion to run the seeding process. + handles app context, data loading, and calls the core seeding logic. + """ + # Create the DEVELOPMENT app when run from the command line + app = create_app() + with app.app_context(): + + user_data_file = "scripts/sample_user_data.json" + + try: + # You can define your default users here or import from another file + with open(user_data_file, "r", encoding="utf-8") as user_file: + default_users = json.load(user_file) + + print("--- Starting user seeding ---") + + message = seed_users(default_users) + print(f"--- {message} ---") + print("--- Seeding complete ---") + + except FileNotFoundError: + print(f"Error: Data file not found at '{user_data_file}'.") + except json.JSONDecodeError: + print(f"Error: Could not decode JSON from '{user_data_file}'. Check for syntax errors.") + +if __name__ == "__main__": + main() From 56a45bba3a650da973e5a9b9d3a8ed9fa43106d5 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 12 Aug 2025 16:41:02 +0100 Subject: [PATCH 10/19] Add tests for main() happy path + failure modes; 100% converage --- tests/scripts/test_seed_users.py | 74 ++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/tests/scripts/test_seed_users.py b/tests/scripts/test_seed_users.py index 512ba02..4761afc 100644 --- a/tests/scripts/test_seed_users.py +++ b/tests/scripts/test_seed_users.py @@ -1,8 +1,11 @@ """ Test file for seeing database with user data""" +from unittest.mock import patch, mock_open +import json import bcrypt from app.extensions import mongo from scripts.seed_users import seed_users +from scripts.seed_users import main as seed_users_main def test_seed_users_successfully(test_app): """ @@ -72,3 +75,74 @@ def test_seed_users_skips_if_user_already_exists(test_app, capsys): captured = capsys.readouterr() assert "Skipping existing user: existing.user@example.com" in captured.out assert "Created user: new.user@example.com" in captured.out + + +def test_main_runs_seeding_process_successfully(capsys): + """ + GIVEN a successful file read + WHEN the main function is called + THEN it should call seed_users with the loaded data and print success messages. + """ + # Arrange + fake_json_data = '[{"email": "fake@user.com", "password": "fakepass"}]' + + # Create mock objects for all of main's dependencies + with patch('scripts.seed_users.create_app'), \ + patch("scripts.seed_users.seed_users") as mock_seed_users, \ + patch("builtins.open", mock_open(read_data=fake_json_data)): + + # Act + seed_users_main() + + # Assert + expected_data = json.loads(fake_json_data) + mock_seed_users.assert_called_once_with(expected_data) + # Did it print the right message? + captured = capsys.readouterr() + assert "--- Starting user seeding ---" in captured.out + assert "--- Seeding complete ---" in captured.out + + +def test_main_throws_filenotfounderror(capsys): + """ + GIVEN the data file does not exist + WHEN the main function is called + THEN it should print a FileNotFoundError message and not call seed_users. + """ + # Arrange + with patch("scripts.seed_users.create_app"), \ + patch("scripts.seed_users.seed_users") as mock_seed_users, \ + patch("builtins.open") as mock_file: + + mock_file.side_effect = FileNotFoundError + + # Act + seed_users_main() + + # Assert + captured = capsys.readouterr() + assert "Error: Data file not found at" in captured.out + + mock_seed_users.assert_not_called() + + +def test_main_throws_jsondecodeerror(capsys): + """ + GIVEN the data file contains invalid JSON + WHEN the main function is called + THEN it should print a JSONDecodeError message and not call seed_users. + """ + # Arrange + corrupted_json_data = '{"email": "bad@user.com", "password": "badpass"' # Missing closing brace + + with patch('scripts.seed_users.create_app'), \ + patch('scripts.seed_users.seed_users') as mock_seed_users, \ + patch('builtins.open', mock_open(read_data=corrupted_json_data)): + + # Act + seed_users_main() + + # Assert + captured = capsys.readouterr() + assert "Error: Could not decode JSON from" in captured.out + mock_seed_users.assert_not_called() From 5bc79e2dee6aba79d344ea06a8f71a421f14494a Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 12 Aug 2025 16:41:42 +0100 Subject: [PATCH 11/19] Run formatting --- scripts/seed_users.py | 22 +++++++------ tests/scripts/test_create_books.py | 39 ++++++++++++++++------- tests/scripts/test_seed_users.py | 50 +++++++++++++++++------------- 3 files changed, 69 insertions(+), 42 deletions(-) diff --git a/scripts/seed_users.py b/scripts/seed_users.py index f269ce2..41b8c99 100644 --- a/scripts/seed_users.py +++ b/scripts/seed_users.py @@ -1,17 +1,20 @@ -""" Seeding user_data script""" +"""Seeding user_data script""" import json + import bcrypt + from app import create_app from app.extensions import mongo + def seed_users(users_to_seed: list) -> str: """ Processes a list of user data, hashes their passwords, and inserts them into the database. Skips existing users. This function MUST be run within an active Flask application context. - + Args: users_to_seed: A list of dicts, each with 'email' and 'password'. @@ -30,15 +33,13 @@ def seed_users(users_to_seed: list) -> str: # hash the password hashed_password = bcrypt.hashpw( - user_data["password"].encode("utf-8"), - bcrypt.gensalt() + user_data["password"].encode("utf-8"), bcrypt.gensalt() ) # insert to new user - mongo.db.users.insert_one({ - "email": email, - "password_hash": hashed_password.decode("utf-8") - }) + mongo.db.users.insert_one( + {"email": email, "password_hash": hashed_password.decode("utf-8")} + ) count += 1 print(f"Created user: {email}") @@ -70,7 +71,10 @@ def main(): except FileNotFoundError: print(f"Error: Data file not found at '{user_data_file}'.") except json.JSONDecodeError: - print(f"Error: Could not decode JSON from '{user_data_file}'. Check for syntax errors.") + print( + f"Error: Could not decode JSON from '{user_data_file}'. Check for syntax errors." + ) + if __name__ == "__main__": main() diff --git a/tests/scripts/test_create_books.py b/tests/scripts/test_create_books.py index be860fb..14ee1d2 100644 --- a/tests/scripts/test_create_books.py +++ b/tests/scripts/test_create_books.py @@ -4,7 +4,6 @@ from scripts.create_books import main, populate_books, run_population - # ------------------------- Test Suite ------------------------------- @@ -133,8 +132,9 @@ def test_main_orchestrates_and_outputs(capsys): 4. Prints the result returned by run_population(). """ - with patch("scripts.create_books.create_app") as mock_create_app, \ - patch("scripts.create_books.run_population") as mock_run_population: + with patch("scripts.create_books.create_app") as mock_create_app, patch( + "scripts.create_books.run_population" + ) as mock_run_population: # Arrange: mock the app and its context manager mock_app = MagicMock() @@ -164,8 +164,11 @@ def test_run_population_should_insert_new_book_when_id_does_not_exist( with test_app.app_context(): # Use 'with patch' to safely contain the mocks - with patch("scripts.create_books.get_book_collection") as mock_get_collection, \ - patch("scripts.create_books.load_books_json") as mock_load_data: + with patch( + "scripts.create_books.get_book_collection" + ) as mock_get_collection, patch( + "scripts.create_books.load_books_json" + ) as mock_load_data: # Configure mocks inside the 'with' block mock_get_collection.return_value = mock_books_collection @@ -189,7 +192,9 @@ def test_run_population_should_insert_new_book_when_id_does_not_exist( assert result_message == "Inserted 2 books" -def test_run_population_correctly_upserts_a_batch_of_books(mock_books_collection, test_app): +def test_run_population_correctly_upserts_a_batch_of_books( + mock_books_collection, test_app +): """ BEHAVIORAL TEST: Verifies that run_population correctly handles a mix of new and existing books, resulting in a fully updated collection. @@ -248,8 +253,11 @@ def test_run_population_correctly_upserts_a_batch_of_books(mock_books_collection with test_app.app_context(): # Replace monkeypatch with the robust 'with patch' context manager - with patch("scripts.create_books.get_book_collection") as mock_get_collection, \ - patch("scripts.create_books.load_books_json") as mock_load_json: + with patch( + "scripts.create_books.get_book_collection" + ) as mock_get_collection, patch( + "scripts.create_books.load_books_json" + ) as mock_load_json: # --- ARRANGE (Mock Setup) --- # Configure mocks inside the 'with' block @@ -269,7 +277,9 @@ def test_run_population_correctly_upserts_a_batch_of_books(mock_books_collection # Retrieve the book we expected to be replaced and verify its contents updated_book = mock_books_collection.find_one({"id": common_id}) - assert updated_book is not None, "The updated book was not found in the database" + assert ( + updated_book is not None + ), "The updated book was not found in the database" assert updated_book["title"] == "The Age of Surveillance Capitalism" assert updated_book["author"] == "Shoshana Zuboff" assert "version" not in updated_book @@ -322,8 +332,11 @@ def test_upsert_book_to_mongo_replaces_document_when_id_exists( assert mock_books_collection.count_documents({}) == 1 with test_app.app_context(): - with patch("scripts.create_books.get_book_collection") as mock_get_collection, \ - patch("scripts.create_books.load_books_json") as mock_load_json: + with patch( + "scripts.create_books.get_book_collection" + ) as mock_get_collection, patch( + "scripts.create_books.load_books_json" + ) as mock_load_json: # Arrange # Configure the mocks inside the 'with' block using .return_value @@ -341,6 +354,8 @@ def test_upsert_book_to_mongo_replaces_document_when_id_exists( # Fetch the document and verify its contents are new updated_book = mock_books_collection.find_one({"id": common_id}) - assert updated_book is not None, "The updated book was not found in the database" + assert ( + updated_book is not None + ), "The updated book was not found in the database" assert updated_book["title"] == "The Age of Surveillance Capitalism" assert updated_book["author"] == "Shoshana Zuboff" diff --git a/tests/scripts/test_seed_users.py b/tests/scripts/test_seed_users.py index 4761afc..a26f848 100644 --- a/tests/scripts/test_seed_users.py +++ b/tests/scripts/test_seed_users.py @@ -1,11 +1,14 @@ -""" Test file for seeing database with user data""" +"""Test file for seeing database with user data""" -from unittest.mock import patch, mock_open import json +from unittest.mock import mock_open, patch + import bcrypt + from app.extensions import mongo -from scripts.seed_users import seed_users from scripts.seed_users import main as seed_users_main +from scripts.seed_users import seed_users + def test_seed_users_successfully(test_app): """ @@ -36,8 +39,7 @@ def test_seed_users_successfully(test_app): # Verify the password was hashed assert admin_user["password_hash"] != "AdminPassword123" assert bcrypt.checkpw( - b"AdminPassword123", - admin_user["password_hash"].encode("utf-8") + b"AdminPassword123", admin_user["password_hash"].encode("utf-8") ) assert "Successfully seeded 2 users" in result_message @@ -58,10 +60,12 @@ def test_seed_users_skips_if_user_already_exists(test_app, capsys): # start with a clean state mongo.db.users.delete_many({}) - mongo.db.users.insert_one({ - "email": "existing.user@example.com", - "password_hash": "some-pre-existing-hash" - }) + mongo.db.users.insert_one( + { + "email": "existing.user@example.com", + "password_hash": "some-pre-existing-hash", + } + ) # ACT result_message = seed_users(users_to_attempt_seeding) @@ -71,7 +75,7 @@ def test_seed_users_skips_if_user_already_exists(test_app, capsys): # Check the return message from the function assert "Successfully seeded 1 users" in result_message - #Check the print + # Check the print captured = capsys.readouterr() assert "Skipping existing user: existing.user@example.com" in captured.out assert "Created user: new.user@example.com" in captured.out @@ -87,9 +91,9 @@ def test_main_runs_seeding_process_successfully(capsys): fake_json_data = '[{"email": "fake@user.com", "password": "fakepass"}]' # Create mock objects for all of main's dependencies - with patch('scripts.seed_users.create_app'), \ - patch("scripts.seed_users.seed_users") as mock_seed_users, \ - patch("builtins.open", mock_open(read_data=fake_json_data)): + with patch("scripts.seed_users.create_app"), patch( + "scripts.seed_users.seed_users" + ) as mock_seed_users, patch("builtins.open", mock_open(read_data=fake_json_data)): # Act seed_users_main() @@ -110,9 +114,9 @@ def test_main_throws_filenotfounderror(capsys): THEN it should print a FileNotFoundError message and not call seed_users. """ # Arrange - with patch("scripts.seed_users.create_app"), \ - patch("scripts.seed_users.seed_users") as mock_seed_users, \ - patch("builtins.open") as mock_file: + with patch("scripts.seed_users.create_app"), patch( + "scripts.seed_users.seed_users" + ) as mock_seed_users, patch("builtins.open") as mock_file: mock_file.side_effect = FileNotFoundError @@ -133,11 +137,15 @@ def test_main_throws_jsondecodeerror(capsys): THEN it should print a JSONDecodeError message and not call seed_users. """ # Arrange - corrupted_json_data = '{"email": "bad@user.com", "password": "badpass"' # Missing closing brace - - with patch('scripts.seed_users.create_app'), \ - patch('scripts.seed_users.seed_users') as mock_seed_users, \ - patch('builtins.open', mock_open(read_data=corrupted_json_data)): + corrupted_json_data = ( + '{"email": "bad@user.com", "password": "badpass"' # Missing closing brace + ) + + with patch("scripts.seed_users.create_app"), patch( + "scripts.seed_users.seed_users" + ) as mock_seed_users, patch( + "builtins.open", mock_open(read_data=corrupted_json_data) + ): # Act seed_users_main() From 9de860a7105c1885b097c070e83ee08c1964ede4 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 12 Aug 2025 17:07:33 +0100 Subject: [PATCH 12/19] Fix to use abs path from the script location --- scripts/seed_users.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/scripts/seed_users.py b/scripts/seed_users.py index 41b8c99..180e563 100644 --- a/scripts/seed_users.py +++ b/scripts/seed_users.py @@ -1,5 +1,6 @@ """Seeding user_data script""" +import os import json import bcrypt @@ -55,11 +56,15 @@ def main(): app = create_app() with app.app_context(): - user_data_file = "scripts/sample_user_data.json" + # 1. Get the directory where THIS script (seed_users.py) lives. + script_dir = os.path.dirname(__file__) + + # 2. Build the full, absolute path to the JSON file. + user_data_path = os.path.join(script_dir, "test_data/sample_user_data.json") try: # You can define your default users here or import from another file - with open(user_data_file, "r", encoding="utf-8") as user_file: + with open(user_data_path, "r", encoding="utf-8") as user_file: default_users = json.load(user_file) print("--- Starting user seeding ---") @@ -69,10 +74,10 @@ def main(): print("--- Seeding complete ---") except FileNotFoundError: - print(f"Error: Data file not found at '{user_data_file}'.") + print(f"Error: Data file not found at '{user_data_path}'.") except json.JSONDecodeError: print( - f"Error: Could not decode JSON from '{user_data_file}'. Check for syntax errors." + f"Error: Could not decode JSON from '{user_data_path}'. Check for syntax errors." ) From c9b2cbe531db0e3b01caa6541bec5b909042e16e Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 12 Aug 2025 17:08:21 +0100 Subject: [PATCH 13/19] Update Makefile with seed-users command --- Makefile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b18984d..a1c4698 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ PIP = $(VENV_DIR)/bin/pip .DEFAULT_GOAL := help # Phony Targets -.PHONY: run clean test help lint db-seed db-clean db-setup +.PHONY: run clean test help lint db-seed db-clean db-setup seed-users # ============================================================================== # CORE COMMANDS - For everyday development @@ -86,3 +86,8 @@ db-seed: install db-clean: install @echo "--> ⚠️ Deleting all books from the database..." PATH=$(VENV_DIR)/bin:$$PATH PYTHONPATH=. $(PYTHON) -m scripts.delete_books + + +seed-users: install + @echo "--- Seeding the data with user data ---" + PATH=$(VENV_DIR)/bin:$$PATH PYTHONPATH=. $(PYTHON) -m scripts.seed_users From 540d8b0e699812c42bdcf6f08e4781ff0812f2f1 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 12 Aug 2025 17:48:17 +0100 Subject: [PATCH 14/19] Update Makefile and README.md to reflect adding seed_users() --- Makefile | 3 ++- README.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index a1c4698..5957f50 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,7 @@ help: ## Show help @echo " make db-setup Reset the database to a clean, seeded state. (Runs db-clean then db-seed)" @echo " make db-seed Populate the database with initial data." @echo " make db-clean Delete all book data from the database." + @echo " make seed-users Pupulate the database with initial user data." install: $(PIP) @@ -89,5 +90,5 @@ db-clean: install seed-users: install - @echo "--- Seeding the data with user data ---" + @echo "--- Seeding the database with user data ---" PATH=$(VENV_DIR)/bin:$$PATH PYTHONPATH=. $(PYTHON) -m scripts.seed_users diff --git a/README.md b/README.md index 0b0996a..a9f0b57 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,9 @@ To use the API, you first need to populate the database with some initial data. | Command | Description | |----------------|-----------------------------------------------------------------------------| | `make db-setup`| **(Recommended)** Resets the database. Runs `db-clean` and then `db-seed`. | -| `make db-seed` | Populates the database with the contents of `scripts/books.json`. | +| `make db-seed` | Populates the database with the contents of `scripts/test_data/books.json`. | | `make db-clean`| Deletes all documents from the 'books' collection. Useful for starting fresh. | +| `make seed-users`| *** THIS IS WIP right now: The user data is required for the JWT authentication system. *** Populates the database with initial user data for authentication from `scripts/test_databooks.json`. | To perform a full database reset, run: ```bash From 70a45d2a81a55ea2db8a69de59e3c7642f4e51ac Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 12 Aug 2025 18:00:06 +0100 Subject: [PATCH 15/19] Update tests/scripts/test_seed_users.py Apply copilot typo fix Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/scripts/test_seed_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/scripts/test_seed_users.py b/tests/scripts/test_seed_users.py index a26f848..db399ce 100644 --- a/tests/scripts/test_seed_users.py +++ b/tests/scripts/test_seed_users.py @@ -1,4 +1,4 @@ -"""Test file for seeing database with user data""" +"""Test file for seeding database with user data""" import json from unittest.mock import mock_open, patch From 78b1d4c2f1ce11f64a72390cf31e5d6c80b21aca Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 12 Aug 2025 18:00:16 +0100 Subject: [PATCH 16/19] Update tests/scripts/test_seed_users.py Apply copilot typo fix Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/scripts/test_seed_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/scripts/test_seed_users.py b/tests/scripts/test_seed_users.py index db399ce..3e36e26 100644 --- a/tests/scripts/test_seed_users.py +++ b/tests/scripts/test_seed_users.py @@ -24,7 +24,7 @@ def test_seed_users_successfully(test_app): ] # Enter application context and - # Ensure the database is clean beofre the test + # Ensure the database is clean before the test with test_app.app_context(): mongo.db.users.delete_many({}) From 69e2edcfaaa42ca505d570db7b0ffe0d8743b373 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 12 Aug 2025 18:00:24 +0100 Subject: [PATCH 17/19] Update scripts/seed_users.py Apply copilot typo fix Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/seed_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/seed_users.py b/scripts/seed_users.py index 180e563..ae6b3a1 100644 --- a/scripts/seed_users.py +++ b/scripts/seed_users.py @@ -49,7 +49,7 @@ def seed_users(users_to_seed: list) -> str: def main(): """ - Main execution fucntion to run the seeding process. + Main execution function to run the seeding process. handles app context, data loading, and calls the core seeding logic. """ # Create the DEVELOPMENT app when run from the command line From 216318447e9395565099308f7b557a9b1d7d6563 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 12 Aug 2025 18:00:31 +0100 Subject: [PATCH 18/19] Update README.md Apply copilot typo fix Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a9f0b57..c6d8fcb 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ To use the API, you first need to populate the database with some initial data. | `make db-setup`| **(Recommended)** Resets the database. Runs `db-clean` and then `db-seed`. | | `make db-seed` | Populates the database with the contents of `scripts/test_data/books.json`. | | `make db-clean`| Deletes all documents from the 'books' collection. Useful for starting fresh. | -| `make seed-users`| *** THIS IS WIP right now: The user data is required for the JWT authentication system. *** Populates the database with initial user data for authentication from `scripts/test_databooks.json`. | +| `make seed-users`| *** THIS IS WIP right now: The user data is required for the JWT authentication system. *** Populates the database with initial user data for authentication from `scripts/test_data/sample_user_data.json`. | To perform a full database reset, run: ```bash From b459c003c20b73a73b83a966d2de686a1e1cb9c0 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 12 Aug 2025 18:00:38 +0100 Subject: [PATCH 19/19] Update Makefile Apply copilot typo fix Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5957f50..beb09c1 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ help: ## Show help @echo " make db-setup Reset the database to a clean, seeded state. (Runs db-clean then db-seed)" @echo " make db-seed Populate the database with initial data." @echo " make db-clean Delete all book data from the database." - @echo " make seed-users Pupulate the database with initial user data." + @echo " make seed-users Populate the database with initial user data." install: $(PIP)