From d41a82c82c199406c8ae268ed4a189a6c0b46c9d Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 1 Sep 2021 14:52:43 +0200 Subject: [PATCH] Issue #78: Add validator command to API (#83) * Add validate only API endpoint * Add test coverage of validate API * Make sure worker validator can run with no command set --- dmci/api/app.py | 32 +++++++++++++++++++++++++++++++- dmci/api/worker.py | 4 ++-- tests/test_api/test_app.py | 31 ++++++++++++++++++++++++++++++- tests/test_api/test_worker.py | 10 +++++----- 4 files changed, 68 insertions(+), 9 deletions(-) diff --git a/dmci/api/app.py b/dmci/api/app.py index e7cd88b..8db0c0a 100644 --- a/dmci/api/app.py +++ b/dmci/api/app.py @@ -76,6 +76,12 @@ def post_delete(metadata_id=None): else: return self._formatMsgReturn(OK_RETURN), 200 + @self.route("/v1/validate", methods=["POST"]) + def post_validate(): + """Process validate command.""" + msg, code = self._validate_method_post(request) + return self._formatMsgReturn(msg), code + return ## @@ -96,7 +102,7 @@ def _formatMsgReturn(self, msg): def _insert_update_method_post(self, cmd, request): """Process insert or update command requests.""" if request.content_length > self._conf.max_permitted_size: - return f"File bigger than permitted size: {self._conf.max_permitted_size}", 413 + return f"The file is larger than maximum size: {self._conf.max_permitted_size}", 413 data = request.get_data() @@ -126,6 +132,30 @@ def _insert_update_method_post(self, cmd, request): self._handle_persist_file(True, full_path) return OK_RETURN, 200 + def _validate_method_post(self, request): + """Only run the validator for submitted file.""" + if request.content_length > self._conf.max_permitted_size: + return f"The file is larger than maximum size: {self._conf.max_permitted_size}", 413 + + data = request.get_data() + + # Cache the job file + file_uuid = uuid.uuid4() + full_path = os.path.join(self._conf.distributor_cache, f"{file_uuid}.xml") + msg, code = self._persist_file(data, full_path) + if code != 200: + self._handle_persist_file(True, full_path) + return msg, code + + # Run the validator + worker = Worker("none", full_path, self._xsd_obj) + valid, msg = worker.validate(data) + self._handle_persist_file(True, full_path) + if valid: + return OK_RETURN, 200 + else: + return msg, 400 + def _distributor_wrapper(self, worker): """Run the distributors and handle and parse the results and parse and combine any error messages. diff --git a/dmci/api/worker.py b/dmci/api/worker.py index bf69dd3..f4b6202 100644 --- a/dmci/api/worker.py +++ b/dmci/api/worker.py @@ -78,7 +78,7 @@ def validate(self, data): # Takes in bytes-object data # Gives msg when both validating and not validating if not isinstance(data, bytes): - return False, "input must be bytes type" + return False, "Input must be bytes type" # Check xml file against XML schema definition valid = self._xsd_obj.validate(etree.fromstring(data)) @@ -148,7 +148,7 @@ def distribute(self): def _check_information_content(self, data): """Check the information content in the submitted file.""" if not isinstance(data, bytes): - return False, "input must be bytes type" + return False, "Input must be bytes type" # Read XML file xml_doc = etree.fromstring(data) diff --git a/tests/test_api/test_app.py b/tests/test_api/test_app.py index 842072f..6e1d247 100644 --- a/tests/test_api/test_app.py +++ b/tests/test_api/test_app.py @@ -89,11 +89,13 @@ def testApiApp_EndPoints(client): assert client.get("/").status_code == 404 assert client.get("/v1/").status_code == 404 + # Get method is not allowed assert client.get("/v1/insert").status_code == 405 assert client.get("/v1/update").status_code == 405 + assert client.get("/v1/validate").status_code == 405 # Bare delete command is not allowed - assert client.get("/v1/delete").status_code == 404 + assert client.post("/v1/delete").status_code == 404 # END Test testApiApp_EndPoints @@ -191,6 +193,33 @@ def testApiApp_DeleteRequests(client, monkeypatch): # END Test testApiApp_DeleteRequests +@pytest.mark.api +def testApiApp_ValidateRequests(client, monkeypatch): + """Test api validate request.""" + assert isinstance(client, flask.testing.FlaskClient) + + # Test sending 3MB of data + tooLargeFile = bytes(3000000) + assert client.post("/v1/validate", data=tooLargeFile).status_code == 413 + + # Fail cahcing the file + with monkeypatch.context() as mp: + mp.setattr("builtins.open", causeOSError) + assert client.post("/v1/validate", data=MOCK_XML).status_code == 507 + + # Data is valid + with monkeypatch.context() as mp: + mp.setattr("dmci.api.app.Worker.validate", lambda *a: (True, "")) + assert client.post("/v1/validate", data=MOCK_XML).status_code == 200 + + # Data is not valid + with monkeypatch.context() as mp: + mp.setattr("dmci.api.app.Worker.validate", lambda *a: (False, "")) + assert client.post("/v1/validate", data=MOCK_XML).status_code == 400 + +# END Test testApiApp_ValidateRequests + + @pytest.mark.api def testApiApp_PersistFile(tmpDir, monkeypatch): """Test the persistent file writer function.""" diff --git a/tests/test_api/test_worker.py b/tests/test_api/test_worker.py index 4162983..77bca26 100644 --- a/tests/test_api/test_worker.py +++ b/tests/test_api/test_worker.py @@ -105,12 +105,12 @@ def testApiWorker_Validator(monkeypatch, filesDir): failFile = os.path.join(filesDir, "api", "failing.xml") xsdObj = lxml.etree.XMLSchema(lxml.etree.parse(xsdFile)) - passWorker = Worker("insert", passFile, xsdObj) - failWorker = Worker("insert", failFile, xsdObj) + passWorker = Worker("none", passFile, xsdObj) + failWorker = Worker("none", failFile, xsdObj) # Invalid data format passData = readFile(passFile) - assert passWorker.validate(passData) == (False, "input must be bytes type") + assert passWorker.validate(passData) == (False, "Input must be bytes type") # Valid data format with monkeypatch.context() as mp: @@ -137,11 +137,11 @@ def testApiWorker_Validator(monkeypatch, filesDir): def testApiWorker_CheckInfoContent(monkeypatch, filesDir): """Test _check_information_content.""" passFile = os.path.join(filesDir, "api", "passing.xml") - tstWorker = Worker("insert", passFile, None) + tstWorker = Worker("none", passFile, None) # Invalid data format passData = readFile(passFile) - assert tstWorker._check_information_content(passData) == (False, "input must be bytes type") + assert tstWorker._check_information_content(passData) == (False, "Input must be bytes type") # Valid data format with monkeypatch.context() as mp: