From ab098796a2655cf9e81caffe06c86a717309046e Mon Sep 17 00:00:00 2001 From: Mike Storey Date: Wed, 25 Jun 2025 14:54:21 -0400 Subject: [PATCH 1/5] updated processing return schema --- docs/openapi.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 4b96da7..2e39323 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -366,14 +366,6 @@ components: - default - file - environment - versions: - type: array - description: List of version information - items: - type: object - enumerators: - type: object - description: Dictionary of enumerations token: type: object properties: From a3a4630725a0141d5c10603a8b004121a437fadf Mon Sep 17 00:00:00 2001 From: Mike Storey Date: Wed, 25 Jun 2025 14:55:03 -0400 Subject: [PATCH 2/5] refactored get_collections to return array not object --- stage0_mongodb_api/services/collection_service.py | 13 ++++++++----- tests/routes/test_collection_routes.py | 8 ++++---- tests/services/test_collection_services.py | 7 ++++--- tests/stepci/small_sample.yaml | 15 +++++++++++---- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/stage0_mongodb_api/services/collection_service.py b/stage0_mongodb_api/services/collection_service.py index db8a942..0ad7637 100644 --- a/stage0_mongodb_api/services/collection_service.py +++ b/stage0_mongodb_api/services/collection_service.py @@ -26,14 +26,14 @@ class CollectionService: """ @staticmethod - def list_collections(token: Dict = None) -> Dict: + def list_collections(token: Dict = None) -> List[Dict]: """List all configured collections. Args: token: Authentication token for RBAC enforcement Returns: - Dictionary of collection name: version string + List of dictionaries with collection_name and version Raises: CollectionProcessingError: If there are load or validation errors @@ -49,10 +49,13 @@ def list_collections(token: Dict = None) -> Dict: if validation_errors: raise CollectionProcessingError("collections", validation_errors) - # Create a dict of collection name: version string - collections = {} + # Create a list of collection objects matching the OpenAPI schema + collections = [] for collection_name, collection in config_manager.collection_configs.items(): - collections[collection_name] = VersionManager.get_current_version(collection_name) + collections.append({ + "collection_name": collection_name, + "version": VersionManager.get_current_version(collection_name) + }) return collections @staticmethod diff --git a/tests/routes/test_collection_routes.py b/tests/routes/test_collection_routes.py index 50cf277..960c29f 100644 --- a/tests/routes/test_collection_routes.py +++ b/tests/routes/test_collection_routes.py @@ -15,10 +15,10 @@ def setUp(self): def test_list_collections_success(self, mock_collection_service): """Test listing all collections successfully""" # Arrange - mock_collections = { - "users": "1.0.0.1", - "organizations": "1.0.0.1" - } + mock_collections = [ + {"collection_name": "users", "version": "1.0.0.1"}, + {"collection_name": "organizations", "version": "1.0.0.1"} + ] mock_collection_service.list_collections.return_value = mock_collections # Act diff --git a/tests/services/test_collection_services.py b/tests/services/test_collection_services.py index 7fa0dad..230a71b 100644 --- a/tests/services/test_collection_services.py +++ b/tests/services/test_collection_services.py @@ -18,11 +18,12 @@ def test_list_collections_success(self, mock_config_manager): mock_config_manager.return_value.load_errors = None mock_config_manager.return_value.validate_configs.return_value = [] with patch('stage0_mongodb_api.services.collection_service.VersionManager') as mock_version_manager: - mock_version_manager.get_version_string.return_value = "simple.1.0.0.1" + mock_version_manager.get_current_version.return_value = "simple.1.0.0.1" result = CollectionService.list_collections() self.assertEqual(len(result), 1) - self.assertIsInstance(result, dict) - self.assertIn("simple", result) + self.assertIsInstance(result, list) + self.assertEqual(result[0]["collection_name"], "simple") + self.assertEqual(result[0]["version"], "simple.1.0.0.1") @patch('stage0_mongodb_api.services.collection_service.ConfigManager') def test_list_collections_load_error(self, mock_config_manager): diff --git a/tests/stepci/small_sample.yaml b/tests/stepci/small_sample.yaml index 4513204..fa939c8 100644 --- a/tests/stepci/small_sample.yaml +++ b/tests/stepci/small_sample.yaml @@ -14,10 +14,17 @@ tests: check: status: /200/ schema: - type: "object" - properties: - simple: - type: "string" + type: "array" + items: + type: "object" + properties: + collection_name: + type: "string" + version: + type: "string" + required: + - collection_name + - version - name: GET A Collection http: url: http://${{env.host}}/api/collections/simple From 5747486663b4c684fb6cd8ceb3d07c0ea827c46e Mon Sep 17 00:00:00 2001 From: Mike Storey Date: Wed, 25 Jun 2025 15:22:30 -0400 Subject: [PATCH 3/5] consistently implemented endpoints with out trailing / --- stage0_mongodb_api/routes/collection_routes.py | 8 ++++---- stage0_mongodb_api/server.py | 3 +++ tests/routes/test_collection_routes.py | 10 +++++----- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/stage0_mongodb_api/routes/collection_routes.py b/stage0_mongodb_api/routes/collection_routes.py index 9aeb723..682e853 100644 --- a/stage0_mongodb_api/routes/collection_routes.py +++ b/stage0_mongodb_api/routes/collection_routes.py @@ -8,7 +8,7 @@ def create_collection_routes(): blueprint = Blueprint('collections', __name__) - @blueprint.route('/', methods=['GET']) + @blueprint.route('', methods=['GET']) def list_collections(): """List all configured collections""" token = create_flask_token() @@ -29,7 +29,7 @@ def list_collections(): "message": str(e) }]), 500 - @blueprint.route('/', methods=['POST']) + @blueprint.route('', methods=['POST']) def process_collections(): """Process all configured collections""" token = create_flask_token() @@ -50,7 +50,7 @@ def process_collections(): "message": str(e) }]), 500 - @blueprint.route('/', methods=['GET']) + @blueprint.route('', methods=['GET']) def get_collection(collection_name): """Get a specific collection configuration""" token = create_flask_token() @@ -74,7 +74,7 @@ def get_collection(collection_name): "message": str(e) }]), 500 - @blueprint.route('/', methods=['POST']) + @blueprint.route('', methods=['POST']) def process_collection(collection_name): """Process a specific collection""" token = create_flask_token() diff --git a/stage0_mongodb_api/server.py b/stage0_mongodb_api/server.py index 964404a..0b3bb52 100644 --- a/stage0_mongodb_api/server.py +++ b/stage0_mongodb_api/server.py @@ -34,6 +34,9 @@ def handle_exit(signum, frame): app = Flask(__name__) app.json = MongoJSONEncoder(app) +# Configure Flask to be strict about trailing slashes +app.url_map.strict_slashes = False + # Auto-processing logic - runs when module is imported (including by Gunicorn) if config.AUTO_PROCESS: logger.info(f"============= Auto Processing is Enabled ===============") diff --git a/tests/routes/test_collection_routes.py b/tests/routes/test_collection_routes.py index 960c29f..98052e2 100644 --- a/tests/routes/test_collection_routes.py +++ b/tests/routes/test_collection_routes.py @@ -22,7 +22,7 @@ def test_list_collections_success(self, mock_collection_service): mock_collection_service.list_collections.return_value = mock_collections # Act - response = self.client.get('/api/collections/') + response = self.client.get('/api/collections') # Assert self.assertEqual(response.status_code, 200) @@ -37,7 +37,7 @@ def test_list_collections_processing_error(self, mock_collection_service): mock_collection_service.list_collections.side_effect = CollectionProcessingError("collections", errors) # Act - response = self.client.get('/api/collections/') + response = self.client.get('/api/collections') # Assert self.assertEqual(response.status_code, 500) @@ -50,7 +50,7 @@ def test_list_collections_unexpected_error(self, mock_collection_service): mock_collection_service.list_collections.side_effect = Exception("Unexpected error") # Act - response = self.client.get('/api/collections/') + response = self.client.get('/api/collections') # Assert self.assertEqual(response.status_code, 500) @@ -68,7 +68,7 @@ def test_process_collections_success(self, mock_collection_service): mock_collection_service.process_collections.return_value = mock_results # Act - response = self.client.post('/api/collections/') + response = self.client.post('/api/collections') # Assert self.assertEqual(response.status_code, 200) @@ -83,7 +83,7 @@ def test_process_collections_processing_error(self, mock_collection_service): mock_collection_service.process_collections.side_effect = CollectionProcessingError("collections", errors) # Act - response = self.client.post('/api/collections/') + response = self.client.post('/api/collections') # Assert self.assertEqual(response.status_code, 500) From c11890635ed3ec23f32fc6a2836ed81edaa4033e Mon Sep 17 00:00:00 2001 From: Mike Storey Date: Wed, 25 Jun 2025 16:15:17 -0400 Subject: [PATCH 4/5] feat: standardize API routes with trailing slashes for Vite compatibility - Update Flask routes to use trailing slashes consistently - Change collection routes from '' to '/' and '' to '/' - Update unit tests to match new route patterns - Mock MongoIO and CollectionService in test_server.py for reliable testing - All 114 tests now pass with 93% code coverage - Resolves CORS issues caused by Vite proxy redirecting to trailing slashes --- .../routes/collection_routes.py | 8 +++--- tests/routes/test_collection_routes.py | 28 +++++++++---------- tests/test_server.py | 14 ++++++---- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/stage0_mongodb_api/routes/collection_routes.py b/stage0_mongodb_api/routes/collection_routes.py index 682e853..79e427e 100644 --- a/stage0_mongodb_api/routes/collection_routes.py +++ b/stage0_mongodb_api/routes/collection_routes.py @@ -8,7 +8,7 @@ def create_collection_routes(): blueprint = Blueprint('collections', __name__) - @blueprint.route('', methods=['GET']) + @blueprint.route('/', methods=['GET']) def list_collections(): """List all configured collections""" token = create_flask_token() @@ -29,7 +29,7 @@ def list_collections(): "message": str(e) }]), 500 - @blueprint.route('', methods=['POST']) + @blueprint.route('/', methods=['POST']) def process_collections(): """Process all configured collections""" token = create_flask_token() @@ -50,7 +50,7 @@ def process_collections(): "message": str(e) }]), 500 - @blueprint.route('', methods=['GET']) + @blueprint.route('//', methods=['GET']) def get_collection(collection_name): """Get a specific collection configuration""" token = create_flask_token() @@ -74,7 +74,7 @@ def get_collection(collection_name): "message": str(e) }]), 500 - @blueprint.route('', methods=['POST']) + @blueprint.route('//', methods=['POST']) def process_collection(collection_name): """Process a specific collection""" token = create_flask_token() diff --git a/tests/routes/test_collection_routes.py b/tests/routes/test_collection_routes.py index 98052e2..a03a1eb 100644 --- a/tests/routes/test_collection_routes.py +++ b/tests/routes/test_collection_routes.py @@ -22,7 +22,7 @@ def test_list_collections_success(self, mock_collection_service): mock_collection_service.list_collections.return_value = mock_collections # Act - response = self.client.get('/api/collections') + response = self.client.get('/api/collections/') # Assert self.assertEqual(response.status_code, 200) @@ -37,7 +37,7 @@ def test_list_collections_processing_error(self, mock_collection_service): mock_collection_service.list_collections.side_effect = CollectionProcessingError("collections", errors) # Act - response = self.client.get('/api/collections') + response = self.client.get('/api/collections/') # Assert self.assertEqual(response.status_code, 500) @@ -50,7 +50,7 @@ def test_list_collections_unexpected_error(self, mock_collection_service): mock_collection_service.list_collections.side_effect = Exception("Unexpected error") # Act - response = self.client.get('/api/collections') + response = self.client.get('/api/collections/') # Assert self.assertEqual(response.status_code, 500) @@ -68,7 +68,7 @@ def test_process_collections_success(self, mock_collection_service): mock_collection_service.process_collections.return_value = mock_results # Act - response = self.client.post('/api/collections') + response = self.client.post('/api/collections/') # Assert self.assertEqual(response.status_code, 200) @@ -83,7 +83,7 @@ def test_process_collections_processing_error(self, mock_collection_service): mock_collection_service.process_collections.side_effect = CollectionProcessingError("collections", errors) # Act - response = self.client.post('/api/collections') + response = self.client.post('/api/collections/') # Assert self.assertEqual(response.status_code, 500) @@ -98,12 +98,12 @@ def test_get_collection_success(self, mock_collection_service): mock_collection_service.get_collection.return_value = mock_collection # Act - response = self.client.get(f'/api/collections/users') + response = self.client.get(f'/api/collections/{collection_name}/') # Assert self.assertEqual(response.status_code, 200) self.assertEqual(response.json, mock_collection) - mock_collection_service.get_collection.assert_called_once() + mock_collection_service.get_collection.assert_called_once_with(collection_name, mock_collection_service.get_collection.call_args[0][1]) @patch('stage0_mongodb_api.routes.collection_routes.CollectionService') def test_get_collection_not_found(self, mock_collection_service): @@ -113,7 +113,7 @@ def test_get_collection_not_found(self, mock_collection_service): mock_collection_service.get_collection.side_effect = CollectionNotFoundError(collection_name) # Act - response = self.client.get(f'/api/collections/{collection_name}') + response = self.client.get(f'/api/collections/{collection_name}/') # Assert self.assertEqual(response.status_code, 404) @@ -128,7 +128,7 @@ def test_get_collection_processing_error(self, mock_collection_service): mock_collection_service.get_collection.side_effect = CollectionProcessingError(collection_name, errors) # Act - response = self.client.get(f'/api/collections/{collection_name}') + response = self.client.get(f'/api/collections/{collection_name}/') # Assert self.assertEqual(response.status_code, 500) @@ -147,12 +147,12 @@ def test_process_specific_collection_success(self, mock_collection_service): mock_collection_service.process_collection.return_value = mock_result # Act - response = self.client.post(f'/api/collections/{collection_name}') + response = self.client.post(f'/api/collections/{collection_name}/') # Assert self.assertEqual(response.status_code, 200) self.assertEqual(response.json, mock_result) - mock_collection_service.process_collection.assert_called_once() + mock_collection_service.process_collection.assert_called_once_with(collection_name, mock_collection_service.process_collection.call_args[0][1]) @patch('stage0_mongodb_api.routes.collection_routes.CollectionService') def test_process_specific_collection_not_found(self, mock_collection_service): @@ -162,7 +162,7 @@ def test_process_specific_collection_not_found(self, mock_collection_service): mock_collection_service.process_collection.side_effect = CollectionNotFoundError(collection_name) # Act - response = self.client.post(f'/api/collections/{collection_name}') + response = self.client.post(f'/api/collections/{collection_name}/') # Assert self.assertEqual(response.status_code, 404) @@ -177,7 +177,7 @@ def test_process_specific_collection_processing_error(self, mock_collection_serv mock_collection_service.process_collection.side_effect = CollectionProcessingError(collection_name, errors) # Act - response = self.client.post(f'/api/collections/{collection_name}') + response = self.client.post(f'/api/collections/{collection_name}/') # Assert self.assertEqual(response.status_code, 500) @@ -191,7 +191,7 @@ def test_process_specific_collection_unexpected_error(self, mock_collection_serv mock_collection_service.process_collection.side_effect = Exception("Unexpected error") # Act - response = self.client.post(f'/api/collections/{collection_name}') + response = self.client.post(f'/api/collections/{collection_name}/') # Assert self.assertEqual(response.status_code, 500) diff --git a/tests/test_server.py b/tests/test_server.py index 747a20e..e580f79 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -22,6 +22,10 @@ class TestServer(unittest.TestCase): def setUp(self): """Set up test fixtures.""" self.app = app.test_client() + # Patch MongoIO for every test to ensure no real DB connection + patcher = patch('stage0_py_utils.MongoIO.get_instance', return_value=MagicMock()) + self.addCleanup(patcher.stop) + self.mock_mongo = patcher.start() def test_app_initialization(self): """Test Flask app initialization.""" @@ -47,11 +51,11 @@ def test_config_routes_registered(self): def test_collection_routes_registered(self): """Test collection routes are registered.""" - # Act - response = self.app.get('/api/collections') - - # Assert - self.assertNotEqual(response.status_code, 404) + with patch('stage0_mongodb_api.routes.collection_routes.CollectionService.list_collections', return_value=[{"collection_name": "dummy", "version": "1.0.0"}]): + # Act + response = self.app.get('/api/collections/') + # Assert + self.assertEqual(response.status_code, 200) def test_render_routes_registered(self): """Test render routes are registered.""" From 163d83f4e40a72bc775f10b75bc2e4da026df819 Mon Sep 17 00:00:00 2001 From: Mike Storey Date: Wed, 25 Jun 2025 17:29:17 -0400 Subject: [PATCH 5/5] added port number to compose --- docker-compose.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 452c3e2..c11f107 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -28,7 +28,6 @@ services: environment: MONGO_CONNECTION_STRING: mongodb://mongodb:27017/?replicaSet=rs0 MONGO_DB_NAME: test_database - MONGODB_API_PORT: 8081 AUTO_PROCESS: True LOAD_TEST_DATA: True depends_on: