From 2089170dcb933a73fe0d45ec6a130948d1c8045e Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Tue, 20 Jun 2023 12:53:11 +0100 Subject: [PATCH 01/97] Create Hello world endpoint Co-authored-by: Pira Tejasakulsin --- README.md | 128 ++++++++++++----------------------------------- app/app.py | 14 ++++++ requirements.txt | 7 +++ 3 files changed, 52 insertions(+), 97 deletions(-) create mode 100644 app/app.py create mode 100644 requirements.txt diff --git a/README.md b/README.md index 9087fc4..68191e5 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,47 @@ # tdse-accessForce-bids-api -Bids API training project with Python and MongoDB +# API Documentation -# Bid Library +This API provides a simple "Hello World" message. -## Contents +## Prerequisites -- [Background](#background) - - [Before working on a bid](#before-working-on-a-bid) - - [Bid phases](#phases) -- [Brief](#brief) - - [Acceptance Criteria](#acceptance-criteria) - - [Iterations](#iterations) +- Python 3.x +- Flask -## Background +## Running the API -Methods being a consultancy agency to win new work we make to make bids on client tenders. +1. Clone the repository to your local machine: -- A tender is a piece of work that an organisation (potential client) needs an external team to work on or to supplement an existing team -- A bid can comprise of several stages to win a tender, usually there are two phases which comprise of phase 1 and phase 2 + ```bash + git clone + ``` +2. Navigate to the root directory of the project: -### Before working on a bid + ```bash + cd tdse-accessForce-bids-api + ``` +3. Install python 3.x if not already installed. You can check if it is installed by running the following command: -Before phase 1, there is time for bidders like Methods to ask questions of the client tender. These questions are open to all those looking to bid on the tender and all questions and answers are available to all bidders on a single tender. + ```bash + python --version + ``` +4. Run the virtual environment by running the following command: -This step is a necessary is really important for Methods to understand whether we really want to bid on a particular tender. Some considerations before bidding: + ```bash + source venv/bin/activate + ``` +5. Install the required dependencies by running the following command: -- Do we like the project and hence want it? -- Is it good value? -- Will this get us known with a new client and give Methods leverage in future work? -- Is the tender something different that would expand our portfolio? -- Can we do it? + ```bash + pip install -r requirements.txt + ``` +6. Run the following command to start the API: -### Phases + ```bash + python app.py + ``` +7. The API will be available at http://localhost:5000 -**Phase 1** compromises of a list of questions set by the tender; in government the scoring system for each question is out of 3, so if there are 6 questions a bidder can achieve a maximum score of 18. The answers to the questions usually have a word limit of 100 or 200 (in this phase). - -The client that puts out a tender decides the pass rate in phase 1 to progress to phase 2. The pass rate may not be known until results of all bids are completed for each of the bidders. The list below are common pass criteria you might come across on a bid (remember this can vary from client to client and even within multiple tenders by the same client): - -- All questions must have a score greater than 1, (so 2 minimum) -- A minimum overall score, e.g. 14 out of 18 -- The 3 highest bids assuming the bids met criteria 1 or/and 2 - -**Phase 2** is a lot more involved than phase 1, it can comprise of a face to face (or virtual) presentation alongside answers to questions limited to x number of words (limit set by client). These questions will cover team culture and technical solution. - -There are usually 3 categories that Methods are scored on and these are weighted by the client. The categories are: - -- Technical - questions from presentation or form and results from phase 1 -- Culture - questions in phase 2 -- Cost (a.k.a. Rate) - -The overall score is worked out as a percentage, (out of 100). Whichever bidder scores highest wins the bid and that is the end of the tendering process. - -Next steps: Statement of Work (SoW, the contract) is put together by the client, handing over CVs of potential staff Methods are going to supply and agreement on project start dates. - --------------- - -## Brief - -Currently Methods store all the information for tenders and bids in Sharepoint, the way the documents are stored and the information available can vary quite a lot making it hard for the bid team to find good answers to questions and successful bids for reuse. We currently do not store who helped answering questions against a bid and in some cases where Methods have done so it only informs us of their initials. - -What intend to build is an API that can store tender/bid information in a structured way to facilitate finding successful bids and high scoring questions. - -### Acceptance Criteria - -**Must** have: - -1. Ability to access all bid data, see list of data below: - - - tender title - - tender short description (problem statement) - - client - - date of the tender - - were Methods successful - - what phase did we get to - - how well we did in each of the phases - - any technologies or skills reuired by client tender - - tender questions and Methods answers and the respective scores - - who helped answer a question - - provide links to further information on bids stored in sharepoint - - when was the data last updated - - any skills or technologies listed in the answers to questions - -1. Ability to find any bid -1. Ability to add new bids -1. Ability to update a bid that is still in progress -1. Ability to delete a bid and associated -1. Ability to recover deleted bid data within 4 weeks of deletion -1. Ability to filter bids and questions based on success and score -1. Ability to sort bids and questions alphanumerically and page through the results -1. Ability to secure access to changing the data to certain users - -**Should** have: - -1. Ability to search for bids containing particular text -1. Ability to search for questions containing particular text - -**Could** have: - -1. Ability to control different user access (permissions) based on roles - - - Admin - - Bid writers - - Bid viewers - -1. Ability to access this software anywhere in the UK - -**Would not** have: - -1. Due to size of some answers and the content not being soley text but images, diagrams etc. Methods does not wish to duplicate this information from Sharepoint into a filesystem like AWS S3 or Azure Storage ### Iterations diff --git a/app/app.py b/app/app.py new file mode 100644 index 0000000..e75f5b0 --- /dev/null +++ b/app/app.py @@ -0,0 +1,14 @@ +from flask import Flask + +app = Flask(__name__) + +@app.route('/') +def index(): + return 'Index Page' + +@app.route('/helloworld') +def hello_world(): + return 'hello world' + +if __name__ == "__main__": + app.run(debug=True, port=4000) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..17c7d81 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +blinker==1.6.2 +click==8.1.3 +Flask==2.3.2 +itsdangerous==2.1.2 +Jinja2==3.1.2 +MarkupSafe==2.1.3 +Werkzeug==2.3.6 From 1571f0a5d6f8d30402610cb9e1b541203cd810a8 Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 20 Jun 2023 13:14:54 +0100 Subject: [PATCH 02/97] tested and updated readme instructions for running app --- README.md | 21 +++++++++++++-------- app/app.py | 2 +- requirements.txt | 1 + 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 68191e5..6b9177e 100644 --- a/README.md +++ b/README.md @@ -23,24 +23,29 @@ This API provides a simple "Hello World" message. 3. Install python 3.x if not already installed. You can check if it is installed by running the following command: ```bash - python --version + python3 --version ``` -4. Run the virtual environment by running the following command: +4. Create the virtual environment by running the following command: - ```bash - source venv/bin/activate ``` -5. Install the required dependencies by running the following command: + python3 -m venv .venv + ``` +5. Run the virtual environment by running the following command: + + ```bash + source .venv/bin/activate + ``` +6. Install the required dependencies by running the following command: ```bash pip install -r requirements.txt ``` -6. Run the following command to start the API: +7. Run the following command to start the API: ```bash - python app.py + python app/app.py ``` -7. The API will be available at http://localhost:5000 +8. The API will be available at http://localhost:3000 ### Iterations diff --git a/app/app.py b/app/app.py index e75f5b0..162be76 100644 --- a/app/app.py +++ b/app/app.py @@ -11,4 +11,4 @@ def hello_world(): return 'hello world' if __name__ == "__main__": - app.run(debug=True, port=4000) \ No newline at end of file + app.run(debug=True, port=3000) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 17c7d81..ad88b92 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ itsdangerous==2.1.2 Jinja2==3.1.2 MarkupSafe==2.1.3 Werkzeug==2.3.6 +pip==23.1.2 From ea1d4994e81fe0f0de1a881a12c009950c31302a Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Tue, 20 Jun 2023 17:04:30 +0100 Subject: [PATCH 03/97] test: test_hello_world controller function. Co-authored-by: Pira Tejasakulsin --- CONTRIBUTING.md | 22 +++++++++++++++++++++- app.py | 11 +++++++++++ app/app.py | 14 -------------- controllers/__init__.py | 0 controllers/hello_controller.py | 2 ++ requirements.txt | 1 + routes/__init__.py | 0 routes/hello_route.py | 9 +++++++++ tests/__init__.py | 0 tests/test_hello_controller.py | 5 +++++ 10 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 app.py delete mode 100644 app/app.py create mode 100644 controllers/__init__.py create mode 100644 controllers/hello_controller.py create mode 100644 routes/__init__.py create mode 100644 routes/hello_route.py create mode 100644 tests/__init__.py create mode 100644 tests/test_hello_controller.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e2f5f4..fd6855d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,9 +2,29 @@ Contributing guidelines ======================= ### Git workflow - * Use git-flow - create a feature branch from `develop`, e.g. `feature/new-feature` * Pull requests must contain a succinct, clear summary of what the user need is driving this feature change * Ensure your branch contains logical atomic commits before sending a pull request * You may rebase your branch after feedback if it's to include relevant updates from the develop branch. It is preferable to rebase here then a merge commit as a clean and straight history on develop with discrete merge commits for features is preferred * To find out more about contributing click [here](https://contributing.md/) + +### Commit messages +Please use the following format for commit messages: + +``` +* fix: for a bug fix +* feat: for a new feature +* docs: for documentation changes +* style: for changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) +* refactor: for refactoring production code +* test: for adding tests +* chore: for updating build tasks, package manager configs, etc +* build: for changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) +``` + +### Pull requests +* Pull requests should be made to the `develop` branch +* Pull requests should be made from a feature branch, not `develop` +* Pull requests should be made with a succinct, clear summary of what the user need is driving this feature change +* An automated template will be provided to help with this process, please fill it out as best you can + diff --git a/app.py b/app.py new file mode 100644 index 0000000..6f956a6 --- /dev/null +++ b/app.py @@ -0,0 +1,11 @@ +from flask import Flask + +from routes.hello_route import hello + +app = Flask(__name__) + +app.register_blueprint(hello, url_prefix='/api') + + +if __name__ == '__main__': + app.run(debug=True, port=3000) \ No newline at end of file diff --git a/app/app.py b/app/app.py deleted file mode 100644 index 162be76..0000000 --- a/app/app.py +++ /dev/null @@ -1,14 +0,0 @@ -from flask import Flask - -app = Flask(__name__) - -@app.route('/') -def index(): - return 'Index Page' - -@app.route('/helloworld') -def hello_world(): - return 'hello world' - -if __name__ == "__main__": - app.run(debug=True, port=3000) \ No newline at end of file diff --git a/controllers/__init__.py b/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/controllers/hello_controller.py b/controllers/hello_controller.py new file mode 100644 index 0000000..57bb734 --- /dev/null +++ b/controllers/hello_controller.py @@ -0,0 +1,2 @@ +def hello_world(): + return 'Hello, World!' diff --git a/requirements.txt b/requirements.txt index ad88b92..3d85fa9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ Jinja2==3.1.2 MarkupSafe==2.1.3 Werkzeug==2.3.6 pip==23.1.2 +pytest==7.3.2 diff --git a/routes/__init__.py b/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routes/hello_route.py b/routes/hello_route.py new file mode 100644 index 0000000..c614f8e --- /dev/null +++ b/routes/hello_route.py @@ -0,0 +1,9 @@ +from flask import Blueprint + +from controllers.hello_controller import hello_world + +hello = Blueprint('hello', __name__) + +@hello.route('/helloworld') +def hey_world(): + return hello_world() \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_hello_controller.py b/tests/test_hello_controller.py new file mode 100644 index 0000000..843fe05 --- /dev/null +++ b/tests/test_hello_controller.py @@ -0,0 +1,5 @@ +from controllers.hello_controller import hello_world + +def test_hello_world(): + result = hello_world() + assert result == 'Hello, World!' \ No newline at end of file From 0af031bd50c696cf542a8a7a43594db711d14919 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Tue, 20 Jun 2023 17:21:37 +0100 Subject: [PATCH 04/97] test: routes on hello_world --- controllers/hello_controller.py | 2 ++ routes/hello_route.py | 2 +- tests/test_hello_controller.py | 5 ----- tests/test_hello_world.py | 22 ++++++++++++++++++++++ 4 files changed, 25 insertions(+), 6 deletions(-) delete mode 100644 tests/test_hello_controller.py create mode 100644 tests/test_hello_world.py diff --git a/controllers/hello_controller.py b/controllers/hello_controller.py index 57bb734..bc1d979 100644 --- a/controllers/hello_controller.py +++ b/controllers/hello_controller.py @@ -1,2 +1,4 @@ + + def hello_world(): return 'Hello, World!' diff --git a/routes/hello_route.py b/routes/hello_route.py index c614f8e..7e1c198 100644 --- a/routes/hello_route.py +++ b/routes/hello_route.py @@ -5,5 +5,5 @@ hello = Blueprint('hello', __name__) @hello.route('/helloworld') -def hey_world(): +def greet_world(): return hello_world() \ No newline at end of file diff --git a/tests/test_hello_controller.py b/tests/test_hello_controller.py deleted file mode 100644 index 843fe05..0000000 --- a/tests/test_hello_controller.py +++ /dev/null @@ -1,5 +0,0 @@ -from controllers.hello_controller import hello_world - -def test_hello_world(): - result = hello_world() - assert result == 'Hello, World!' \ No newline at end of file diff --git a/tests/test_hello_world.py b/tests/test_hello_world.py new file mode 100644 index 0000000..0258a1c --- /dev/null +++ b/tests/test_hello_world.py @@ -0,0 +1,22 @@ +from flask import Flask +import pytest + +from controllers.hello_controller import hello_world + +from routes.hello_route import hello + +def test_hello_world(): + result = hello_world() + assert result == 'Hello, World!' + +@pytest.fixture +def client(): + app = Flask(__name__) + app.register_blueprint(hello, url_prefix='/api') + with app.test_client() as client: + yield client + +def test_hello_world_route(client): + response = client.get('/api/helloworld') + assert response.status_code == 200 + assert response.data.decode('utf-8') == 'Hello, World!' \ No newline at end of file From 63c02ba327b1091a7926da7c01ffe18931de745f Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 21 Jun 2023 16:47:34 +0100 Subject: [PATCH 05/97] feat: added and manually tested post route for bid Co-authored-by: Julio Velezmoro --- {controllers => api/models}/__init__.py | 0 .../__init__.py => api/models/bid_models.py | 0 .../models}/hello_controller.py | 0 api/routes/__init__.py | 0 api/routes/bid_route.py | 54 +++++++++++++++++++ {routes => api/routes}/hello_route.py | 2 +- api/schemas/__init__.py | 0 api/schemas/bid_schema.py | 33 ++++++++++++ app.py | 4 +- example.http | 34 ++++++++++++ tests/test_bid.py | 1 + 11 files changed, 125 insertions(+), 3 deletions(-) rename {controllers => api/models}/__init__.py (100%) rename routes/__init__.py => api/models/bid_models.py (100%) rename {controllers => api/models}/hello_controller.py (100%) create mode 100644 api/routes/__init__.py create mode 100644 api/routes/bid_route.py rename {routes => api/routes}/hello_route.py (72%) create mode 100644 api/schemas/__init__.py create mode 100644 api/schemas/bid_schema.py create mode 100644 example.http create mode 100644 tests/test_bid.py diff --git a/controllers/__init__.py b/api/models/__init__.py similarity index 100% rename from controllers/__init__.py rename to api/models/__init__.py diff --git a/routes/__init__.py b/api/models/bid_models.py similarity index 100% rename from routes/__init__.py rename to api/models/bid_models.py diff --git a/controllers/hello_controller.py b/api/models/hello_controller.py similarity index 100% rename from controllers/hello_controller.py rename to api/models/hello_controller.py diff --git a/api/routes/__init__.py b/api/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/routes/bid_route.py b/api/routes/bid_route.py new file mode 100644 index 0000000..dcf8b2a --- /dev/null +++ b/api/routes/bid_route.py @@ -0,0 +1,54 @@ + +from flask import Blueprint, jsonify, request +from api.schemas.bid_schema import BidSchema + +bid = Blueprint('bid', __name__) + +@bid.route("/bids", methods=["GET"]) +def get_test(): + if request.method == "GET": + return 'test', 200 + +@bid.route("/bids", methods=["POST"]) +def create_bid(): + mandatory_fields = ['id', 'tender', 'client', 'bid_date', 'status'] + if not request.is_json: + return jsonify({'error': 'Invalid JSON'}), 400 + + for field in mandatory_fields: + if field not in request.json: + return jsonify({'error': f'Missing mandatory field: {field}'}), 400 + + # Create a mock BidSchema object with sample values + bid_schema = BidSchema( + id=request.json['id'], + tender=request.json['tender'], + client=request.json['client'], + alias=request.json.get('alias', ''), + bid_date=request.json['bid_date'], + bid_folder_url=request.json.get('bid_folder_url', ''), + status='in-progress', + links={ + 'self': f"/bids/{request.json['id']}", + 'questions': f"/bids/{request.json['id']}/questions" + }, + was_successful=request.json.get('was_successful', ''), + success=request.json.get('success', []), + failed=request.json.get('failed', {}), + feedback=request.json.get('feedback', {}), + last_updated='' + ) + + # Convert the mock BidSchema object to a dictionary + bid_json = bid_schema.toDbCollection() + return bid_json, 201 + +@bid.errorhandler(404) +def notFound(error=None): + message = { + "message": "Resource not found", + "status": 404 + } + response = jsonify(message) + response.status_code = 404 + return response diff --git a/routes/hello_route.py b/api/routes/hello_route.py similarity index 72% rename from routes/hello_route.py rename to api/routes/hello_route.py index 7e1c198..d7142f1 100644 --- a/routes/hello_route.py +++ b/api/routes/hello_route.py @@ -1,6 +1,6 @@ from flask import Blueprint -from controllers.hello_controller import hello_world +from api.models.hello_controller import hello_world hello = Blueprint('hello', __name__) diff --git a/api/schemas/__init__.py b/api/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py new file mode 100644 index 0000000..2807fea --- /dev/null +++ b/api/schemas/bid_schema.py @@ -0,0 +1,33 @@ +# Description: Schema for the bid object +class BidSchema: + def __init__(self, id, tender, client, bid_date, alias='', bid_folder_url='', status='in-progress', links=None, was_successful='', success=None, failed=None, feedback=None, last_updated=''): + self.id = id + self.tender = tender + self.client = client + self.alias = alias + self.bid_date = bid_date + self.bid_folder_url = bid_folder_url + self.status = status + self.links = links + self.was_successful = was_successful + self.success = success + self.failed = failed + self.feedback = feedback + self.last_updated = last_updated + + def toDbCollection(self): + return { + "id": self.id, + "tender": self.tender, + "client": self.client, + "alias": self.alias, + "bid_date": self.bid_date, + "bid_folder_url": self.bid_folder_url, + "status": self.status, + "links": self.links, + "was_successful": self.was_successful, + "success": self.success, + "failed": self.failed, + "feedback": self.feedback, + "last_updated": self.last_updated + } \ No newline at end of file diff --git a/app.py b/app.py index 6f956a6..bc25825 100644 --- a/app.py +++ b/app.py @@ -1,10 +1,10 @@ from flask import Flask -from routes.hello_route import hello +from api.routes.bid_route import bid app = Flask(__name__) -app.register_blueprint(hello, url_prefix='/api') +app.register_blueprint(bid, url_prefix='/api') if __name__ == '__main__': diff --git a/example.http b/example.http new file mode 100644 index 0000000..f1a2690 --- /dev/null +++ b/example.http @@ -0,0 +1,34 @@ +POST http://localhost:3000/api/bids HTTP/1.1 +Content-Type: application/json + +{ + "id": "test_id", + "tender": "test_tender", + "client": "test_client", + "alias": "test_alias", + "bid_date": "2023-06-21T10:00:00Z", + "bid_folder_url": "test_folder_url", + "status": "in-progress", + "links": { + "self": "/bids/test_id", + "questions": "/bids/test_id/questions" + }, + "was_successful": "test_success", + "success": [ + { + "phase": 1, + "has_score": true, + "score": 80, + "out_of": 100 + } + ], + "failed": { + "phase": 2, + "has_score": false + }, + "feedback": { + "description": "test_description", + "url": "test_url" + }, + "last_updated": "2023-06-21T12:00:00Z" +} \ No newline at end of file diff --git a/tests/test_bid.py b/tests/test_bid.py new file mode 100644 index 0000000..e2fd963 --- /dev/null +++ b/tests/test_bid.py @@ -0,0 +1 @@ +#pass \ No newline at end of file From ac87d4e27d457815a7daeb0cb08227359ee606c9 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Thu, 22 Jun 2023 10:46:40 +0100 Subject: [PATCH 06/97] feat: saving data in memory and separating logic Co-authored-by: Pira Tejasakulsin --- api/models/bid_models.py | 45 +++++++++++++++++++++++++++ api/models/hello_controller.py | 4 --- api/routes/bid_route.py | 56 ++++++---------------------------- api/routes/hello_route.py | 9 ------ api/schemas/bid_schema.py | 6 ++-- example.http | 6 ++-- test.txt | 1 + 7 files changed, 61 insertions(+), 66 deletions(-) delete mode 100644 api/models/hello_controller.py delete mode 100644 api/routes/hello_route.py create mode 100644 test.txt diff --git a/api/models/bid_models.py b/api/models/bid_models.py index e69de29..faf76be 100644 --- a/api/models/bid_models.py +++ b/api/models/bid_models.py @@ -0,0 +1,45 @@ +from flask import request, jsonify +from uuid import uuid4 +from api.schemas.bid_schema import BidSchema + +def get_test(): + return 'test', 200 + +def create_bid(): + mandatory_fields = ['tender', 'client', 'bid_date', 'status'] + if not request.is_json: + return jsonify({'error': 'Invalid JSON'}), 400 + + for field in mandatory_fields: + if field not in request.json: + return jsonify({'error': f'Missing mandatory field: {field}'}), 400 + + # BidSchema object + id_obj = uuid4() + bid_schema = BidSchema( + id=id_obj, + tender=request.json['tender'], + client=request.json['client'], + alias=request.json.get('alias', ''), + bid_date=request.json['bid_date'], + bid_folder_url=request.json.get('bid_folder_url', ''), + status='in-progress', + links={ + 'self': f"/bids/{id_obj}", + 'questions': f"/bids/{id_obj}/questions" + }, + was_successful=request.json.get('was_successful', ''), + success=request.json.get('success', []), + failed=request.json.get('failed', {}), + feedback=request.json.get('feedback', {}), + last_updated='' + ) + # Convert the mock BidSchema object to a dictionary + bid_json = bid_schema.toDbCollection() + + # Save data in memory + f=open('test.txt','a') + f.write(str(bid_json)) + f.close + + return bid_json, 201 \ No newline at end of file diff --git a/api/models/hello_controller.py b/api/models/hello_controller.py deleted file mode 100644 index bc1d979..0000000 --- a/api/models/hello_controller.py +++ /dev/null @@ -1,4 +0,0 @@ - - -def hello_world(): - return 'Hello, World!' diff --git a/api/routes/bid_route.py b/api/routes/bid_route.py index dcf8b2a..d2c60fb 100644 --- a/api/routes/bid_route.py +++ b/api/routes/bid_route.py @@ -1,54 +1,16 @@ -from flask import Blueprint, jsonify, request -from api.schemas.bid_schema import BidSchema +from flask import Blueprint +from api.models.bid_models import get_test, create_bid bid = Blueprint('bid', __name__) @bid.route("/bids", methods=["GET"]) -def get_test(): - if request.method == "GET": - return 'test', 200 - -@bid.route("/bids", methods=["POST"]) -def create_bid(): - mandatory_fields = ['id', 'tender', 'client', 'bid_date', 'status'] - if not request.is_json: - return jsonify({'error': 'Invalid JSON'}), 400 - - for field in mandatory_fields: - if field not in request.json: - return jsonify({'error': f'Missing mandatory field: {field}'}), 400 - - # Create a mock BidSchema object with sample values - bid_schema = BidSchema( - id=request.json['id'], - tender=request.json['tender'], - client=request.json['client'], - alias=request.json.get('alias', ''), - bid_date=request.json['bid_date'], - bid_folder_url=request.json.get('bid_folder_url', ''), - status='in-progress', - links={ - 'self': f"/bids/{request.json['id']}", - 'questions': f"/bids/{request.json['id']}/questions" - }, - was_successful=request.json.get('was_successful', ''), - success=request.json.get('success', []), - failed=request.json.get('failed', {}), - feedback=request.json.get('feedback', {}), - last_updated='' - ) - - # Convert the mock BidSchema object to a dictionary - bid_json = bid_schema.toDbCollection() - return bid_json, 201 +def get_tested(): + response = get_test() + return response -@bid.errorhandler(404) -def notFound(error=None): - message = { - "message": "Resource not found", - "status": 404 - } - response = jsonify(message) - response.status_code = 404 +@bid.route("/bids", methods=["POST"]) +def post_bid(): + response = create_bid() return response + diff --git a/api/routes/hello_route.py b/api/routes/hello_route.py deleted file mode 100644 index d7142f1..0000000 --- a/api/routes/hello_route.py +++ /dev/null @@ -1,9 +0,0 @@ -from flask import Blueprint - -from api.models.hello_controller import hello_world - -hello = Blueprint('hello', __name__) - -@hello.route('/helloworld') -def greet_world(): - return hello_world() \ No newline at end of file diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py index 2807fea..ac6861b 100644 --- a/api/schemas/bid_schema.py +++ b/api/schemas/bid_schema.py @@ -7,7 +7,7 @@ def __init__(self, id, tender, client, bid_date, alias='', bid_folder_url='', st self.alias = alias self.bid_date = bid_date self.bid_folder_url = bid_folder_url - self.status = status + self.status = status # enum: "deleted", "in-progress" or "completed" self.links = links self.was_successful = was_successful self.success = success @@ -23,8 +23,8 @@ def toDbCollection(self): "alias": self.alias, "bid_date": self.bid_date, "bid_folder_url": self.bid_folder_url, - "status": self.status, - "links": self.links, + "status": self.status, + "links": self.links, "was_successful": self.was_successful, "success": self.success, "failed": self.failed, diff --git a/example.http b/example.http index f1a2690..8b80cf1 100644 --- a/example.http +++ b/example.http @@ -2,7 +2,7 @@ POST http://localhost:3000/api/bids HTTP/1.1 Content-Type: application/json { - "id": "test_id", + "tender": "test_tender", "client": "test_client", "alias": "test_alias", @@ -10,8 +10,8 @@ Content-Type: application/json "bid_folder_url": "test_folder_url", "status": "in-progress", "links": { - "self": "/bids/test_id", - "questions": "/bids/test_id/questions" + "self": "/bids/", + "questions": "/bids//questions" }, "was_successful": "test_success", "success": [ diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..1e218c9 --- /dev/null +++ b/test.txt @@ -0,0 +1 @@ +{'id': UUID('60309da9-73a1-4e9c-a647-09d6310be535'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T10:00:00Z', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/60309da9-73a1-4e9c-a647-09d6310be535', 'questions': '/bids/60309da9-73a1-4e9c-a647-09d6310be535/questions'}, 'was_successful': 'test_success', 'success': [{'phase': 1, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 2, 'has_score': False}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': ''}{'id': UUID('6e8607cb-12af-4dde-8ed2-b15786a5c1f8'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T10:00:00Z', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/6e8607cb-12af-4dde-8ed2-b15786a5c1f8', 'questions': '/bids/6e8607cb-12af-4dde-8ed2-b15786a5c1f8/questions'}, 'was_successful': 'test_success', 'success': [{'phase': 1, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 2, 'has_score': False}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': ''}{'id': UUID('c465d845-f677-4700-8236-fad15dd12859'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T10:00:00Z', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/c465d845-f677-4700-8236-fad15dd12859', 'questions': '/bids/c465d845-f677-4700-8236-fad15dd12859/questions'}, 'was_successful': 'test_success', 'success': [{'phase': 1, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 2, 'has_score': False}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': ''}{'id': UUID('99f38b86-ae2c-405c-8508-a897a3521551'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T10:00:00Z', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/99f38b86-ae2c-405c-8508-a897a3521551', 'questions': '/bids/99f38b86-ae2c-405c-8508-a897a3521551/questions'}, 'was_successful': 'test_success', 'success': [{'phase': 1, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 2, 'has_score': False}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': ''} \ No newline at end of file From d1f230cd04e7ef69a97cbb6152c363c300366e9b Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 22 Jun 2023 12:48:00 +0100 Subject: [PATCH 07/97] wip: add phase info to success/failed Co-authored-by: Julio Velezmoro --- api/models/bid_models.py | 22 ++++++++-------------- api/schemas/bid_schema.py | 30 ++++++++++++++++++++++-------- example.http | 25 +++---------------------- helpers/helpers.py | 0 test.txt | 1 - 5 files changed, 33 insertions(+), 45 deletions(-) create mode 100644 helpers/helpers.py diff --git a/api/models/bid_models.py b/api/models/bid_models.py index faf76be..5c010e4 100644 --- a/api/models/bid_models.py +++ b/api/models/bid_models.py @@ -1,6 +1,5 @@ from flask import request, jsonify -from uuid import uuid4 -from api.schemas.bid_schema import BidSchema +from api.schemas.bid_schema import BidSchema, PhaseInfo def get_test(): return 'test', 200 @@ -14,31 +13,26 @@ def create_bid(): if field not in request.json: return jsonify({'error': f'Missing mandatory field: {field}'}), 400 - # BidSchema object - id_obj = uuid4() + # BidSchema object bid_schema = BidSchema( - id=id_obj, tender=request.json['tender'], client=request.json['client'], alias=request.json.get('alias', ''), bid_date=request.json['bid_date'], bid_folder_url=request.json.get('bid_folder_url', ''), status='in-progress', - links={ - 'self': f"/bids/{id_obj}", - 'questions': f"/bids/{id_obj}/questions" - }, - was_successful=request.json.get('was_successful', ''), - success=request.json.get('success', []), + failed=request.json.get('failed', {}), - feedback=request.json.get('feedback', {}), - last_updated='' + feedback=request.json.get('feedback', {}) ) + success_phase = PhaseInfo() + bid_schema.success.append(success_phase(1, True, 2, 3)) + bid_schema.was_successful = bid_schema.failed == {} # Convert the mock BidSchema object to a dictionary bid_json = bid_schema.toDbCollection() # Save data in memory - f=open('test.txt','a') + f=open('./test.txt','a') f.write(str(bid_json)) f.close diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py index ac6861b..63328ac 100644 --- a/api/schemas/bid_schema.py +++ b/api/schemas/bid_schema.py @@ -1,19 +1,25 @@ +from uuid import uuid4 +from datetime import datetime + # Description: Schema for the bid object class BidSchema: - def __init__(self, id, tender, client, bid_date, alias='', bid_folder_url='', status='in-progress', links=None, was_successful='', success=None, failed=None, feedback=None, last_updated=''): - self.id = id + def __init__(self, tender, client, bid_date, alias='', bid_folder_url='', status='in-progress', was_successful=False, failed=None, feedback=None): + self.id = uuid4() self.tender = tender self.client = client self.alias = alias - self.bid_date = bid_date + self.bid_date = datetime.strptime(bid_date, '%d-%m-%Y').isoformat() # DD-MM-YYYY self.bid_folder_url = bid_folder_url self.status = status # enum: "deleted", "in-progress" or "completed" - self.links = links + self.links = { + 'self': f"/bids/{self.id}", + 'questions': f"/bids/{self.id}/questions" + } self.was_successful = was_successful - self.success = success + self.success = [] self.failed = failed self.feedback = feedback - self.last_updated = last_updated + self.last_updated = datetime.now().isoformat() def toDbCollection(self): return { @@ -26,8 +32,16 @@ def toDbCollection(self): "status": self.status, "links": self.links, "was_successful": self.was_successful, - "success": self.success, + "success": [s.__dict__ for s in self.success] if self.success else None, "failed": self.failed, "feedback": self.feedback, "last_updated": self.last_updated - } \ No newline at end of file + } + +# Schema for phaseInfo object +class PhaseInfo: + def __init__(self, phase, has_score, score=None, out_of=None): + self.phase = phase + self.has_score = has_score + self.score = score + self.out_of = out_of \ No newline at end of file diff --git a/example.http b/example.http index 8b80cf1..dae357c 100644 --- a/example.http +++ b/example.http @@ -2,33 +2,14 @@ POST http://localhost:3000/api/bids HTTP/1.1 Content-Type: application/json { - "tender": "test_tender", "client": "test_client", "alias": "test_alias", - "bid_date": "2023-06-21T10:00:00Z", + "bid_date": "21-06-2023", "bid_folder_url": "test_folder_url", - "status": "in-progress", - "links": { - "self": "/bids/", - "questions": "/bids//questions" - }, - "was_successful": "test_success", - "success": [ - { - "phase": 1, - "has_score": true, - "score": 80, - "out_of": 100 - } - ], - "failed": { - "phase": 2, - "has_score": false - }, + "status": "in-progress", "feedback": { "description": "test_description", "url": "test_url" - }, - "last_updated": "2023-06-21T12:00:00Z" + } } \ No newline at end of file diff --git a/helpers/helpers.py b/helpers/helpers.py new file mode 100644 index 0000000..e69de29 diff --git a/test.txt b/test.txt index 1e218c9..e69de29 100644 --- a/test.txt +++ b/test.txt @@ -1 +0,0 @@ -{'id': UUID('60309da9-73a1-4e9c-a647-09d6310be535'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T10:00:00Z', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/60309da9-73a1-4e9c-a647-09d6310be535', 'questions': '/bids/60309da9-73a1-4e9c-a647-09d6310be535/questions'}, 'was_successful': 'test_success', 'success': [{'phase': 1, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 2, 'has_score': False}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': ''}{'id': UUID('6e8607cb-12af-4dde-8ed2-b15786a5c1f8'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T10:00:00Z', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/6e8607cb-12af-4dde-8ed2-b15786a5c1f8', 'questions': '/bids/6e8607cb-12af-4dde-8ed2-b15786a5c1f8/questions'}, 'was_successful': 'test_success', 'success': [{'phase': 1, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 2, 'has_score': False}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': ''}{'id': UUID('c465d845-f677-4700-8236-fad15dd12859'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T10:00:00Z', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/c465d845-f677-4700-8236-fad15dd12859', 'questions': '/bids/c465d845-f677-4700-8236-fad15dd12859/questions'}, 'was_successful': 'test_success', 'success': [{'phase': 1, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 2, 'has_score': False}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': ''}{'id': UUID('99f38b86-ae2c-405c-8508-a897a3521551'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T10:00:00Z', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/99f38b86-ae2c-405c-8508-a897a3521551', 'questions': '/bids/99f38b86-ae2c-405c-8508-a897a3521551/questions'}, 'was_successful': 'test_success', 'success': [{'phase': 1, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 2, 'has_score': False}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': ''} \ No newline at end of file From 53d3913f6e599497b5356e40cc5263954ac6782c Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 22 Jun 2023 16:19:07 +0100 Subject: [PATCH 08/97] feat: field validation, set status enum Co-authored-by: Julio Velezmoro --- .vscode/settings.json | 6 +++++ README.md | 16 +---------- api/models/bid_models.py | 19 ++++++------- api/schemas/bid_schema.py | 52 +++++++++++++++++------------------- api/schemas/phase_schema.py | 7 +++++ api/schemas/status_schema.py | 7 +++++ example.http | 9 +++---- test.txt | 1 + 8 files changed, 60 insertions(+), 57 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 api/schemas/phase_schema.py create mode 100644 api/schemas/status_schema.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d99f2f3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "python.formatting.provider": "none" +} \ No newline at end of file diff --git a/README.md b/README.md index 6b9177e..1402680 100644 --- a/README.md +++ b/README.md @@ -45,23 +45,9 @@ This API provides a simple "Hello World" message. ```bash python app/app.py ``` -8. The API will be available at http://localhost:3000 +8. The API will be available at http://localhost:3000/api/bids -### Iterations - -**Iteration 1** Build API and initial storage system to find, add, update and remove. Steps 1 to 8 from Must section - -**Iteration 2** Secure the API to users who need access, based on the "Principle least priviledge principle. Step 9 from Must section - -**Iteration 3** Build search engine to allow for a more sophisticated way of finding questions and bids related to your needs. Steps 1 and 2 from Should section - -**Iteration 4** Expand on access control to bid library based on roles, users and teams where necessary. Step 1 of Could section - -**Iteration 5** Host the bid library to be accessed by users across the country. Step 2 of Could section - -**Iteration 6** Build a web app to integrate with the bids API, create user journeys that allow users to find, add and update bid content - -------------- **Note:** If this is part of your training, you should look for guidance by your mentor of how to progress this project. Your coach can use the [generic project rest API doc](/training/generic-projects/rest-api/README.md) to setup your initial project that will cover iteration 1. diff --git a/api/models/bid_models.py b/api/models/bid_models.py index 5c010e4..96d3a66 100644 --- a/api/models/bid_models.py +++ b/api/models/bid_models.py @@ -1,11 +1,11 @@ from flask import request, jsonify -from api.schemas.bid_schema import BidSchema, PhaseInfo +from api.schemas.bid_schema import BidSchema, Status def get_test(): return 'test', 200 def create_bid(): - mandatory_fields = ['tender', 'client', 'bid_date', 'status'] + mandatory_fields = ['tender', 'client', 'bid_date'] if not request.is_json: return jsonify({'error': 'Invalid JSON'}), 400 @@ -20,14 +20,15 @@ def create_bid(): alias=request.json.get('alias', ''), bid_date=request.json['bid_date'], bid_folder_url=request.json.get('bid_folder_url', ''), - status='in-progress', - - failed=request.json.get('failed', {}), - feedback=request.json.get('feedback', {}) + feedback_description=request.json.get('feedback_description', ''), + feedback_url=request.json.get('feedback_url', '') ) - success_phase = PhaseInfo() - bid_schema.success.append(success_phase(1, True, 2, 3)) - bid_schema.was_successful = bid_schema.failed == {} + # Append phase information to the success list + bid_schema.addSuccessPhase(phase=2, has_score=True, score=80, out_of=100) + # Set failed phase info + # bid_schema.setFailedPhase(phase=3, has_score=True, score=50, out_of=100) + # Change status + # bid_schema.setStatus('deleted') # Convert the mock BidSchema object to a dictionary bid_json = bid_schema.toDbCollection() diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py index 63328ac..1da4f83 100644 --- a/api/schemas/bid_schema.py +++ b/api/schemas/bid_schema.py @@ -1,47 +1,45 @@ from uuid import uuid4 from datetime import datetime +from .phase_schema import PhaseInfo +from .status_schema import Status # Description: Schema for the bid object class BidSchema: - def __init__(self, tender, client, bid_date, alias='', bid_folder_url='', status='in-progress', was_successful=False, failed=None, feedback=None): + def __init__(self, tender, client, bid_date, alias='', bid_folder_url='', status='in_progress', was_successful=True,success=[], failed={}, feedback_description=None, feedback_url=None): self.id = uuid4() self.tender = tender self.client = client self.alias = alias self.bid_date = datetime.strptime(bid_date, '%d-%m-%Y').isoformat() # DD-MM-YYYY self.bid_folder_url = bid_folder_url - self.status = status # enum: "deleted", "in-progress" or "completed" + self.status = status # enum: "deleted", "in_progress" or "completed" self.links = { 'self': f"/bids/{self.id}", 'questions': f"/bids/{self.id}/questions" } self.was_successful = was_successful - self.success = [] + self.success = success self.failed = failed - self.feedback = feedback + self.feedback = {"description": feedback_description, + "url": feedback_url} self.last_updated = datetime.now().isoformat() + def addSuccessPhase(self, phase, has_score, score=None, out_of=None): + phase_info = PhaseInfo(phase=phase, has_score=has_score, score=score, out_of=out_of) + self.success.append(phase_info) + + def setFailedPhase(self, phase, has_score, score=None, out_of=None): + self.was_successful = False + self.failed = PhaseInfo(phase=phase, has_score=has_score, score=score, out_of=out_of) + + def setStatus(self, status): + if hasattr(Status, status): + self.status = status + else: + raise ValueError("Invalid status. Please provide a valid Status enum value") + def toDbCollection(self): - return { - "id": self.id, - "tender": self.tender, - "client": self.client, - "alias": self.alias, - "bid_date": self.bid_date, - "bid_folder_url": self.bid_folder_url, - "status": self.status, - "links": self.links, - "was_successful": self.was_successful, - "success": [s.__dict__ for s in self.success] if self.success else None, - "failed": self.failed, - "feedback": self.feedback, - "last_updated": self.last_updated - } - -# Schema for phaseInfo object -class PhaseInfo: - def __init__(self, phase, has_score, score=None, out_of=None): - self.phase = phase - self.has_score = has_score - self.score = score - self.out_of = out_of \ No newline at end of file + self.success = [s.__dict__ for s in self.success] if self.success else [] + self.failed = self.failed.__dict__ if self.failed else {} + return self.__dict__ + diff --git a/api/schemas/phase_schema.py b/api/schemas/phase_schema.py new file mode 100644 index 0000000..0e8d842 --- /dev/null +++ b/api/schemas/phase_schema.py @@ -0,0 +1,7 @@ +# Schema for phaseInfo object +class PhaseInfo: + def __init__(self, phase, has_score, score=None, out_of=None): + self.phase = phase + self.has_score = has_score + self.score = score + self.out_of = out_of \ No newline at end of file diff --git a/api/schemas/status_schema.py b/api/schemas/status_schema.py new file mode 100644 index 0000000..944d3d9 --- /dev/null +++ b/api/schemas/status_schema.py @@ -0,0 +1,7 @@ +from enum import Enum + +# Enum for status +class Status(Enum): + deleted = 1 + in_progress = 2 + completed = 3 \ No newline at end of file diff --git a/example.http b/example.http index dae357c..82a2fd9 100644 --- a/example.http +++ b/example.http @@ -6,10 +6,7 @@ Content-Type: application/json "client": "test_client", "alias": "test_alias", "bid_date": "21-06-2023", - "bid_folder_url": "test_folder_url", - "status": "in-progress", - "feedback": { - "description": "test_description", - "url": "test_url" - } + "bid_folder_url": "test_folder_url", + "feedback_description": "test_description", + "feedback_url": "test_url" } \ No newline at end of file diff --git a/test.txt b/test.txt index e69de29..d4f08e2 100644 --- a/test.txt +++ b/test.txt @@ -0,0 +1 @@ +{'id': UUID('693b5b7a-bdf2-45cc-ad74-754e8b5f9d4c'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/693b5b7a-bdf2-45cc-ad74-754e8b5f9d4c', 'questions': '/bids/693b5b7a-bdf2-45cc-ad74-754e8b5f9d4c/questions'}, 'was_successful': False, 'success': None, 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:15:27.454791'}{'id': UUID('b0de2267-cdaa-4d69-9ee4-6a6d84f71dca'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/b0de2267-cdaa-4d69-9ee4-6a6d84f71dca', 'questions': '/bids/b0de2267-cdaa-4d69-9ee4-6a6d84f71dca/questions'}, 'was_successful': True, 'success': None, 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:16:27.956614'}{'id': UUID('1cb57ab4-2127-4e52-a421-b6216c6547b7'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/1cb57ab4-2127-4e52-a421-b6216c6547b7', 'questions': '/bids/1cb57ab4-2127-4e52-a421-b6216c6547b7/questions'}, 'was_successful': [], 'success': None, 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:17:33.222148'}{'id': UUID('db3a7156-838f-4731-a6f0-fa055c8454e8'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/db3a7156-838f-4731-a6f0-fa055c8454e8', 'questions': '/bids/db3a7156-838f-4731-a6f0-fa055c8454e8/questions'}, 'was_successful': False, 'success': None, 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:18:03.508194'}{'id': UUID('5cffaafd-8118-4f78-b40c-1663005961bb'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/5cffaafd-8118-4f78-b40c-1663005961bb', 'questions': '/bids/5cffaafd-8118-4f78-b40c-1663005961bb/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:29:58.854984'}{'id': UUID('d2f8c326-2cdf-43c6-bb96-839b7f4e788d'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/d2f8c326-2cdf-43c6-bb96-839b7f4e788d', 'questions': '/bids/d2f8c326-2cdf-43c6-bb96-839b7f4e788d/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:33:26.672083'}{'id': UUID('61e25771-b189-4c72-8932-ea3dd1ade393'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/61e25771-b189-4c72-8932-ea3dd1ade393', 'questions': '/bids/61e25771-b189-4c72-8932-ea3dd1ade393/questions'}, 'was_successful': False, 'success': None, 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:34:03.828679'}{'id': UUID('4befe258-4a25-4617-82b5-7896eed5dd6c'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/4befe258-4a25-4617-82b5-7896eed5dd6c', 'questions': '/bids/4befe258-4a25-4617-82b5-7896eed5dd6c/questions'}, 'was_successful': False, 'success': [], 'failed': , 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:37:45.578632'}{'id': UUID('e9b12c46-0b51-4cb3-8cd4-f1ab5e8555f6'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/e9b12c46-0b51-4cb3-8cd4-f1ab5e8555f6', 'questions': '/bids/e9b12c46-0b51-4cb3-8cd4-f1ab5e8555f6/questions'}, 'was_successful': False, 'success': [], 'failed': , 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:38:31.898597'}{'id': UUID('a83bf859-0e85-4542-a2ef-9e669ad58f9a'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/a83bf859-0e85-4542-a2ef-9e669ad58f9a', 'questions': '/bids/a83bf859-0e85-4542-a2ef-9e669ad58f9a/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': , 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:39:35.260041'}{'id': UUID('7b9582dd-8995-4714-b820-6949375e8e52'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/7b9582dd-8995-4714-b820-6949375e8e52', 'questions': '/bids/7b9582dd-8995-4714-b820-6949375e8e52/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:41:13.240111'}{'id': UUID('564bdc5e-0ff7-4675-8eb3-5bea21e657c1'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/564bdc5e-0ff7-4675-8eb3-5bea21e657c1', 'questions': '/bids/564bdc5e-0ff7-4675-8eb3-5bea21e657c1/questions'}, 'was_successful': False, 'success': [], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:49:36.904155'}{'id': UUID('6bd0f136-cbb7-41f3-8a8d-ba11012fed54'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/6bd0f136-cbb7-41f3-8a8d-ba11012fed54', 'questions': '/bids/6bd0f136-cbb7-41f3-8a8d-ba11012fed54/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:51:48.289708'}{'id': UUID('fff0236c-6f7e-4776-9854-57fd23a7eea5'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/fff0236c-6f7e-4776-9854-57fd23a7eea5', 'questions': '/bids/fff0236c-6f7e-4776-9854-57fd23a7eea5/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': None, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:52:22.888148'}{'id': UUID('994ed36c-f676-4164-9317-a0734b60b2aa'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/994ed36c-f676-4164-9317-a0734b60b2aa', 'questions': '/bids/994ed36c-f676-4164-9317-a0734b60b2aa/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:53:20.237606'}{'id': UUID('2b7ec5f5-36fa-4b9c-8304-e60d72127173'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/2b7ec5f5-36fa-4b9c-8304-e60d72127173', 'questions': '/bids/2b7ec5f5-36fa-4b9c-8304-e60d72127173/questions'}, 'was_successful': [], 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:54:20.417266'}{'id': UUID('945bb74e-09fa-4cc4-ab5b-5fe6e24c471b'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/945bb74e-09fa-4cc4-ab5b-5fe6e24c471b', 'questions': '/bids/945bb74e-09fa-4cc4-ab5b-5fe6e24c471b/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:54:37.710712'}{'id': UUID('d0689c36-9f1b-40f4-8d25-8ae2313aafc0'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/d0689c36-9f1b-40f4-8d25-8ae2313aafc0', 'questions': '/bids/d0689c36-9f1b-40f4-8d25-8ae2313aafc0/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:55:04.646678'}{'id': UUID('7e6a84a7-ff32-4718-85c9-a79d67d1089e'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/7e6a84a7-ff32-4718-85c9-a79d67d1089e', 'questions': '/bids/7e6a84a7-ff32-4718-85c9-a79d67d1089e/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': None, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:55:51.634078'}{'id': UUID('73a2a70b-70c9-4f25-93cc-6d9008d8c6ac'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/73a2a70b-70c9-4f25-93cc-6d9008d8c6ac', 'questions': '/bids/73a2a70b-70c9-4f25-93cc-6d9008d8c6ac/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': None, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:57:16.032048'}{'id': UUID('c8f9c0f5-26d8-493b-9572-7a814fde0bf6'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/c8f9c0f5-26d8-493b-9572-7a814fde0bf6', 'questions': '/bids/c8f9c0f5-26d8-493b-9572-7a814fde0bf6/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': None, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:59:25.691014'}{'id': UUID('a85ba4c1-c143-4f20-ba54-979f351791f3'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/a85ba4c1-c143-4f20-ba54-979f351791f3', 'questions': '/bids/a85ba4c1-c143-4f20-ba54-979f351791f3/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:59:44.518952'}{'id': UUID('f2f36ded-5613-45ac-85f3-62c3ace988b0'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/f2f36ded-5613-45ac-85f3-62c3ace988b0', 'questions': '/bids/f2f36ded-5613-45ac-85f3-62c3ace988b0/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T15:02:08.221305'}{'id': UUID('b4331526-bfa4-492e-8f74-f48085c5fc3d'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/b4331526-bfa4-492e-8f74-f48085c5fc3d', 'questions': '/bids/b4331526-bfa4-492e-8f74-f48085c5fc3d/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T15:13:57.612374'}{'id': UUID('b7611d33-ff77-4dbf-b2ff-7b2c1a593b6d'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'whatever i want', 'links': {'self': '/bids/b7611d33-ff77-4dbf-b2ff-7b2c1a593b6d', 'questions': '/bids/b7611d33-ff77-4dbf-b2ff-7b2c1a593b6d/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T15:23:50.300674'}{'id': UUID('db4c2fd8-040d-4b5d-92a7-2cfc7b45ee1f'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'DELETED', 'links': {'self': '/bids/db4c2fd8-040d-4b5d-92a7-2cfc7b45ee1f', 'questions': '/bids/db4c2fd8-040d-4b5d-92a7-2cfc7b45ee1f/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T15:51:39.455359'}{'id': UUID('09297307-f699-4602-b409-cde1d3f3e4ee'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'deleted', 'links': {'self': '/bids/09297307-f699-4602-b409-cde1d3f3e4ee', 'questions': '/bids/09297307-f699-4602-b409-cde1d3f3e4ee/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T15:55:02.238493'}{'id': UUID('196d2d56-d8a7-4440-8b02-b7011df7772d'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/196d2d56-d8a7-4440-8b02-b7011df7772d', 'questions': '/bids/196d2d56-d8a7-4440-8b02-b7011df7772d/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T15:58:52.828606'}{'id': UUID('9ae6a39b-fd14-4999-9842-59e82a13b4d6'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/9ae6a39b-fd14-4999-9842-59e82a13b4d6', 'questions': '/bids/9ae6a39b-fd14-4999-9842-59e82a13b4d6/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': None, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T15:59:10.101877'}{'id': UUID('fbc57553-e818-4c05-ae02-1f7f9a5a0034'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/fbc57553-e818-4c05-ae02-1f7f9a5a0034', 'questions': '/bids/fbc57553-e818-4c05-ae02-1f7f9a5a0034/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': None, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T16:00:05.556958'}{'id': UUID('0af62297-15f1-4155-a2e5-b202fd390ead'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/0af62297-15f1-4155-a2e5-b202fd390ead', 'questions': '/bids/0af62297-15f1-4155-a2e5-b202fd390ead/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': None, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T16:01:51.681482'}{'id': UUID('4ad0c0fb-7602-40e0-8666-c87dcea0d4e4'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/4ad0c0fb-7602-40e0-8666-c87dcea0d4e4', 'questions': '/bids/4ad0c0fb-7602-40e0-8666-c87dcea0d4e4/questions'}, 'was_successful': False, 'success': None, 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T16:02:35.963210'}{'id': UUID('61e120ad-e5bb-429d-9265-452edcfdb8eb'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/61e120ad-e5bb-429d-9265-452edcfdb8eb', 'questions': '/bids/61e120ad-e5bb-429d-9265-452edcfdb8eb/questions'}, 'was_successful': False, 'success': [], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T16:03:21.618859'}{'id': UUID('52684de8-ac8f-42d9-973f-5ca673acb3ea'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/52684de8-ac8f-42d9-973f-5ca673acb3ea', 'questions': '/bids/52684de8-ac8f-42d9-973f-5ca673acb3ea/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T16:04:34.858039'}{'id': UUID('d1eeba37-2858-4dff-b363-f77a3d752d78'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/d1eeba37-2858-4dff-b363-f77a3d752d78', 'questions': '/bids/d1eeba37-2858-4dff-b363-f77a3d752d78/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T16:11:29.602248'}{'id': UUID('8cf95f0d-ce27-40a2-aae4-76c163582a59'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/8cf95f0d-ce27-40a2-aae4-76c163582a59', 'questions': '/bids/8cf95f0d-ce27-40a2-aae4-76c163582a59/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T16:15:55.130771'} \ No newline at end of file From 3d0dc1cebf51ba34a54980d7b3b4c3bf8b26f562 Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 22 Jun 2023 17:20:46 +0100 Subject: [PATCH 09/97] test: add post bid route tests --- test.txt | 1 - tests/test_bid.py | 56 ++++++++++++++++++++++++++++++++++++++- tests/test_hello_world.py | 22 --------------- 3 files changed, 55 insertions(+), 24 deletions(-) delete mode 100644 tests/test_hello_world.py diff --git a/test.txt b/test.txt index d4f08e2..e69de29 100644 --- a/test.txt +++ b/test.txt @@ -1 +0,0 @@ -{'id': UUID('693b5b7a-bdf2-45cc-ad74-754e8b5f9d4c'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/693b5b7a-bdf2-45cc-ad74-754e8b5f9d4c', 'questions': '/bids/693b5b7a-bdf2-45cc-ad74-754e8b5f9d4c/questions'}, 'was_successful': False, 'success': None, 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:15:27.454791'}{'id': UUID('b0de2267-cdaa-4d69-9ee4-6a6d84f71dca'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/b0de2267-cdaa-4d69-9ee4-6a6d84f71dca', 'questions': '/bids/b0de2267-cdaa-4d69-9ee4-6a6d84f71dca/questions'}, 'was_successful': True, 'success': None, 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:16:27.956614'}{'id': UUID('1cb57ab4-2127-4e52-a421-b6216c6547b7'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/1cb57ab4-2127-4e52-a421-b6216c6547b7', 'questions': '/bids/1cb57ab4-2127-4e52-a421-b6216c6547b7/questions'}, 'was_successful': [], 'success': None, 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:17:33.222148'}{'id': UUID('db3a7156-838f-4731-a6f0-fa055c8454e8'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/db3a7156-838f-4731-a6f0-fa055c8454e8', 'questions': '/bids/db3a7156-838f-4731-a6f0-fa055c8454e8/questions'}, 'was_successful': False, 'success': None, 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:18:03.508194'}{'id': UUID('5cffaafd-8118-4f78-b40c-1663005961bb'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/5cffaafd-8118-4f78-b40c-1663005961bb', 'questions': '/bids/5cffaafd-8118-4f78-b40c-1663005961bb/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:29:58.854984'}{'id': UUID('d2f8c326-2cdf-43c6-bb96-839b7f4e788d'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/d2f8c326-2cdf-43c6-bb96-839b7f4e788d', 'questions': '/bids/d2f8c326-2cdf-43c6-bb96-839b7f4e788d/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:33:26.672083'}{'id': UUID('61e25771-b189-4c72-8932-ea3dd1ade393'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/61e25771-b189-4c72-8932-ea3dd1ade393', 'questions': '/bids/61e25771-b189-4c72-8932-ea3dd1ade393/questions'}, 'was_successful': False, 'success': None, 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:34:03.828679'}{'id': UUID('4befe258-4a25-4617-82b5-7896eed5dd6c'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/4befe258-4a25-4617-82b5-7896eed5dd6c', 'questions': '/bids/4befe258-4a25-4617-82b5-7896eed5dd6c/questions'}, 'was_successful': False, 'success': [], 'failed': , 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:37:45.578632'}{'id': UUID('e9b12c46-0b51-4cb3-8cd4-f1ab5e8555f6'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/e9b12c46-0b51-4cb3-8cd4-f1ab5e8555f6', 'questions': '/bids/e9b12c46-0b51-4cb3-8cd4-f1ab5e8555f6/questions'}, 'was_successful': False, 'success': [], 'failed': , 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:38:31.898597'}{'id': UUID('a83bf859-0e85-4542-a2ef-9e669ad58f9a'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/a83bf859-0e85-4542-a2ef-9e669ad58f9a', 'questions': '/bids/a83bf859-0e85-4542-a2ef-9e669ad58f9a/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': , 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:39:35.260041'}{'id': UUID('7b9582dd-8995-4714-b820-6949375e8e52'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/7b9582dd-8995-4714-b820-6949375e8e52', 'questions': '/bids/7b9582dd-8995-4714-b820-6949375e8e52/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:41:13.240111'}{'id': UUID('564bdc5e-0ff7-4675-8eb3-5bea21e657c1'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/564bdc5e-0ff7-4675-8eb3-5bea21e657c1', 'questions': '/bids/564bdc5e-0ff7-4675-8eb3-5bea21e657c1/questions'}, 'was_successful': False, 'success': [], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:49:36.904155'}{'id': UUID('6bd0f136-cbb7-41f3-8a8d-ba11012fed54'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/6bd0f136-cbb7-41f3-8a8d-ba11012fed54', 'questions': '/bids/6bd0f136-cbb7-41f3-8a8d-ba11012fed54/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:51:48.289708'}{'id': UUID('fff0236c-6f7e-4776-9854-57fd23a7eea5'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/fff0236c-6f7e-4776-9854-57fd23a7eea5', 'questions': '/bids/fff0236c-6f7e-4776-9854-57fd23a7eea5/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': None, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:52:22.888148'}{'id': UUID('994ed36c-f676-4164-9317-a0734b60b2aa'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/994ed36c-f676-4164-9317-a0734b60b2aa', 'questions': '/bids/994ed36c-f676-4164-9317-a0734b60b2aa/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:53:20.237606'}{'id': UUID('2b7ec5f5-36fa-4b9c-8304-e60d72127173'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/2b7ec5f5-36fa-4b9c-8304-e60d72127173', 'questions': '/bids/2b7ec5f5-36fa-4b9c-8304-e60d72127173/questions'}, 'was_successful': [], 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:54:20.417266'}{'id': UUID('945bb74e-09fa-4cc4-ab5b-5fe6e24c471b'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/945bb74e-09fa-4cc4-ab5b-5fe6e24c471b', 'questions': '/bids/945bb74e-09fa-4cc4-ab5b-5fe6e24c471b/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:54:37.710712'}{'id': UUID('d0689c36-9f1b-40f4-8d25-8ae2313aafc0'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/d0689c36-9f1b-40f4-8d25-8ae2313aafc0', 'questions': '/bids/d0689c36-9f1b-40f4-8d25-8ae2313aafc0/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:55:04.646678'}{'id': UUID('7e6a84a7-ff32-4718-85c9-a79d67d1089e'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/7e6a84a7-ff32-4718-85c9-a79d67d1089e', 'questions': '/bids/7e6a84a7-ff32-4718-85c9-a79d67d1089e/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': None, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:55:51.634078'}{'id': UUID('73a2a70b-70c9-4f25-93cc-6d9008d8c6ac'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/73a2a70b-70c9-4f25-93cc-6d9008d8c6ac', 'questions': '/bids/73a2a70b-70c9-4f25-93cc-6d9008d8c6ac/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': None, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:57:16.032048'}{'id': UUID('c8f9c0f5-26d8-493b-9572-7a814fde0bf6'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/c8f9c0f5-26d8-493b-9572-7a814fde0bf6', 'questions': '/bids/c8f9c0f5-26d8-493b-9572-7a814fde0bf6/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': None, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:59:25.691014'}{'id': UUID('a85ba4c1-c143-4f20-ba54-979f351791f3'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/a85ba4c1-c143-4f20-ba54-979f351791f3', 'questions': '/bids/a85ba4c1-c143-4f20-ba54-979f351791f3/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T14:59:44.518952'}{'id': UUID('f2f36ded-5613-45ac-85f3-62c3ace988b0'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/f2f36ded-5613-45ac-85f3-62c3ace988b0', 'questions': '/bids/f2f36ded-5613-45ac-85f3-62c3ace988b0/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T15:02:08.221305'}{'id': UUID('b4331526-bfa4-492e-8f74-f48085c5fc3d'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in-progress', 'links': {'self': '/bids/b4331526-bfa4-492e-8f74-f48085c5fc3d', 'questions': '/bids/b4331526-bfa4-492e-8f74-f48085c5fc3d/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T15:13:57.612374'}{'id': UUID('b7611d33-ff77-4dbf-b2ff-7b2c1a593b6d'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'whatever i want', 'links': {'self': '/bids/b7611d33-ff77-4dbf-b2ff-7b2c1a593b6d', 'questions': '/bids/b7611d33-ff77-4dbf-b2ff-7b2c1a593b6d/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T15:23:50.300674'}{'id': UUID('db4c2fd8-040d-4b5d-92a7-2cfc7b45ee1f'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'DELETED', 'links': {'self': '/bids/db4c2fd8-040d-4b5d-92a7-2cfc7b45ee1f', 'questions': '/bids/db4c2fd8-040d-4b5d-92a7-2cfc7b45ee1f/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T15:51:39.455359'}{'id': UUID('09297307-f699-4602-b409-cde1d3f3e4ee'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'deleted', 'links': {'self': '/bids/09297307-f699-4602-b409-cde1d3f3e4ee', 'questions': '/bids/09297307-f699-4602-b409-cde1d3f3e4ee/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T15:55:02.238493'}{'id': UUID('196d2d56-d8a7-4440-8b02-b7011df7772d'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/196d2d56-d8a7-4440-8b02-b7011df7772d', 'questions': '/bids/196d2d56-d8a7-4440-8b02-b7011df7772d/questions'}, 'was_successful': False, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T15:58:52.828606'}{'id': UUID('9ae6a39b-fd14-4999-9842-59e82a13b4d6'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/9ae6a39b-fd14-4999-9842-59e82a13b4d6', 'questions': '/bids/9ae6a39b-fd14-4999-9842-59e82a13b4d6/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': None, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T15:59:10.101877'}{'id': UUID('fbc57553-e818-4c05-ae02-1f7f9a5a0034'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/fbc57553-e818-4c05-ae02-1f7f9a5a0034', 'questions': '/bids/fbc57553-e818-4c05-ae02-1f7f9a5a0034/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': None, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T16:00:05.556958'}{'id': UUID('0af62297-15f1-4155-a2e5-b202fd390ead'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/0af62297-15f1-4155-a2e5-b202fd390ead', 'questions': '/bids/0af62297-15f1-4155-a2e5-b202fd390ead/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': None, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T16:01:51.681482'}{'id': UUID('4ad0c0fb-7602-40e0-8666-c87dcea0d4e4'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/4ad0c0fb-7602-40e0-8666-c87dcea0d4e4', 'questions': '/bids/4ad0c0fb-7602-40e0-8666-c87dcea0d4e4/questions'}, 'was_successful': False, 'success': None, 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T16:02:35.963210'}{'id': UUID('61e120ad-e5bb-429d-9265-452edcfdb8eb'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/61e120ad-e5bb-429d-9265-452edcfdb8eb', 'questions': '/bids/61e120ad-e5bb-429d-9265-452edcfdb8eb/questions'}, 'was_successful': False, 'success': [], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T16:03:21.618859'}{'id': UUID('52684de8-ac8f-42d9-973f-5ca673acb3ea'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/52684de8-ac8f-42d9-973f-5ca673acb3ea', 'questions': '/bids/52684de8-ac8f-42d9-973f-5ca673acb3ea/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T16:04:34.858039'}{'id': UUID('d1eeba37-2858-4dff-b363-f77a3d752d78'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/d1eeba37-2858-4dff-b363-f77a3d752d78', 'questions': '/bids/d1eeba37-2858-4dff-b363-f77a3d752d78/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T16:11:29.602248'}{'id': UUID('8cf95f0d-ce27-40a2-aae4-76c163582a59'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/8cf95f0d-ce27-40a2-aae4-76c163582a59', 'questions': '/bids/8cf95f0d-ce27-40a2-aae4-76c163582a59/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-22T16:15:55.130771'} \ No newline at end of file diff --git a/tests/test_bid.py b/tests/test_bid.py index e2fd963..4fa45ae 100644 --- a/tests/test_bid.py +++ b/tests/test_bid.py @@ -1 +1,55 @@ -#pass \ No newline at end of file +from flask import Flask +from datetime import datetime +import pytest + +from api.routes.bid_route import bid + + +@pytest.fixture +def client(): + app = Flask(__name__) + app.register_blueprint(bid, url_prefix='/api') + with app.test_client() as client: + yield client + +# Case 1: Valid data +def test_post_is_valid(client): + data = { + "tender": "Sample Tender", + "client": "Sample Client", + "bid_date": "20-06-2023", + "alias": "Sample Alias", + "bid_folder_url": "https://example.com/bid", + "feedback":{ + "feedback_description": "Sample feedback", + "feedback_url": "https://example.com/feedback" + } + } + response = client.post("api/bids", json=data) + assert response.status_code == 201 + assert response.get_json() is not None + assert response.get_json().get("id") is not None + assert response.get_json().get("tender") == data.get("tender") + assert response.get_json().get("client") == data.get("client") + assert response.get_json().get("bid_date") == datetime.strptime(data.get("bid_date"), "%d-%m-%Y").isoformat() + assert response.get_json().get("alias") == data.get("alias") + assert response.get_json().get("bid_folder_url") == data.get("bid_folder_url") + assert response.get_json().get("feedback") is not None + assert response.get_json().get("feedback_description") == data.get("feedback_description") + assert response.get_json().get("feedback_url") == data.get("feedback_url") + +# Case 2: Missing mandatory fields +def test_field_missing(client): + data = { + "client": "Sample Client", + "bid_date": "20-06-2023" + } + response = client.post("api/bids", json=data) + assert response.status_code == 400 + assert response.get_json().get("error") == "Missing mandatory field: tender" + +# Case 3: Invalid JSON +def test_post_is_invalid(client): + response = client.post("api/bids", data="Invalid JSON") + assert response.status_code == 400 + assert response.get_json().get("error") == "Invalid JSON" \ No newline at end of file diff --git a/tests/test_hello_world.py b/tests/test_hello_world.py deleted file mode 100644 index 0258a1c..0000000 --- a/tests/test_hello_world.py +++ /dev/null @@ -1,22 +0,0 @@ -from flask import Flask -import pytest - -from controllers.hello_controller import hello_world - -from routes.hello_route import hello - -def test_hello_world(): - result = hello_world() - assert result == 'Hello, World!' - -@pytest.fixture -def client(): - app = Flask(__name__) - app.register_blueprint(hello, url_prefix='/api') - with app.test_client() as client: - yield client - -def test_hello_world_route(client): - response = client.get('/api/helloworld') - assert response.status_code == 200 - assert response.data.decode('utf-8') == 'Hello, World!' \ No newline at end of file From 2d6d8cfc36f4c66fef037ae3993faaa4080a81ff Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 23 Jun 2023 11:20:19 +0100 Subject: [PATCH 10/97] fix: renamed files and folders for readability --- api/{routes => controllers}/__init__.py | 0 api/{routes/bid_route.py => controllers/bid_controller.py} | 0 api/models/bid_models.py | 2 +- app.py | 2 +- db.txt | 1 + test.txt | 0 example.http => test_request.http | 0 tests/test_bid.py | 2 +- 8 files changed, 4 insertions(+), 3 deletions(-) rename api/{routes => controllers}/__init__.py (100%) rename api/{routes/bid_route.py => controllers/bid_controller.py} (100%) create mode 100644 db.txt delete mode 100644 test.txt rename example.http => test_request.http (100%) diff --git a/api/routes/__init__.py b/api/controllers/__init__.py similarity index 100% rename from api/routes/__init__.py rename to api/controllers/__init__.py diff --git a/api/routes/bid_route.py b/api/controllers/bid_controller.py similarity index 100% rename from api/routes/bid_route.py rename to api/controllers/bid_controller.py diff --git a/api/models/bid_models.py b/api/models/bid_models.py index 96d3a66..c9c8a9a 100644 --- a/api/models/bid_models.py +++ b/api/models/bid_models.py @@ -33,7 +33,7 @@ def create_bid(): bid_json = bid_schema.toDbCollection() # Save data in memory - f=open('./test.txt','a') + f=open('./db.txt','a') f.write(str(bid_json)) f.close diff --git a/app.py b/app.py index bc25825..dcde602 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,6 @@ from flask import Flask -from api.routes.bid_route import bid +from api.controllers.bid_controller import bid app = Flask(__name__) diff --git a/db.txt b/db.txt new file mode 100644 index 0000000..8f32986 --- /dev/null +++ b/db.txt @@ -0,0 +1 @@ +{'id': UUID('471fea1f-705c-4851-9a5b-df7bc2651428'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428', 'questions': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:16:59.404946'}{'id': UUID('0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8', 'questions': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:17:23.593171'} \ No newline at end of file diff --git a/test.txt b/test.txt deleted file mode 100644 index e69de29..0000000 diff --git a/example.http b/test_request.http similarity index 100% rename from example.http rename to test_request.http diff --git a/tests/test_bid.py b/tests/test_bid.py index 4fa45ae..ae0802e 100644 --- a/tests/test_bid.py +++ b/tests/test_bid.py @@ -2,7 +2,7 @@ from datetime import datetime import pytest -from api.routes.bid_route import bid +from api.controllers.bid_controller import bid @pytest.fixture From 027cc43d4585f4b5fe3fcd221d3d57bd98280f96 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 23 Jun 2023 15:59:59 +0100 Subject: [PATCH 11/97] fix: changed set_status --- api/models/bid_models.py | 2 +- api/schemas/bid_schema.py | 4 ++-- api/schemas/status_schema.py | 6 +++--- db.txt | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api/models/bid_models.py b/api/models/bid_models.py index c9c8a9a..7afbf35 100644 --- a/api/models/bid_models.py +++ b/api/models/bid_models.py @@ -28,7 +28,7 @@ def create_bid(): # Set failed phase info # bid_schema.setFailedPhase(phase=3, has_score=True, score=50, out_of=100) # Change status - # bid_schema.setStatus('deleted') + bid_schema.setStatus(Status.DELETED) # Convert the mock BidSchema object to a dictionary bid_json = bid_schema.toDbCollection() diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py index 1da4f83..d5558f5 100644 --- a/api/schemas/bid_schema.py +++ b/api/schemas/bid_schema.py @@ -33,8 +33,8 @@ def setFailedPhase(self, phase, has_score, score=None, out_of=None): self.failed = PhaseInfo(phase=phase, has_score=has_score, score=score, out_of=out_of) def setStatus(self, status): - if hasattr(Status, status): - self.status = status + if isinstance(status, Status): + self.status = status.name.lower() else: raise ValueError("Invalid status. Please provide a valid Status enum value") diff --git a/api/schemas/status_schema.py b/api/schemas/status_schema.py index 944d3d9..90437e7 100644 --- a/api/schemas/status_schema.py +++ b/api/schemas/status_schema.py @@ -2,6 +2,6 @@ # Enum for status class Status(Enum): - deleted = 1 - in_progress = 2 - completed = 3 \ No newline at end of file + DELETED = 1 + IN_PROGRESS = 2 + COMPLETED = 3 \ No newline at end of file diff --git a/db.txt b/db.txt index 8f32986..99bd9ac 100644 --- a/db.txt +++ b/db.txt @@ -1 +1 @@ -{'id': UUID('471fea1f-705c-4851-9a5b-df7bc2651428'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428', 'questions': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:16:59.404946'}{'id': UUID('0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8', 'questions': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:17:23.593171'} \ No newline at end of file +{'id': UUID('471fea1f-705c-4851-9a5b-df7bc2651428'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428', 'questions': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:16:59.404946'}{'id': UUID('0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8', 'questions': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:17:23.593171'}{'id': UUID('8fed99f8-3b7c-42ef-8c1f-fb3fe25ec039'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/8fed99f8-3b7c-42ef-8c1f-fb3fe25ec039', 'questions': '/bids/8fed99f8-3b7c-42ef-8c1f-fb3fe25ec039/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T15:57:34.571615'}{'id': UUID('96d69775-29af-46b1-aaf4-bfbdb1543412'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'deleted', 'links': {'self': '/bids/96d69775-29af-46b1-aaf4-bfbdb1543412', 'questions': '/bids/96d69775-29af-46b1-aaf4-bfbdb1543412/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T15:57:50.238595'} \ No newline at end of file From 817cd3d14263ac85054b3546d26fa709721ee5c8 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 23 Jun 2023 16:54:08 +0100 Subject: [PATCH 12/97] wip: swagger spec config --- api/controllers/bid_controller.py | 2 + app.py | 2 + db.txt | 2 +- swagger_config.yaml | 144 ++++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 swagger_config.yaml diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index d2c60fb..ac05fb0 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -1,5 +1,6 @@ from flask import Blueprint +from flasgger import swag_from from api.models.bid_models import get_test, create_bid bid = Blueprint('bid', __name__) @@ -10,6 +11,7 @@ def get_tested(): return response @bid.route("/bids", methods=["POST"]) +@swag_from('../../swagger_config.yaml') def post_bid(): response = create_bid() return response diff --git a/app.py b/app.py index dcde602..9745815 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,10 @@ from flask import Flask +from flasgger import Swagger from api.controllers.bid_controller import bid app = Flask(__name__) +swagger = Swagger(app) app.register_blueprint(bid, url_prefix='/api') diff --git a/db.txt b/db.txt index 8f32986..ba26722 100644 --- a/db.txt +++ b/db.txt @@ -1 +1 @@ -{'id': UUID('471fea1f-705c-4851-9a5b-df7bc2651428'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428', 'questions': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:16:59.404946'}{'id': UUID('0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8', 'questions': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:17:23.593171'} \ No newline at end of file +{'id': UUID('471fea1f-705c-4851-9a5b-df7bc2651428'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428', 'questions': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:16:59.404946'}{'id': UUID('0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8', 'questions': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:17:23.593171'}{'id': UUID('49636126-7c54-46b0-a3b0-e7abcc026c3a'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/49636126-7c54-46b0-a3b0-e7abcc026c3a', 'questions': '/bids/49636126-7c54-46b0-a3b0-e7abcc026c3a/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T16:49:49.949167'} \ No newline at end of file diff --git a/swagger_config.yaml b/swagger_config.yaml new file mode 100644 index 0000000..2c5a9f5 --- /dev/null +++ b/swagger_config.yaml @@ -0,0 +1,144 @@ +--- +openapi: 3.0.1 +info: + title: defaultTitle + description: defaultDescription + version: "0.1" +servers: +- url: http://127.0.0.1:3000 +paths: + /api/bids: + post: + description: Auto generated using Swagger Inspector + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/api_bids_body' + examples: + "0": + value: "{\n \"tender\": \"test_tender\",\n \"client\": \"test_client\",\n \"alias\": \"test_alias\",\n \"bid_date\": \"21-06-2023\",\n \"bid_folder_url\": \"test_folder_url\", \n \"feedback_description\": \"test_description\",\n \"feedback_url\": \"test_url\"\n}" + responses: + "201": + description: Auto generated using Swagger Inspector + content: + application/json: + schema: + $ref: '#/components/schemas/inline_response_201' + examples: + "0": + value: | + { + "alias": "test_alias", + "bid_date": "2023-06-21T00:00:00", + "bid_folder_url": "test_folder_url", + "client": "test_client", + "failed": {}, + "feedback": { + "description": "test_description", + "url": "test_url" + }, + "id": "bf1e3f96-fcb0-48d6-be3f-83de1a944be3", + "last_updated": "2023-06-23T14:01:26.906857", + "links": { + "questions": "/bids/bf1e3f96-fcb0-48d6-be3f-83de1a944be3/questions", + "self": "/bids/bf1e3f96-fcb0-48d6-be3f-83de1a944be3" + }, + "status": "in_progress", + "success": [ + { + "has_score": true, + "out_of": 100, + "phase": 2, + "score": 80 + }, + { + "has_score": true, + "out_of": 100, + "phase": 2, + "score": 80 + }, + { + "has_score": true, + "out_of": 100, + "phase": 2, + "score": 80 + } + ], + "tender": "test_tender", + "was_successful": true + } + servers: + - url: http://127.0.0.1:3000 + servers: + - url: http://127.0.0.1:3000 +components: + schemas: + api_bids_body: + type: object + properties: + tender: + type: string + bid_folder_url: + type: string + feedback_url: + type: string + feedback_description: + type: string + client: + type: string + alias: + type: string + bid_date: + type: string + inline_response_201: + type: object + properties: + tender: + type: string + last_updated: + type: string + failed: + type: object + properties: {} + feedback: + type: object + properties: + description: + type: string + url: + type: string + bid_folder_url: + type: string + success: + type: array + items: + type: object + properties: + phase: + type: integer + score: + type: integer + has_score: + type: boolean + out_of: + type: integer + alias: + type: string + client: + type: string + links: + type: object + properties: + questions: + type: string + self: + type: string + id: + type: string + bid_date: + type: string + status: + type: string + was_successful: + type: boolean From cc3763c25f30fc20870ade6ec92886e0c1fa1108 Mon Sep 17 00:00:00 2001 From: Nathan Shumoogum Date: Fri, 23 Jun 2023 16:56:18 +0100 Subject: [PATCH 13/97] docs: Update contribution guidelines to incl. release and tagging --- CONTRIBUTING.md | 45 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fd6855d..3f02b29 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,17 +1,18 @@ -Contributing guidelines -======================= +# Contributing guidelines -### Git workflow -* Use git-flow - create a feature branch from `develop`, e.g. `feature/new-feature` +## Git workflow + +* Use git-flow - create a feature branch from `develop`, e.g. `feat/new-feature` * Pull requests must contain a succinct, clear summary of what the user need is driving this feature change * Ensure your branch contains logical atomic commits before sending a pull request * You may rebase your branch after feedback if it's to include relevant updates from the develop branch. It is preferable to rebase here then a merge commit as a clean and straight history on develop with discrete merge commits for features is preferred * To find out more about contributing click [here](https://contributing.md/) -### Commit messages +## Commit messages + Please use the following format for commit messages: -``` +```textbox * fix: for a bug fix * feat: for a new feature * docs: for documentation changes @@ -22,9 +23,39 @@ Please use the following format for commit messages: * build: for changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) ``` -### Pull requests +## Pull requests + * Pull requests should be made to the `develop` branch * Pull requests should be made from a feature branch, not `develop` * Pull requests should be made with a succinct, clear summary of what the user need is driving this feature change * An automated template will be provided to help with this process, please fill it out as best you can +## Release branches and Tagging + +* Once a single or set of changes has been made that can be released, we create a release branch off develop + * Releases should be as small as possible so that it is easier to fix issues and rollback code in production environment without losing many features at once + * Release branches should be of the following structure: + + ```sh + release/0.1.0 + ``` + + * The release name should abide by [semantic versioning (semver)](https://semver.org/) +* Once release branch has been created, a pull request should be made against the `main` branch +* Once approved, the release branch can be merged locally :warning: **WARNING - Do not push to remote** :warning: +* Tag the release and push to remote - [see tagging](#tagging) + +## Tagging + +* Tag latest commit on branch, this should be the local merge commit + * Check latest local commit, run: `git show HEAD` + * Create tag, run: `tag -s -m ` + * where `tag` is the semver value (should be taken from the release branch name) and `` is a general, short and concise message of the change +* Push release merge and tag to remote, run: `git push --follow-tags` +* Once pushed, go to the Github releases page for the repository in question, which can be found by clicking on the <> Code tab and clicking on Releases on the right hand side: + * Select Tags + * Click on the v0.1.0 tag that you just created + * Choose Create release from tag + * Copy the release name and make human-friendly (capitalise, remove /) to create a release title (e.g. release/0.1.0 -> Release 0.1.0) + * Add relevant release notes, this can be expanded from initial message created against the tag. Advised to use bullet points to list out changes + * Hit Publish release From 34c1a455ce4b25ea206c1000a9d2eefc7548e013 Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 26 Jun 2023 15:34:07 +0100 Subject: [PATCH 14/97] fix: installed flask_swagger_ui; created swagger_config.yml --- api/controllers/bid_controller.py | 2 - app.py | 15 ++- db.txt | 2 +- static/swagger_config.yml | 178 ++++++++++++++++++++++++++++++ swagger_config.yaml | 144 ------------------------ 5 files changed, 192 insertions(+), 149 deletions(-) create mode 100644 static/swagger_config.yml delete mode 100644 swagger_config.yaml diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index ac05fb0..d2c60fb 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -1,6 +1,5 @@ from flask import Blueprint -from flasgger import swag_from from api.models.bid_models import get_test, create_bid bid = Blueprint('bid', __name__) @@ -11,7 +10,6 @@ def get_tested(): return response @bid.route("/bids", methods=["POST"]) -@swag_from('../../swagger_config.yaml') def post_bid(): response = create_bid() return response diff --git a/app.py b/app.py index 9745815..8622fdc 100644 --- a/app.py +++ b/app.py @@ -1,11 +1,22 @@ from flask import Flask -from flasgger import Swagger +from flask_swagger_ui import get_swaggerui_blueprint from api.controllers.bid_controller import bid app = Flask(__name__) -swagger = Swagger(app) +SWAGGER_URL = '/api/docs' # URL for exposing Swagger UI (without trailing '/') +API_URL = '/static/swagger_config.yml' # Our API url (can of course be a local resource) + +# Call factory function to create our blueprint +swaggerui_blueprint = get_swaggerui_blueprint( + SWAGGER_URL, # Swagger UI static files will be mapped to '{SWAGGER_URL}/dist/' + API_URL, + config={ # Swagger UI config overrides + 'app_name': "Test application" + }) + +app.register_blueprint(swaggerui_blueprint) app.register_blueprint(bid, url_prefix='/api') diff --git a/db.txt b/db.txt index ba26722..c8a63fc 100644 --- a/db.txt +++ b/db.txt @@ -1 +1 @@ -{'id': UUID('471fea1f-705c-4851-9a5b-df7bc2651428'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428', 'questions': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:16:59.404946'}{'id': UUID('0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8', 'questions': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:17:23.593171'}{'id': UUID('49636126-7c54-46b0-a3b0-e7abcc026c3a'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/49636126-7c54-46b0-a3b0-e7abcc026c3a', 'questions': '/bids/49636126-7c54-46b0-a3b0-e7abcc026c3a/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T16:49:49.949167'} \ No newline at end of file +{'id': UUID('471fea1f-705c-4851-9a5b-df7bc2651428'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428', 'questions': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:16:59.404946'}{'id': UUID('0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8', 'questions': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:17:23.593171'}{'id': UUID('49636126-7c54-46b0-a3b0-e7abcc026c3a'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/49636126-7c54-46b0-a3b0-e7abcc026c3a', 'questions': '/bids/49636126-7c54-46b0-a3b0-e7abcc026c3a/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T16:49:49.949167'}{'id': UUID('75af19f4-55ae-4ca7-88ec-7298a4b54d72'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/75af19f4-55ae-4ca7-88ec-7298a4b54d72', 'questions': '/bids/75af19f4-55ae-4ca7-88ec-7298a4b54d72/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-26T14:36:06.350990'}{'id': UUID('be613df5-7a23-4274-8551-ed27344a75ac'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/be613df5-7a23-4274-8551-ed27344a75ac', 'questions': '/bids/be613df5-7a23-4274-8551-ed27344a75ac/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-26T14:43:13.246088'}{'id': UUID('19f4936c-acd6-4966-ab01-0eec1897331c'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/19f4936c-acd6-4966-ab01-0eec1897331c', 'questions': '/bids/19f4936c-acd6-4966-ab01-0eec1897331c/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-26T14:45:10.965516'} \ No newline at end of file diff --git a/static/swagger_config.yml b/static/swagger_config.yml new file mode 100644 index 0000000..8c7638c --- /dev/null +++ b/static/swagger_config.yml @@ -0,0 +1,178 @@ +--- +openapi: 3.0.3 +# -------------------------------------------- +# Info +info: + title: "Bids API" + description: " + This is a API for the Bids API. + You can find out more about + this API at [GitHub](https://github.com/methods/tdse-accessForce-bids-api). + You can now help us improve the API whether it's by making changes to the definition itself or to the code. + That way, with time, we can improve the API in general, and expose some of the new features in OAS3. + Some useful links: + - [API repository](https://github.com/methods/tdse-accessForce-bids-api)" + version: "1.0.0" + termsOfService: "https://github.com/methods/tdse-accessForce-bids-api" + contact: + email: "accessForce@example.com" + license: + name: "MIT License" + url: "https://github.com/methods/tdse-accessForce-bids-api/blob/develop/LICENSE.md" +# -------------------------------------------- +# Server +servers: + - url: http://127.0.0.1:3000/api/ + description: "Local server" +# -------------------------------------------- +# Tags +tags: + - name: "bids" + description: "Everything about BIDS" + externalDocs: + description: Find out more + url: "example.com" + - name: "questions" + description: "Everything about QUESTIONS" + externalDocs: + description: "Find out more" + url: "example.com" +# -------------------------------------------- +paths: + /bids: + get: + tags: + - "bids" + summary: Returns a test string + description: Returns a test string + responses: + '200': # status code + description: OK + content: # Response body + text/plain: # Media type + schema: # Must-have + type: string # Data type + post: + tags: + - "bids" + summary: "Create a new bid" + description: "Create a new bid" + operationId: "post_bid" + requestBody: + description: Create a new bid in the store + content: + application/json: + schema: + $ref: '#/components/schemas/bid_body' + examples: + "201: Successful request": + value: "{\n \"tender\": \"test_tender\",\n \"client\": \"test_client\",\n \"alias\": \"test_alias\",\n \"bid_date\": \"21-06-2023\",\n \"bid_folder_url\": \"test_folder_url\", \n \"feedback_description\": \"test_description\",\n \"feedback_url\": \"test_url\"\n}" + "400: Missing mandatory field": + value: "{\n \"client\": \"test_clien2\",\n \"alias\": \"test_alias2\",\n \"bid_date\": \"21-06-2023\",\n \"bid_folder_url\": \"test_folder_url\", \n \"feedback_description\": \"test_description\",\n \"feedback_url\": \"test_url\"\n}" + required: true + responses: + "201": + description: Successful operation - returns the created bid + content: + application/json: + schema: + $ref: "#/components/schemas/inline_response_201" + "400": + description: Invalid input + content: + application/json: + schema: + $ref: "#/components/schemas/error_response" + examples: + "Invalid JSON": + value: {"error": Invalid JSON} + "Missing mandatory field": + value: {"error": "Missing mandatory field: {field}"} + "409": + description: Bid already exists + content: + application/json: + schema: + $ref: "#/components/schemas/error_response" + example: + {"error": Bid for this url already exists} + security: + - bids_auth: + - write:bids + - read:bids +components: + schemas: + bid_body: + type: object + properties: + tender: + type: string + bid_folder_url: + type: string + feedback_url: + type: string + feedback_description: + type: string + client: + type: string + alias: + type: string + bid_date: + type: string + inline_response_201: + type: object + properties: + tender: + type: string + last_updated: + type: string + failed: + type: object + properties: {} + feedback: + type: object + properties: + description: + type: string + url: + type: string + bid_folder_url: + type: string + success: + type: array + items: + type: object + properties: + phase: + type: integer + score: + type: integer + has_score: + type: boolean + out_of: + type: integer + alias: + type: string + client: + type: string + links: + type: object + properties: + questions: + type: string + self: + type: string + id: + type: string + bid_date: + type: string + status: + type: string + was_successful: + type: boolean + error_response: + type: object + properties: + error: + type: string + \ No newline at end of file diff --git a/swagger_config.yaml b/swagger_config.yaml deleted file mode 100644 index 2c5a9f5..0000000 --- a/swagger_config.yaml +++ /dev/null @@ -1,144 +0,0 @@ ---- -openapi: 3.0.1 -info: - title: defaultTitle - description: defaultDescription - version: "0.1" -servers: -- url: http://127.0.0.1:3000 -paths: - /api/bids: - post: - description: Auto generated using Swagger Inspector - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/api_bids_body' - examples: - "0": - value: "{\n \"tender\": \"test_tender\",\n \"client\": \"test_client\",\n \"alias\": \"test_alias\",\n \"bid_date\": \"21-06-2023\",\n \"bid_folder_url\": \"test_folder_url\", \n \"feedback_description\": \"test_description\",\n \"feedback_url\": \"test_url\"\n}" - responses: - "201": - description: Auto generated using Swagger Inspector - content: - application/json: - schema: - $ref: '#/components/schemas/inline_response_201' - examples: - "0": - value: | - { - "alias": "test_alias", - "bid_date": "2023-06-21T00:00:00", - "bid_folder_url": "test_folder_url", - "client": "test_client", - "failed": {}, - "feedback": { - "description": "test_description", - "url": "test_url" - }, - "id": "bf1e3f96-fcb0-48d6-be3f-83de1a944be3", - "last_updated": "2023-06-23T14:01:26.906857", - "links": { - "questions": "/bids/bf1e3f96-fcb0-48d6-be3f-83de1a944be3/questions", - "self": "/bids/bf1e3f96-fcb0-48d6-be3f-83de1a944be3" - }, - "status": "in_progress", - "success": [ - { - "has_score": true, - "out_of": 100, - "phase": 2, - "score": 80 - }, - { - "has_score": true, - "out_of": 100, - "phase": 2, - "score": 80 - }, - { - "has_score": true, - "out_of": 100, - "phase": 2, - "score": 80 - } - ], - "tender": "test_tender", - "was_successful": true - } - servers: - - url: http://127.0.0.1:3000 - servers: - - url: http://127.0.0.1:3000 -components: - schemas: - api_bids_body: - type: object - properties: - tender: - type: string - bid_folder_url: - type: string - feedback_url: - type: string - feedback_description: - type: string - client: - type: string - alias: - type: string - bid_date: - type: string - inline_response_201: - type: object - properties: - tender: - type: string - last_updated: - type: string - failed: - type: object - properties: {} - feedback: - type: object - properties: - description: - type: string - url: - type: string - bid_folder_url: - type: string - success: - type: array - items: - type: object - properties: - phase: - type: integer - score: - type: integer - has_score: - type: boolean - out_of: - type: integer - alias: - type: string - client: - type: string - links: - type: object - properties: - questions: - type: string - self: - type: string - id: - type: string - bid_date: - type: string - status: - type: string - was_successful: - type: boolean From 44145f6958d7ff2d0ae6c476d17498570c9759b5 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Tue, 27 Jun 2023 09:45:13 +0100 Subject: [PATCH 15/97] chore: flask-ui added to the requirements --- api/controllers/bid_controller.py | 6 +++--- api/models/bid_models.py | 8 ++++++-- requirements.txt | 1 + static/swagger_config.yml | 18 ++++++++++-------- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index d2c60fb..3a90122 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -1,12 +1,12 @@ from flask import Blueprint -from api.models.bid_models import get_test, create_bid +from api.models.bid_models import get_bids, create_bid bid = Blueprint('bid', __name__) @bid.route("/bids", methods=["GET"]) -def get_tested(): - response = get_test() +def get_all_bids(): + response = get_bids() return response @bid.route("/bids", methods=["POST"]) diff --git a/api/models/bid_models.py b/api/models/bid_models.py index c9c8a9a..c885303 100644 --- a/api/models/bid_models.py +++ b/api/models/bid_models.py @@ -1,8 +1,12 @@ from flask import request, jsonify from api.schemas.bid_schema import BidSchema, Status -def get_test(): - return 'test', 200 +def get_bids(): + f = open('./db.txt','r') + bids = f.read() + f.close() + return bids, 200 + def create_bid(): mandatory_fields = ['tender', 'client', 'bid_date'] diff --git a/requirements.txt b/requirements.txt index 3d85fa9..775a778 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ MarkupSafe==2.1.3 Werkzeug==2.3.6 pip==23.1.2 pytest==7.3.2 +flask_swagger_ui==4.11.1 diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 8c7638c..b2c0bc1 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -22,7 +22,7 @@ info: # -------------------------------------------- # Server servers: - - url: http://127.0.0.1:3000/api/ + - url: http://localhost:3000/api/ description: "Local server" # -------------------------------------------- # Tags @@ -43,15 +43,17 @@ paths: get: tags: - "bids" - summary: Returns a test string - description: Returns a test string + summary: Returns all bids + description: Returns all bids responses: '200': # status code - description: OK - content: # Response body - text/plain: # Media type - schema: # Must-have - type: string # Data type + description: A JSON array of bids + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/inline_response_201' post: tags: - "bids" From 2d2d28f58f63454ab6c72ce0c4d4b6bd18dfedc9 Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 27 Jun 2023 11:44:25 +0100 Subject: [PATCH 16/97] fix: swagger config structure --- static/swagger_config.yml | 103 +++++++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 41 deletions(-) diff --git a/static/swagger_config.yml b/static/swagger_config.yml index b2c0bc1..3da8e74 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -1,48 +1,50 @@ ---- +# --- openapi: 3.0.3 # -------------------------------------------- # Info info: - title: "Bids API" - description: " + title: Bids API + description: | This is a API for the Bids API. You can find out more about this API at [GitHub](https://github.com/methods/tdse-accessForce-bids-api). You can now help us improve the API whether it's by making changes to the definition itself or to the code. That way, with time, we can improve the API in general, and expose some of the new features in OAS3. Some useful links: - - [API repository](https://github.com/methods/tdse-accessForce-bids-api)" - version: "1.0.0" - termsOfService: "https://github.com/methods/tdse-accessForce-bids-api" + - [API repository](https://github.com/methods/tdse-accessForce-bids-api) + version: 1.0.0 + termsOfService: 'https://github.com/methods/tdse-accessForce-bids-api' contact: - email: "accessForce@example.com" + email: accessForce@example.com license: - name: "MIT License" - url: "https://github.com/methods/tdse-accessForce-bids-api/blob/develop/LICENSE.md" + name: MIT License + url: 'https://github.com/methods/tdse-accessForce-bids-api/blob/develop/LICENSE.md' # -------------------------------------------- # Server servers: - - url: http://localhost:3000/api/ - description: "Local server" + - url: 'http://localhost:3000/api/' + description: Local server # -------------------------------------------- # Tags tags: - - name: "bids" - description: "Everything about BIDS" + - name: bids + description: Everything about BIDS externalDocs: description: Find out more - url: "example.com" - - name: "questions" - description: "Everything about QUESTIONS" + url: example.com + - name: questions + description: Everything about QUESTIONS externalDocs: - description: "Find out more" - url: "example.com" + description: Find out more + url: example.com # -------------------------------------------- +# # Paths paths: /bids: +# -------------------------------------------- get: tags: - - "bids" + - bids summary: Returns all bids description: Returns all bids responses: @@ -54,18 +56,19 @@ paths: type: array items: $ref: '#/components/schemas/inline_response_201' +# -------------------------------------------- post: tags: - - "bids" - summary: "Create a new bid" - description: "Create a new bid" - operationId: "post_bid" + - bids + summary: Create a new bid + description: Create a new bid + operationId: post_bid requestBody: description: Create a new bid in the store content: application/json: schema: - $ref: '#/components/schemas/bid_body' + $ref: '#/components/schemas/Bid' examples: "201: Successful request": value: "{\n \"tender\": \"test_tender\",\n \"client\": \"test_client\",\n \"alias\": \"test_alias\",\n \"bid_date\": \"21-06-2023\",\n \"bid_folder_url\": \"test_folder_url\", \n \"feedback_description\": \"test_description\",\n \"feedback_url\": \"test_url\"\n}" @@ -73,54 +76,71 @@ paths: value: "{\n \"client\": \"test_clien2\",\n \"alias\": \"test_alias2\",\n \"bid_date\": \"21-06-2023\",\n \"bid_folder_url\": \"test_folder_url\", \n \"feedback_description\": \"test_description\",\n \"feedback_url\": \"test_url\"\n}" required: true responses: - "201": + '201': description: Successful operation - returns the created bid content: application/json: schema: - $ref: "#/components/schemas/inline_response_201" - "400": + $ref: '#/components/schemas/inline_response_201' + '400': description: Invalid input content: application/json: schema: - $ref: "#/components/schemas/error_response" + $ref: '#/components/schemas/error_response' examples: - "Invalid JSON": - value: {"error": Invalid JSON} - "Missing mandatory field": - value: {"error": "Missing mandatory field: {field}"} - "409": + Invalid JSON: + value: {error: Invalid JSON} + Missing mandatory field: + value: {error: 'Missing mandatory field: {field}'} + '409': description: Bid already exists content: application/json: schema: - $ref: "#/components/schemas/error_response" + $ref: '#/components/schemas/error_response' example: - {"error": Bid for this url already exists} + {error: Bid for this url already exists} security: - bids_auth: - write:bids - read:bids +# -------------------------------------------- + components: +# -------------------------------------------- +# schemas schemas: - bid_body: + Bid: type: object + required: + - tender + - client + - bid_date properties: tender: - type: string + description: Name of tender for which bid is made + type: string bid_folder_url: - type: string + description: Link to where bid info is stored on Sharepoint + type: string feedback_url: - type: string + description: Link to where feedback on the bid is stored on Sharepoint (if exists) + type: string feedback_description: - type: string + description: Summary of feedback on the bid (if exists) + type: string client: + description: Name of client tendering type: string alias: + description: Acronym/abbreviation of client name type: string bid_date: + description: Date when bid was submitted type: string +# -------------------------------------------- +# Responses inline_response_201: type: object properties: @@ -177,4 +197,5 @@ components: properties: error: type: string - \ No newline at end of file +# -------------------------------------------- + From 5f10e678072f93acd500b0cd0d492dadf52ea995 Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 27 Jun 2023 17:02:51 +0100 Subject: [PATCH 17/97] refactor: swagger config components and refs Co-authored-by: Julio Velezmoro --- api/schemas/phase_schema.py | 2 +- db.txt | 1 + static/swagger_config.yml | 253 +++++++++++++++++++++++------------- 3 files changed, 168 insertions(+), 88 deletions(-) diff --git a/api/schemas/phase_schema.py b/api/schemas/phase_schema.py index 0e8d842..150de34 100644 --- a/api/schemas/phase_schema.py +++ b/api/schemas/phase_schema.py @@ -1,6 +1,6 @@ # Schema for phaseInfo object class PhaseInfo: - def __init__(self, phase, has_score, score=None, out_of=None): + def __init__(self, phase, has_score, score=int, out_of=int): self.phase = phase self.has_score = has_score self.score = score diff --git a/db.txt b/db.txt index 8041e2d..3cb6714 100644 --- a/db.txt +++ b/db.txt @@ -1,2 +1,3 @@ {'id': UUID('471fea1f-705c-4851-9a5b-df7bc2651428'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428', 'questions': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:16:59.404946'}{'id': UUID('0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8', 'questions': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:17:23.593171'}{'id': UUID('8fed99f8-3b7c-42ef-8c1f-fb3fe25ec039'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/8fed99f8-3b7c-42ef-8c1f-fb3fe25ec039', 'questions': '/bids/8fed99f8-3b7c-42ef-8c1f-fb3fe25ec039/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T15:57:34.571615'}{'id': UUID('96d69775-29af-46b1-aaf4-bfbdb1543412'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'deleted', 'links': {'self': '/bids/96d69775-29af-46b1-aaf4-bfbdb1543412', 'questions': '/bids/96d69775-29af-46b1-aaf4-bfbdb1543412/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T15:57:50.238595'} +{'id': UUID('a1729d42-3962-47ce-b54b-2a93e879891c'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'deleted', 'links': {'self': '/bids/a1729d42-3962-47ce-b54b-2a93e879891c', 'questions': '/bids/a1729d42-3962-47ce-b54b-2a93e879891c/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-27T14:05:17.623827'}{'id': UUID('ab7bec38-f866-46ba-8e94-e85fec951326'), 'tender': 'Business Intelligence and Data Warehousing', 'client': 'Office for National Statistics', 'alias': 'ONS', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'https://organisation.sharepoint.com/Docs/dummyfolder', 'status': 'deleted', 'links': {'self': '/bids/ab7bec38-f866-46ba-8e94-e85fec951326', 'questions': '/bids/ab7bec38-f866-46ba-8e94-e85fec951326/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': '', 'url': ''}, 'last_updated': '2023-06-27T16:51:58.932142'}{'id': UUID('dfff0088-0ae0-4c80-8a32-e70c166453c2'), 'tender': 'Business Intelligence and Data Warehousing', 'client': 'Office for National Statistics', 'alias': 'ONS', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'https://organisation.sharepoint.com/Docs/dummyfolder', 'status': 'deleted', 'links': {'self': '/bids/dfff0088-0ae0-4c80-8a32-e70c166453c2', 'questions': '/bids/dfff0088-0ae0-4c80-8a32-e70c166453c2/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': '', 'url': ''}, 'last_updated': '2023-06-27T16:52:00.527900'} \ No newline at end of file diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 3da8e74..9040f12 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -38,7 +38,7 @@ tags: description: Find out more url: example.com # -------------------------------------------- -# # Paths +# Paths paths: /bids: # -------------------------------------------- @@ -55,7 +55,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/inline_response_201' + $ref: '#/components/schemas/Bid' # -------------------------------------------- post: tags: @@ -64,138 +64,217 @@ paths: description: Create a new bid operationId: post_bid requestBody: - description: Create a new bid in the store - content: - application/json: - schema: - $ref: '#/components/schemas/Bid' - examples: - "201: Successful request": - value: "{\n \"tender\": \"test_tender\",\n \"client\": \"test_client\",\n \"alias\": \"test_alias\",\n \"bid_date\": \"21-06-2023\",\n \"bid_folder_url\": \"test_folder_url\", \n \"feedback_description\": \"test_description\",\n \"feedback_url\": \"test_url\"\n}" - "400: Missing mandatory field": - value: "{\n \"client\": \"test_clien2\",\n \"alias\": \"test_alias2\",\n \"bid_date\": \"21-06-2023\",\n \"bid_folder_url\": \"test_folder_url\", \n \"feedback_description\": \"test_description\",\n \"feedback_url\": \"test_url\"\n}" + $ref: '#/components/requestBodies/Bid' required: true responses: '201': - description: Successful operation - returns the created bid + description: Created content: application/json: schema: - $ref: '#/components/schemas/inline_response_201' + $ref: '#/components/schemas/Bid' '400': - description: Invalid input - content: - application/json: - schema: - $ref: '#/components/schemas/error_response' - examples: - Invalid JSON: - value: {error: Invalid JSON} - Missing mandatory field: - value: {error: 'Missing mandatory field: {field}'} - '409': - description: Bid already exists - content: - application/json: - schema: - $ref: '#/components/schemas/error_response' - example: - {error: Bid for this url already exists} - security: - - bids_auth: - - write:bids - - read:bids + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalServerError' # -------------------------------------------- - +# Components components: -# -------------------------------------------- -# schemas + # -------------------------------------------- + # Schemas schemas: Bid: + description: Bid document type: object required: + - id - tender - client - bid_date - properties: - tender: - description: Name of tender for which bid is made - type: string - bid_folder_url: - description: Link to where bid info is stored on Sharepoint - type: string - feedback_url: - description: Link to where feedback on the bid is stored on Sharepoint (if exists) - type: string - feedback_description: - description: Summary of feedback on the bid (if exists) - type: string - client: - description: Name of client tendering - type: string - alias: - description: Acronym/abbreviation of client name - type: string - bid_date: - description: Date when bid was submitted - type: string -# -------------------------------------------- -# Responses - inline_response_201: - type: object + - status + - links + - last_updated properties: tender: type: string + example: 'Business Intelligence and Data Warehousing' last_updated: type: string + example: "2023-06-27T14:05:17.623827" failed: type: object - properties: {} + $ref: '#/components/schemas/Phase' + example: '#/components/schemas/Phase' feedback: type: object - properties: - description: - type: string - url: - type: string - bid_folder_url: - type: string + $ref: '#/components/schemas/Feedback' success: type: array items: type: object - properties: - phase: - type: integer - score: - type: integer - has_score: - type: boolean - out_of: - type: integer + $ref: '#/components/schemas/Phase' alias: + example: 'ONS' type: string client: + example: 'Office for National Statistics' type: string links: type: object + required: + - questions + - self properties: questions: - type: string + $ref: '#/components/schemas/QuestionsLink' self: - type: string + $ref: '#/components/schemas/SelfLink' id: + format: uuid + example: "UUID('471fea1f-705c-4851-9a5b-df7bc2651428')" type: string bid_date: + format: ISO-8601 + example: '2023-06-21T00:00:00' type: string status: type: string + description: Bid Status + example: in_progress + enum: + - IN_PROGRESS + - DELETED + - COMPLETED was_successful: + example: true + type: boolean +# -------------------------------------------- + Phase: + description: Phase information + type: object + required: + - phase + - has_score + properties: + phase: + description: Phase of bid + type: integer + example: 2 + score: + description: Score achieved at phase + type: integer + example: 22 + has_score: + description: Score information available or not type: boolean - error_response: + example: true + out_of: + description: Maximum score + type: integer + example: 36 +# -------------------------------------------- + Feedback: + description: Feedback from client (if provided) type: object + required: + - feedback_url + - feedback_description properties: - error: + feedback_url: + description: Link to feedback + type: string + example: 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback' + feedback_description: + description: Summary of feedback type: string + example: 'Feedback from client in detail' +# -------------------------------------------- + QuestionsLink: + description: A link to a collection of questions for a bid + type: string + example: 'https://localhost:3000/api//bids/96d69775-29af-46b1-aaf4-bfbdb1543412/questions' # -------------------------------------------- - + SelfLink: + description: A link to the current resource + type: string + example: 'https://localhost:3000/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428' +# -------------------------------------------- + BidRequestBody: + type: object + required: + - tender + - client + - bid_date + properties: + tender: + description: Name of tender + type: string + bid_folder_url: + description: Link to bid info + type: string + feedback: + description: Feedback info + type: object + $ref: '#/components/schemas/Feedback' + client: + description: Name of client tendering + type: string + alias: + description: Client alias + type: string + bid_date: + description: Date of bid + type: string + failed_phase: + description: Failed phase info + type: object + $ref: '#/components/schemas/Phase' + # -------------------------------------------- + # Request bodies + requestBodies: + Bid: + description: Bid object to be added to collection + content: + application/json: + schema: + $ref: '#/components/schemas/BidRequestBody' + examples: + 200 Created: + summary: 200 Created + value: + tender: 'Business Intelligence and Data Warehousing' + bid_folder_url: 'https://organisation.sharepoint.com/Docs/dummyfolder' + feedback: + feedback_url: 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback' + feedback_description: 'Feedback from client in detail' + client: 'Office for National Statistics' + alias: 'ONS' + bid_date: '21-06-2023' + 400 Bad Request: + summary: 400 Bad Request + value: + bid_folder_url: 'https://organisation.sharepoint.com/Docs/dummyfolder' + feedback: + feedback_url: 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback' + feedback_description: 'Feedback from client in detail' + client: 'Office for National Statistics' + alias: 'ONS' + bid_date: '21-06-2023' + # -------------------------------------------- + # Error responses + responses: + BadRequest: + description: Bad Request Error + content: + text/plain: + schema: + type: string + example: Missing mandatory field + InternalServerError: + description: Internal Server Error + content: + text/plain: + schema: + type: string + example: Could not connect to a database +# -------------------------------------------- \ No newline at end of file From cd2bc7439c4c9d4341b2303ce75032566f023378 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 28 Jun 2023 10:52:31 +0100 Subject: [PATCH 18/97] feat: completed swagger spec for post bid endpoint --- static/swagger_config.yml | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 9040f12..12df7f1 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -80,8 +80,8 @@ paths: # -------------------------------------------- # Components components: - # -------------------------------------------- - # Schemas +# -------------------------------------------- +# Schemas schemas: Bid: description: Bid document @@ -114,11 +114,11 @@ components: type: object $ref: '#/components/schemas/Phase' alias: - example: 'ONS' type: string + example: 'ONS' client: - example: 'Office for National Statistics' type: string + example: 'Office for National Statistics' links: type: object required: @@ -130,13 +130,13 @@ components: self: $ref: '#/components/schemas/SelfLink' id: + type: string format: uuid example: "UUID('471fea1f-705c-4851-9a5b-df7bc2651428')" - type: string bid_date: + type: string format: ISO-8601 example: '2023-06-21T00:00:00' - type: string status: type: string description: Bid Status @@ -146,8 +146,8 @@ components: - DELETED - COMPLETED was_successful: - example: true type: boolean + example: true # -------------------------------------------- Phase: description: Phase information @@ -229,8 +229,13 @@ components: description: Failed phase info type: object $ref: '#/components/schemas/Phase' - # -------------------------------------------- - # Request bodies + success_list: + description: List of successful phases + type: array + items: + $ref: '#/components/schemas/Phase' +# -------------------------------------------- +# Request bodies requestBodies: Bid: description: Bid object to be added to collection @@ -260,8 +265,8 @@ components: client: 'Office for National Statistics' alias: 'ONS' bid_date: '21-06-2023' - # -------------------------------------------- - # Error responses +# -------------------------------------------- +# Error responses responses: BadRequest: description: Bad Request Error From 9249e09d893b1266877146ec51b6d2f1721fcb87 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 28 Jun 2023 12:58:05 +0100 Subject: [PATCH 19/97] refactor: success and failed phase info Co-authored-by: Julio Velezmoro --- api/models/bid_models.py | 24 +++++++++++++----------- api/schemas/bid_schema.py | 23 +++++++++++------------ api/schemas/feedback_schema.py | 5 +++++ api/schemas/status_schema.py | 6 +++--- db.txt | 2 +- test_request.http | 5 ++--- 6 files changed, 35 insertions(+), 30 deletions(-) create mode 100644 api/schemas/feedback_schema.py diff --git a/api/models/bid_models.py b/api/models/bid_models.py index 7100948..3f8ff58 100644 --- a/api/models/bid_models.py +++ b/api/models/bid_models.py @@ -1,5 +1,6 @@ from flask import request, jsonify from api.schemas.bid_schema import BidSchema, Status +from api.schemas.phase_schema import PhaseInfo def get_bids(): f = open('./db.txt','r') @@ -19,20 +20,21 @@ def create_bid(): # BidSchema object bid_schema = BidSchema( - tender=request.json['tender'], - client=request.json['client'], - alias=request.json.get('alias', ''), - bid_date=request.json['bid_date'], - bid_folder_url=request.json.get('bid_folder_url', ''), - feedback_description=request.json.get('feedback_description', ''), - feedback_url=request.json.get('feedback_url', '') + tender= request.json['tender'], + client= request.json['client'], + alias= request.json.get('alias', ''), + bid_date= request.json['bid_date'], + bid_folder_url= request.json.get('bid_folder_url', ''), + feedback= request.json.get('feedback', '') ) # Append phase information to the success list - bid_schema.addSuccessPhase(phase=2, has_score=True, score=80, out_of=100) + successPhases= [PhaseInfo(phase=3, has_score=True, score=50, out_of=100), PhaseInfo(phase=4, has_score=True, score=50, out_of=100)] + for phase in successPhases: + bid_schema.addSuccessPhase(phase) + # Set failed phase info - # bid_schema.setFailedPhase(phase=3, has_score=True, score=50, out_of=100) - # Change status - bid_schema.setStatus(Status.DELETED) + failedPhase = bid_schema.setFailedPhase(PhaseInfo(phase=3, has_score=True, score=50, out_of=100)) + # Convert the mock BidSchema object to a dictionary bid_json = bid_schema.toDbCollection() diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py index d5558f5..f796c22 100644 --- a/api/schemas/bid_schema.py +++ b/api/schemas/bid_schema.py @@ -2,17 +2,18 @@ from datetime import datetime from .phase_schema import PhaseInfo from .status_schema import Status +from .feedback_schema import Feedback # Description: Schema for the bid object class BidSchema: - def __init__(self, tender, client, bid_date, alias='', bid_folder_url='', status='in_progress', was_successful=True,success=[], failed={}, feedback_description=None, feedback_url=None): + def __init__(self, tender, client, bid_date, alias='', bid_folder_url='', feedback='', failed={}, was_successful=False, success=[]): self.id = uuid4() self.tender = tender self.client = client self.alias = alias self.bid_date = datetime.strptime(bid_date, '%d-%m-%Y').isoformat() # DD-MM-YYYY self.bid_folder_url = bid_folder_url - self.status = status # enum: "deleted", "in_progress" or "completed" + self.status = Status.IN_PROGRESS.value # enum: "deleted", "in_progress" or "completed" self.links = { 'self': f"/bids/{self.id}", 'questions': f"/bids/{self.id}/questions" @@ -20,26 +21,24 @@ def __init__(self, tender, client, bid_date, alias='', bid_folder_url='', status self.was_successful = was_successful self.success = success self.failed = failed - self.feedback = {"description": feedback_description, - "url": feedback_url} + self.feedback = feedback self.last_updated = datetime.now().isoformat() - def addSuccessPhase(self, phase, has_score, score=None, out_of=None): - phase_info = PhaseInfo(phase=phase, has_score=has_score, score=score, out_of=out_of) + def addSuccessPhase(self, phase_info): self.success.append(phase_info) - def setFailedPhase(self, phase, has_score, score=None, out_of=None): - self.was_successful = False - self.failed = PhaseInfo(phase=phase, has_score=has_score, score=score, out_of=out_of) + def setFailedPhase(self, phase_info): + self.was_successful = False + self.failed = phase_info def setStatus(self, status): if isinstance(status, Status): - self.status = status.name.lower() + self.status = status.value else: raise ValueError("Invalid status. Please provide a valid Status enum value") - + def toDbCollection(self): self.success = [s.__dict__ for s in self.success] if self.success else [] - self.failed = self.failed.__dict__ if self.failed else {} + self.failed = self.failed.__dict__ if self.failed else None return self.__dict__ diff --git a/api/schemas/feedback_schema.py b/api/schemas/feedback_schema.py new file mode 100644 index 0000000..c1d80c0 --- /dev/null +++ b/api/schemas/feedback_schema.py @@ -0,0 +1,5 @@ +# Schema for Feedback object +class Feedback: + def __init__(self,feedback_description, feedback_url): + self.feedback_description = feedback_description + self.feedback_url = feedback_url \ No newline at end of file diff --git a/api/schemas/status_schema.py b/api/schemas/status_schema.py index 90437e7..ce0f2c7 100644 --- a/api/schemas/status_schema.py +++ b/api/schemas/status_schema.py @@ -2,6 +2,6 @@ # Enum for status class Status(Enum): - DELETED = 1 - IN_PROGRESS = 2 - COMPLETED = 3 \ No newline at end of file + DELETED = "deleted" + IN_PROGRESS = "in_progress" + COMPLETED = "completed" \ No newline at end of file diff --git a/db.txt b/db.txt index 3cb6714..12c401c 100644 --- a/db.txt +++ b/db.txt @@ -1,3 +1,3 @@ {'id': UUID('471fea1f-705c-4851-9a5b-df7bc2651428'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428', 'questions': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:16:59.404946'}{'id': UUID('0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8', 'questions': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:17:23.593171'}{'id': UUID('8fed99f8-3b7c-42ef-8c1f-fb3fe25ec039'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/8fed99f8-3b7c-42ef-8c1f-fb3fe25ec039', 'questions': '/bids/8fed99f8-3b7c-42ef-8c1f-fb3fe25ec039/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T15:57:34.571615'}{'id': UUID('96d69775-29af-46b1-aaf4-bfbdb1543412'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'deleted', 'links': {'self': '/bids/96d69775-29af-46b1-aaf4-bfbdb1543412', 'questions': '/bids/96d69775-29af-46b1-aaf4-bfbdb1543412/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T15:57:50.238595'} -{'id': UUID('a1729d42-3962-47ce-b54b-2a93e879891c'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'deleted', 'links': {'self': '/bids/a1729d42-3962-47ce-b54b-2a93e879891c', 'questions': '/bids/a1729d42-3962-47ce-b54b-2a93e879891c/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-27T14:05:17.623827'}{'id': UUID('ab7bec38-f866-46ba-8e94-e85fec951326'), 'tender': 'Business Intelligence and Data Warehousing', 'client': 'Office for National Statistics', 'alias': 'ONS', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'https://organisation.sharepoint.com/Docs/dummyfolder', 'status': 'deleted', 'links': {'self': '/bids/ab7bec38-f866-46ba-8e94-e85fec951326', 'questions': '/bids/ab7bec38-f866-46ba-8e94-e85fec951326/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': '', 'url': ''}, 'last_updated': '2023-06-27T16:51:58.932142'}{'id': UUID('dfff0088-0ae0-4c80-8a32-e70c166453c2'), 'tender': 'Business Intelligence and Data Warehousing', 'client': 'Office for National Statistics', 'alias': 'ONS', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'https://organisation.sharepoint.com/Docs/dummyfolder', 'status': 'deleted', 'links': {'self': '/bids/dfff0088-0ae0-4c80-8a32-e70c166453c2', 'questions': '/bids/dfff0088-0ae0-4c80-8a32-e70c166453c2/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': '', 'url': ''}, 'last_updated': '2023-06-27T16:52:00.527900'} \ No newline at end of file +{'id': UUID('a1729d42-3962-47ce-b54b-2a93e879891c'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'deleted', 'links': {'self': '/bids/a1729d42-3962-47ce-b54b-2a93e879891c', 'questions': '/bids/a1729d42-3962-47ce-b54b-2a93e879891c/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-27T14:05:17.623827'}{'id': UUID('ab7bec38-f866-46ba-8e94-e85fec951326'), 'tender': 'Business Intelligence and Data Warehousing', 'client': 'Office for National Statistics', 'alias': 'ONS', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'https://organisation.sharepoint.com/Docs/dummyfolder', 'status': 'deleted', 'links': {'self': '/bids/ab7bec38-f866-46ba-8e94-e85fec951326', 'questions': '/bids/ab7bec38-f866-46ba-8e94-e85fec951326/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': '', 'url': ''}, 'last_updated': '2023-06-27T16:51:58.932142'}{'id': UUID('dfff0088-0ae0-4c80-8a32-e70c166453c2'), 'tender': 'Business Intelligence and Data Warehousing', 'client': 'Office for National Statistics', 'alias': 'ONS', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'https://organisation.sharepoint.com/Docs/dummyfolder', 'status': 'deleted', 'links': {'self': '/bids/dfff0088-0ae0-4c80-8a32-e70c166453c2', 'questions': '/bids/dfff0088-0ae0-4c80-8a32-e70c166453c2/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': '', 'url': ''}, 'last_updated': '2023-06-27T16:52:00.527900'}{'id': UUID('ec9aded1-dc17-4eb2-ab2f-299a7db3f327'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'deleted', 'links': {'self': '/bids/ec9aded1-dc17-4eb2-ab2f-299a7db3f327', 'questions': '/bids/ec9aded1-dc17-4eb2-ab2f-299a7db3f327/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {}, 'last_updated': '2023-06-28T11:37:11.764715'}{'id': UUID('7e6c75ff-0401-48c2-bc33-27e831a30db7'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'deleted', 'links': {'self': '/bids/7e6c75ff-0401-48c2-bc33-27e831a30db7', 'questions': '/bids/7e6c75ff-0401-48c2-bc33-27e831a30db7/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T11:38:34.673343'}{'id': UUID('abf2422f-2632-4e75-ba7b-8cb827861813'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'deleted', 'links': {'self': '/bids/abf2422f-2632-4e75-ba7b-8cb827861813', 'questions': '/bids/abf2422f-2632-4e75-ba7b-8cb827861813/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {}, 'last_updated': '2023-06-28T11:39:09.216544'}{'id': UUID('3e72fff1-df91-4f2e-8758-5936a58ab216'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'deleted', 'links': {'self': '/bids/3e72fff1-df91-4f2e-8758-5936a58ab216', 'questions': '/bids/3e72fff1-df91-4f2e-8758-5936a58ab216/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T11:39:31.225212'}{'id': UUID('2a705a23-7ae5-4b10-a802-fc19b42c252e'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': , 'links': {'self': '/bids/2a705a23-7ae5-4b10-a802-fc19b42c252e', 'questions': '/bids/2a705a23-7ae5-4b10-a802-fc19b42c252e/questions'}, 'was_successful': , 'success': [], 'failed': {}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T11:57:42.260083'}{'id': UUID('0ecaeab6-3cba-463a-8a83-51b289368c4c'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': , 'links': {'self': '/bids/0ecaeab6-3cba-463a-8a83-51b289368c4c', 'questions': '/bids/0ecaeab6-3cba-463a-8a83-51b289368c4c/questions'}, 'was_successful': , 'success': [], 'failed': {'test': 'test'}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T11:58:26.287023'}{'id': UUID('d8e4a941-3079-4917-94f2-e1b6d4fa92fa'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': , 'links': {'self': '/bids/d8e4a941-3079-4917-94f2-e1b6d4fa92fa', 'questions': '/bids/d8e4a941-3079-4917-94f2-e1b6d4fa92fa/questions'}, 'was_successful': , 'success': [], 'failed': {'test': 'test'}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T11:59:20.748727'}{'id': UUID('238f406d-69a1-4aa8-9614-182000bb4a35'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': , 'links': {'self': '/bids/238f406d-69a1-4aa8-9614-182000bb4a35', 'questions': '/bids/238f406d-69a1-4aa8-9614-182000bb4a35/questions'}, 'was_successful': , 'success': [], 'failed': {'test': 'test'}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:00:03.332593'}{'id': UUID('f11de2fa-c5b0-4ffa-918d-1dcc28d0c186'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': , 'links': {'self': '/bids/f11de2fa-c5b0-4ffa-918d-1dcc28d0c186', 'questions': '/bids/f11de2fa-c5b0-4ffa-918d-1dcc28d0c186/questions'}, 'was_successful': , 'success': [], 'failed': {'test': 'test'}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:00:45.022221'}{'id': UUID('3f1d1d43-df57-44d3-bdf9-0f6b50f48b57'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': '', 'links': {'self': '/bids/3f1d1d43-df57-44d3-bdf9-0f6b50f48b57', 'questions': '/bids/3f1d1d43-df57-44d3-bdf9-0f6b50f48b57/questions'}, 'was_successful': , 'success': [], 'failed': {'test': 'test'}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:01:16.003853'}{'id': UUID('9d1d19ed-36e7-47e6-b122-7ceb68c5a39f'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'IN_PROGRESS', 'links': {'self': '/bids/9d1d19ed-36e7-47e6-b122-7ceb68c5a39f', 'questions': '/bids/9d1d19ed-36e7-47e6-b122-7ceb68c5a39f/questions'}, 'was_successful': , 'success': [], 'failed': None, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:09:32.911235'}{'id': UUID('2c016b52-5e1f-407c-b3f3-245a2d6670ca'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'IN_PROGRESS', 'links': {'self': '/bids/2c016b52-5e1f-407c-b3f3-245a2d6670ca', 'questions': '/bids/2c016b52-5e1f-407c-b3f3-245a2d6670ca/questions'}, 'was_successful': True, 'success': [], 'failed': None, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:25:46.445061'}{'id': UUID('3dd937f5-ff70-4e04-90c3-93cf8a1fac8d'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': , 'links': {'self': '/bids/3dd937f5-ff70-4e04-90c3-93cf8a1fac8d', 'questions': '/bids/3dd937f5-ff70-4e04-90c3-93cf8a1fac8d/questions'}, 'was_successful': True, 'success': [], 'failed': None, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:26:36.844227'}{'id': UUID('4e9761d0-9a6e-4a73-a526-ca6e9809a133'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'IN_PROGRESS', 'links': {'self': '/bids/4e9761d0-9a6e-4a73-a526-ca6e9809a133', 'questions': '/bids/4e9761d0-9a6e-4a73-a526-ca6e9809a133/questions'}, 'was_successful': True, 'success': [], 'failed': None, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:26:50.230010'}{'id': UUID('1f26a4f6-3c4e-4965-967b-7940eb04577d'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/1f26a4f6-3c4e-4965-967b-7940eb04577d', 'questions': '/bids/1f26a4f6-3c4e-4965-967b-7940eb04577d/questions'}, 'was_successful': True, 'success': [], 'failed': None, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:27:07.915123'}{'id': UUID('ba9cfeaf-facf-49ef-8fd5-5b49b0e566ab'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/ba9cfeaf-facf-49ef-8fd5-5b49b0e566ab', 'questions': '/bids/ba9cfeaf-facf-49ef-8fd5-5b49b0e566ab/questions'}, 'was_successful': True, 'success': [], 'failed': None, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:29:09.037829'}{'id': UUID('4a42fcbe-246a-49ef-afc6-fa50a5aee424'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/4a42fcbe-246a-49ef-afc6-fa50a5aee424', 'questions': '/bids/4a42fcbe-246a-49ef-afc6-fa50a5aee424/questions'}, 'was_successful': False, 'success': [{'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, {'phase': 4, 'has_score': True, 'score': 50, 'out_of': 100}], 'failed': None, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:42:56.798021'}{'id': UUID('5b28fb9c-96df-4b89-a6b3-581cbecec177'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/5b28fb9c-96df-4b89-a6b3-581cbecec177', 'questions': '/bids/5b28fb9c-96df-4b89-a6b3-581cbecec177/questions'}, 'was_successful': False, 'success': [{'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, {'phase': 4, 'has_score': True, 'score': 50, 'out_of': 100}], 'failed': None, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:46:07.456529'}{'id': UUID('c8614428-a5ef-4a64-8571-4fdeafa1d60a'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/c8614428-a5ef-4a64-8571-4fdeafa1d60a', 'questions': '/bids/c8614428-a5ef-4a64-8571-4fdeafa1d60a/questions'}, 'was_successful': False, 'success': [{'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, {'phase': 4, 'has_score': True, 'score': 50, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:49:25.680814'}{'id': UUID('1690b875-900a-4292-9b75-8c20afd2b2d8'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/1690b875-900a-4292-9b75-8c20afd2b2d8', 'questions': '/bids/1690b875-900a-4292-9b75-8c20afd2b2d8/questions'}, 'was_successful': False, 'success': [{'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, {'phase': 4, 'has_score': True, 'score': 50, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:50:09.715531'}{'id': UUID('1890e1fa-10dc-453d-8f46-38d65386594a'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/1890e1fa-10dc-453d-8f46-38d65386594a', 'questions': '/bids/1890e1fa-10dc-453d-8f46-38d65386594a/questions'}, 'was_successful': False, 'success': [{'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, {'phase': 4, 'has_score': True, 'score': 50, 'out_of': 100}, {'phase': 103, 'has_score': True, 'score': 50, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:52:49.354905'} \ No newline at end of file diff --git a/test_request.http b/test_request.http index 82a2fd9..637be19 100644 --- a/test_request.http +++ b/test_request.http @@ -6,7 +6,6 @@ Content-Type: application/json "client": "test_client", "alias": "test_alias", "bid_date": "21-06-2023", - "bid_folder_url": "test_folder_url", - "feedback_description": "test_description", - "feedback_url": "test_url" + "bid_folder_url": "test_folder_url", + "feedback": {"feedback_description": "test", "feedback_url": "url"} } \ No newline at end of file From 6d1fdbc88f98ffad5d801613240e6db0a474c236 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 28 Jun 2023 17:20:57 +0100 Subject: [PATCH 20/97] refactor: added helper function and logic for feedback --- api/models/bid_models.py | 28 ++++++++++++++++------------ api/schemas/bid_schema.py | 11 ++++++----- api/schemas/feedback_schema.py | 6 +++--- api/schemas/phase_schema.py | 2 +- app.py | 6 +++--- db.txt | 4 +--- helpers/helpers.py | 5 +++++ test_request.http | 5 ++--- 8 files changed, 37 insertions(+), 30 deletions(-) diff --git a/api/models/bid_models.py b/api/models/bid_models.py index 3f8ff58..9dfedfc 100644 --- a/api/models/bid_models.py +++ b/api/models/bid_models.py @@ -1,6 +1,8 @@ from flask import request, jsonify -from api.schemas.bid_schema import BidSchema, Status +from api.schemas.bid_schema import BidSchema from api.schemas.phase_schema import PhaseInfo +from api.schemas.feedback_schema import Feedback +from helpers.helpers import save_in_memory def get_bids(): f = open('./db.txt','r') @@ -12,14 +14,14 @@ def get_bids(): def create_bid(): mandatory_fields = ['tender', 'client', 'bid_date'] if not request.is_json: - return jsonify({'error': 'Invalid JSON'}), 400 + return 'Invalid JSON', 400 for field in mandatory_fields: if field not in request.json: - return jsonify({'error': f'Missing mandatory field: {field}'}), 400 + return 'Missing mandatory field: %s' % field, 400 # BidSchema object - bid_schema = BidSchema( + bid_document = BidSchema( tender= request.json['tender'], client= request.json['client'], alias= request.json.get('alias', ''), @@ -27,20 +29,22 @@ def create_bid(): bid_folder_url= request.json.get('bid_folder_url', ''), feedback= request.json.get('feedback', '') ) - # Append phase information to the success list + # Add successful phase info to success list successPhases= [PhaseInfo(phase=3, has_score=True, score=50, out_of=100), PhaseInfo(phase=4, has_score=True, score=50, out_of=100)] for phase in successPhases: - bid_schema.addSuccessPhase(phase) + bid_document.addSuccessPhase(phase) - # Set failed phase info - failedPhase = bid_schema.setFailedPhase(PhaseInfo(phase=3, has_score=True, score=50, out_of=100)) + # Add failed phase info + failedPhase = bid_document.setFailedPhase(PhaseInfo(phase=3, has_score=True, score=50, out_of=100)) + + # Add feedback info + feedback = Feedback(description="Description of feedback", url="https://organisation.sharepoint.com/Docs/dummyfolder/feedback") + bid_document.addFeedback(feedback) # Convert the mock BidSchema object to a dictionary - bid_json = bid_schema.toDbCollection() + bid_json = bid_document.toDbCollection() # Save data in memory - f=open('./db.txt','a') - f.write(str(bid_json)) - f.close + save_in_memory('./db.txt', bid_json) return bid_json, 201 \ No newline at end of file diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py index f796c22..ef381c0 100644 --- a/api/schemas/bid_schema.py +++ b/api/schemas/bid_schema.py @@ -6,7 +6,7 @@ # Description: Schema for the bid object class BidSchema: - def __init__(self, tender, client, bid_date, alias='', bid_folder_url='', feedback='', failed={}, was_successful=False, success=[]): + def __init__(self, tender, client, bid_date, alias=None, bid_folder_url=None, feedback=None, failed=None, was_successful=False, success=[]): self.id = uuid4() self.tender = tender self.client = client @@ -25,11 +25,14 @@ def __init__(self, tender, client, bid_date, alias='', bid_folder_url='', feedba self.last_updated = datetime.now().isoformat() def addSuccessPhase(self, phase_info): - self.success.append(phase_info) + self.success.append(phase_info.__dict__) def setFailedPhase(self, phase_info): self.was_successful = False - self.failed = phase_info + self.failed = phase_info.__dict__ + + def addFeedback(self, feedback): + self.feedback = feedback.__dict__ def setStatus(self, status): if isinstance(status, Status): @@ -38,7 +41,5 @@ def setStatus(self, status): raise ValueError("Invalid status. Please provide a valid Status enum value") def toDbCollection(self): - self.success = [s.__dict__ for s in self.success] if self.success else [] - self.failed = self.failed.__dict__ if self.failed else None return self.__dict__ diff --git a/api/schemas/feedback_schema.py b/api/schemas/feedback_schema.py index c1d80c0..790afab 100644 --- a/api/schemas/feedback_schema.py +++ b/api/schemas/feedback_schema.py @@ -1,5 +1,5 @@ # Schema for Feedback object class Feedback: - def __init__(self,feedback_description, feedback_url): - self.feedback_description = feedback_description - self.feedback_url = feedback_url \ No newline at end of file + def __init__(self,description, url): + self.description = description + self.url = url \ No newline at end of file diff --git a/api/schemas/phase_schema.py b/api/schemas/phase_schema.py index 150de34..0e8d842 100644 --- a/api/schemas/phase_schema.py +++ b/api/schemas/phase_schema.py @@ -1,6 +1,6 @@ # Schema for phaseInfo object class PhaseInfo: - def __init__(self, phase, has_score, score=int, out_of=int): + def __init__(self, phase, has_score, score=None, out_of=None): self.phase = phase self.has_score = has_score self.score = score diff --git a/app.py b/app.py index 8622fdc..fadd3da 100644 --- a/app.py +++ b/app.py @@ -5,15 +5,15 @@ app = Flask(__name__) -SWAGGER_URL = '/api/docs' # URL for exposing Swagger UI (without trailing '/') -API_URL = '/static/swagger_config.yml' # Our API url (can of course be a local resource) +SWAGGER_URL = '/api/docs' # URL for exposing Swagger UI +API_URL = '/static/swagger_config.yml' # Our API url # Call factory function to create our blueprint swaggerui_blueprint = get_swaggerui_blueprint( SWAGGER_URL, # Swagger UI static files will be mapped to '{SWAGGER_URL}/dist/' API_URL, config={ # Swagger UI config overrides - 'app_name': "Test application" + 'app_name': "Bids API Swagger" }) app.register_blueprint(swaggerui_blueprint) diff --git a/db.txt b/db.txt index 12c401c..a0ac831 100644 --- a/db.txt +++ b/db.txt @@ -1,3 +1 @@ - -{'id': UUID('471fea1f-705c-4851-9a5b-df7bc2651428'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428', 'questions': '/bids/471fea1f-705c-4851-9a5b-df7bc2651428/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:16:59.404946'}{'id': UUID('0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8', 'questions': '/bids/0b695d23-5d4f-4cd7-b3a9-bb6497abd2f8/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T11:17:23.593171'}{'id': UUID('8fed99f8-3b7c-42ef-8c1f-fb3fe25ec039'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/8fed99f8-3b7c-42ef-8c1f-fb3fe25ec039', 'questions': '/bids/8fed99f8-3b7c-42ef-8c1f-fb3fe25ec039/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T15:57:34.571615'}{'id': UUID('96d69775-29af-46b1-aaf4-bfbdb1543412'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'deleted', 'links': {'self': '/bids/96d69775-29af-46b1-aaf4-bfbdb1543412', 'questions': '/bids/96d69775-29af-46b1-aaf4-bfbdb1543412/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-23T15:57:50.238595'} -{'id': UUID('a1729d42-3962-47ce-b54b-2a93e879891c'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'deleted', 'links': {'self': '/bids/a1729d42-3962-47ce-b54b-2a93e879891c', 'questions': '/bids/a1729d42-3962-47ce-b54b-2a93e879891c/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': 'test_description', 'url': 'test_url'}, 'last_updated': '2023-06-27T14:05:17.623827'}{'id': UUID('ab7bec38-f866-46ba-8e94-e85fec951326'), 'tender': 'Business Intelligence and Data Warehousing', 'client': 'Office for National Statistics', 'alias': 'ONS', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'https://organisation.sharepoint.com/Docs/dummyfolder', 'status': 'deleted', 'links': {'self': '/bids/ab7bec38-f866-46ba-8e94-e85fec951326', 'questions': '/bids/ab7bec38-f866-46ba-8e94-e85fec951326/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': '', 'url': ''}, 'last_updated': '2023-06-27T16:51:58.932142'}{'id': UUID('dfff0088-0ae0-4c80-8a32-e70c166453c2'), 'tender': 'Business Intelligence and Data Warehousing', 'client': 'Office for National Statistics', 'alias': 'ONS', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'https://organisation.sharepoint.com/Docs/dummyfolder', 'status': 'deleted', 'links': {'self': '/bids/dfff0088-0ae0-4c80-8a32-e70c166453c2', 'questions': '/bids/dfff0088-0ae0-4c80-8a32-e70c166453c2/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}, {'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'description': '', 'url': ''}, 'last_updated': '2023-06-27T16:52:00.527900'}{'id': UUID('ec9aded1-dc17-4eb2-ab2f-299a7db3f327'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'deleted', 'links': {'self': '/bids/ec9aded1-dc17-4eb2-ab2f-299a7db3f327', 'questions': '/bids/ec9aded1-dc17-4eb2-ab2f-299a7db3f327/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {}, 'last_updated': '2023-06-28T11:37:11.764715'}{'id': UUID('7e6c75ff-0401-48c2-bc33-27e831a30db7'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'deleted', 'links': {'self': '/bids/7e6c75ff-0401-48c2-bc33-27e831a30db7', 'questions': '/bids/7e6c75ff-0401-48c2-bc33-27e831a30db7/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T11:38:34.673343'}{'id': UUID('abf2422f-2632-4e75-ba7b-8cb827861813'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'deleted', 'links': {'self': '/bids/abf2422f-2632-4e75-ba7b-8cb827861813', 'questions': '/bids/abf2422f-2632-4e75-ba7b-8cb827861813/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {}, 'last_updated': '2023-06-28T11:39:09.216544'}{'id': UUID('3e72fff1-df91-4f2e-8758-5936a58ab216'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'deleted', 'links': {'self': '/bids/3e72fff1-df91-4f2e-8758-5936a58ab216', 'questions': '/bids/3e72fff1-df91-4f2e-8758-5936a58ab216/questions'}, 'was_successful': True, 'success': [{'phase': 2, 'has_score': True, 'score': 80, 'out_of': 100}], 'failed': {}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T11:39:31.225212'}{'id': UUID('2a705a23-7ae5-4b10-a802-fc19b42c252e'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': , 'links': {'self': '/bids/2a705a23-7ae5-4b10-a802-fc19b42c252e', 'questions': '/bids/2a705a23-7ae5-4b10-a802-fc19b42c252e/questions'}, 'was_successful': , 'success': [], 'failed': {}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T11:57:42.260083'}{'id': UUID('0ecaeab6-3cba-463a-8a83-51b289368c4c'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': , 'links': {'self': '/bids/0ecaeab6-3cba-463a-8a83-51b289368c4c', 'questions': '/bids/0ecaeab6-3cba-463a-8a83-51b289368c4c/questions'}, 'was_successful': , 'success': [], 'failed': {'test': 'test'}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T11:58:26.287023'}{'id': UUID('d8e4a941-3079-4917-94f2-e1b6d4fa92fa'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': , 'links': {'self': '/bids/d8e4a941-3079-4917-94f2-e1b6d4fa92fa', 'questions': '/bids/d8e4a941-3079-4917-94f2-e1b6d4fa92fa/questions'}, 'was_successful': , 'success': [], 'failed': {'test': 'test'}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T11:59:20.748727'}{'id': UUID('238f406d-69a1-4aa8-9614-182000bb4a35'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': , 'links': {'self': '/bids/238f406d-69a1-4aa8-9614-182000bb4a35', 'questions': '/bids/238f406d-69a1-4aa8-9614-182000bb4a35/questions'}, 'was_successful': , 'success': [], 'failed': {'test': 'test'}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:00:03.332593'}{'id': UUID('f11de2fa-c5b0-4ffa-918d-1dcc28d0c186'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': , 'links': {'self': '/bids/f11de2fa-c5b0-4ffa-918d-1dcc28d0c186', 'questions': '/bids/f11de2fa-c5b0-4ffa-918d-1dcc28d0c186/questions'}, 'was_successful': , 'success': [], 'failed': {'test': 'test'}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:00:45.022221'}{'id': UUID('3f1d1d43-df57-44d3-bdf9-0f6b50f48b57'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': '', 'links': {'self': '/bids/3f1d1d43-df57-44d3-bdf9-0f6b50f48b57', 'questions': '/bids/3f1d1d43-df57-44d3-bdf9-0f6b50f48b57/questions'}, 'was_successful': , 'success': [], 'failed': {'test': 'test'}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:01:16.003853'}{'id': UUID('9d1d19ed-36e7-47e6-b122-7ceb68c5a39f'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'IN_PROGRESS', 'links': {'self': '/bids/9d1d19ed-36e7-47e6-b122-7ceb68c5a39f', 'questions': '/bids/9d1d19ed-36e7-47e6-b122-7ceb68c5a39f/questions'}, 'was_successful': , 'success': [], 'failed': None, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:09:32.911235'}{'id': UUID('2c016b52-5e1f-407c-b3f3-245a2d6670ca'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'IN_PROGRESS', 'links': {'self': '/bids/2c016b52-5e1f-407c-b3f3-245a2d6670ca', 'questions': '/bids/2c016b52-5e1f-407c-b3f3-245a2d6670ca/questions'}, 'was_successful': True, 'success': [], 'failed': None, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:25:46.445061'}{'id': UUID('3dd937f5-ff70-4e04-90c3-93cf8a1fac8d'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': , 'links': {'self': '/bids/3dd937f5-ff70-4e04-90c3-93cf8a1fac8d', 'questions': '/bids/3dd937f5-ff70-4e04-90c3-93cf8a1fac8d/questions'}, 'was_successful': True, 'success': [], 'failed': None, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:26:36.844227'}{'id': UUID('4e9761d0-9a6e-4a73-a526-ca6e9809a133'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'IN_PROGRESS', 'links': {'self': '/bids/4e9761d0-9a6e-4a73-a526-ca6e9809a133', 'questions': '/bids/4e9761d0-9a6e-4a73-a526-ca6e9809a133/questions'}, 'was_successful': True, 'success': [], 'failed': None, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:26:50.230010'}{'id': UUID('1f26a4f6-3c4e-4965-967b-7940eb04577d'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/1f26a4f6-3c4e-4965-967b-7940eb04577d', 'questions': '/bids/1f26a4f6-3c4e-4965-967b-7940eb04577d/questions'}, 'was_successful': True, 'success': [], 'failed': None, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:27:07.915123'}{'id': UUID('ba9cfeaf-facf-49ef-8fd5-5b49b0e566ab'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/ba9cfeaf-facf-49ef-8fd5-5b49b0e566ab', 'questions': '/bids/ba9cfeaf-facf-49ef-8fd5-5b49b0e566ab/questions'}, 'was_successful': True, 'success': [], 'failed': None, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:29:09.037829'}{'id': UUID('4a42fcbe-246a-49ef-afc6-fa50a5aee424'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/4a42fcbe-246a-49ef-afc6-fa50a5aee424', 'questions': '/bids/4a42fcbe-246a-49ef-afc6-fa50a5aee424/questions'}, 'was_successful': False, 'success': [{'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, {'phase': 4, 'has_score': True, 'score': 50, 'out_of': 100}], 'failed': None, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:42:56.798021'}{'id': UUID('5b28fb9c-96df-4b89-a6b3-581cbecec177'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/5b28fb9c-96df-4b89-a6b3-581cbecec177', 'questions': '/bids/5b28fb9c-96df-4b89-a6b3-581cbecec177/questions'}, 'was_successful': False, 'success': [{'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, {'phase': 4, 'has_score': True, 'score': 50, 'out_of': 100}], 'failed': None, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:46:07.456529'}{'id': UUID('c8614428-a5ef-4a64-8571-4fdeafa1d60a'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/c8614428-a5ef-4a64-8571-4fdeafa1d60a', 'questions': '/bids/c8614428-a5ef-4a64-8571-4fdeafa1d60a/questions'}, 'was_successful': False, 'success': [{'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, {'phase': 4, 'has_score': True, 'score': 50, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:49:25.680814'}{'id': UUID('1690b875-900a-4292-9b75-8c20afd2b2d8'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/1690b875-900a-4292-9b75-8c20afd2b2d8', 'questions': '/bids/1690b875-900a-4292-9b75-8c20afd2b2d8/questions'}, 'was_successful': False, 'success': [{'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, {'phase': 4, 'has_score': True, 'score': 50, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:50:09.715531'}{'id': UUID('1890e1fa-10dc-453d-8f46-38d65386594a'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 'test_alias', 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/1890e1fa-10dc-453d-8f46-38d65386594a', 'questions': '/bids/1890e1fa-10dc-453d-8f46-38d65386594a/questions'}, 'was_successful': False, 'success': [{'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, {'phase': 4, 'has_score': True, 'score': 50, 'out_of': 100}, {'phase': 103, 'has_score': True, 'score': 50, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'feedback_description': 'test', 'feedback_url': 'url'}, 'last_updated': '2023-06-28T12:52:49.354905'} \ No newline at end of file +{'id': UUID('2404d08e-596a-45d5-917b-f081a6e31701'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 2, 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/2404d08e-596a-45d5-917b-f081a6e31701', 'questions': '/bids/2404d08e-596a-45d5-917b-f081a6e31701/questions'}, 'was_successful': False, 'success': [{'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, {'phase': 4, 'has_score': True, 'score': 50, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': '', 'last_updated': '2023-06-28T16:04:32.853309'}{'id': UUID('a728bac2-ff61-4a66-a215-be0d0593457d'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 2, 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/a728bac2-ff61-4a66-a215-be0d0593457d', 'questions': '/bids/a728bac2-ff61-4a66-a215-be0d0593457d/questions'}, 'was_successful': False, 'success': [{'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, {'phase': 4, 'has_score': True, 'score': 50, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': , 'last_updated': '2023-06-28T16:46:44.003149'}{'id': UUID('0d7e7577-04c0-4261-9334-44d876fc949f'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 2, 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/0d7e7577-04c0-4261-9334-44d876fc949f', 'questions': '/bids/0d7e7577-04c0-4261-9334-44d876fc949f/questions'}, 'was_successful': False, 'success': [{'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, {'phase': 4, 'has_score': True, 'score': 50, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'Description of feedback', 'url': 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback'}, 'last_updated': '2023-06-28T16:47:09.130934'} \ No newline at end of file diff --git a/helpers/helpers.py b/helpers/helpers.py index e69de29..04ecab2 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -0,0 +1,5 @@ +# Save data in memory +def save_in_memory(file, document): + f=open(file,'a') + f.write(str(document)) + f.close \ No newline at end of file diff --git a/test_request.http b/test_request.http index 637be19..2455b24 100644 --- a/test_request.http +++ b/test_request.http @@ -4,8 +4,7 @@ Content-Type: application/json { "tender": "test_tender", "client": "test_client", - "alias": "test_alias", + "alias": 2, "bid_date": "21-06-2023", - "bid_folder_url": "test_folder_url", - "feedback": {"feedback_description": "test", "feedback_url": "url"} + "bid_folder_url": "test_folder_url" } \ No newline at end of file From 8d7aaacacce3852f6cb2dc0dbf3e9390300a8f9c Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 29 Jun 2023 14:21:26 +0100 Subject: [PATCH 21/97] refactor: added marshmallow schemas; renamed classes and fixed imports; serialised bid_doc using schema --- api/controllers/bid_controller.py | 2 + api/models/bid_models.py | 33 +++++++------- api/schemas/bid_schema.py | 45 ++++++++++++------- api/schemas/feedback_schema.py | 8 +++- api/schemas/links_schema.py | 11 +++++ api/schemas/phase_schema.py | 12 ++++- .../{status_schema.py => status_enum.py} | 0 db.txt | 2 +- test_request.http | 2 +- 9 files changed, 77 insertions(+), 38 deletions(-) create mode 100644 api/schemas/links_schema.py rename api/schemas/{status_schema.py => status_enum.py} (100%) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 3a90122..02506f3 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -1,6 +1,8 @@ from flask import Blueprint +from marshmallow import ValidationError from api.models.bid_models import get_bids, create_bid +from helpers.helpers import save_in_memory bid = Blueprint('bid', __name__) diff --git a/api/models/bid_models.py b/api/models/bid_models.py index 9dfedfc..0de84cc 100644 --- a/api/models/bid_models.py +++ b/api/models/bid_models.py @@ -1,6 +1,6 @@ from flask import request, jsonify -from api.schemas.bid_schema import BidSchema -from api.schemas.phase_schema import PhaseInfo +from api.schemas.bid_schema import Bid, BidSchema +from api.schemas.phase_schema import Phase from api.schemas.feedback_schema import Feedback from helpers.helpers import save_in_memory @@ -12,16 +12,16 @@ def get_bids(): def create_bid(): - mandatory_fields = ['tender', 'client', 'bid_date'] - if not request.is_json: - return 'Invalid JSON', 400 + # mandatory_fields = ['tender', 'client', 'bid_date'] + # if not request.is_json: + # return 'Invalid JSON', 400 - for field in mandatory_fields: - if field not in request.json: - return 'Missing mandatory field: %s' % field, 400 + # for field in mandatory_fields: + # if field not in request.json: + # return 'Missing mandatory field: %s' % field, 400 # BidSchema object - bid_document = BidSchema( + bid_document = Bid( tender= request.json['tender'], client= request.json['client'], alias= request.json.get('alias', ''), @@ -30,21 +30,22 @@ def create_bid(): feedback= request.json.get('feedback', '') ) # Add successful phase info to success list - successPhases= [PhaseInfo(phase=3, has_score=True, score=50, out_of=100), PhaseInfo(phase=4, has_score=True, score=50, out_of=100)] + successPhases= [Phase(phase=3, has_score=True, score=50, out_of=100), Phase(phase=4, has_score=True, score=50, out_of=100)] for phase in successPhases: bid_document.addSuccessPhase(phase) # Add failed phase info - failedPhase = bid_document.setFailedPhase(PhaseInfo(phase=3, has_score=True, score=50, out_of=100)) + failedPhase = bid_document.setFailedPhase(Phase(phase=3, has_score=True, score=50, out_of=100)) # Add feedback info feedback = Feedback(description="Description of feedback", url="https://organisation.sharepoint.com/Docs/dummyfolder/feedback") bid_document.addFeedback(feedback) - # Convert the mock BidSchema object to a dictionary - bid_json = bid_document.toDbCollection() - + # Serialize bid_document object + schema = BidSchema() + data = schema.dump(bid_document) + # Save data in memory - save_in_memory('./db.txt', bid_json) + save_in_memory('./db.txt', data) - return bid_json, 201 \ No newline at end of file + return data, 201 \ No newline at end of file diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py index ef381c0..49bf994 100644 --- a/api/schemas/bid_schema.py +++ b/api/schemas/bid_schema.py @@ -1,11 +1,13 @@ from uuid import uuid4 from datetime import datetime -from .phase_schema import PhaseInfo -from .status_schema import Status -from .feedback_schema import Feedback +from marshmallow import Schema, fields +from .links_schema import Links, LinksSchema +from .phase_schema import Phase, PhaseSchema +from .feedback_schema import Feedback, FeedbackSchema +from .status_enum import Status # Description: Schema for the bid object -class BidSchema: +class Bid(): def __init__(self, tender, client, bid_date, alias=None, bid_folder_url=None, feedback=None, failed=None, was_successful=False, success=[]): self.id = uuid4() self.tender = tender @@ -13,33 +15,42 @@ def __init__(self, tender, client, bid_date, alias=None, bid_folder_url=None, fe self.alias = alias self.bid_date = datetime.strptime(bid_date, '%d-%m-%Y').isoformat() # DD-MM-YYYY self.bid_folder_url = bid_folder_url - self.status = Status.IN_PROGRESS.value # enum: "deleted", "in_progress" or "completed" - self.links = { - 'self': f"/bids/{self.id}", - 'questions': f"/bids/{self.id}/questions" - } + self.status = Status.IN_PROGRESS # enum: "deleted", "in_progress" or "completed" + self.links = Links(self.id) self.was_successful = was_successful self.success = success self.failed = failed self.feedback = feedback self.last_updated = datetime.now().isoformat() - def addSuccessPhase(self, phase_info): - self.success.append(phase_info.__dict__) + def addSuccessPhase(self, phase): + self.success.append(phase) - def setFailedPhase(self, phase_info): + def setFailedPhase(self, phase): self.was_successful = False - self.failed = phase_info.__dict__ + self.failed = phase def addFeedback(self, feedback): - self.feedback = feedback.__dict__ + self.feedback = feedback def setStatus(self, status): if isinstance(status, Status): self.status = status.value else: raise ValueError("Invalid status. Please provide a valid Status enum value") - - def toDbCollection(self): - return self.__dict__ +# Marshmallow schema +class BidSchema(Schema): + id = fields.Str(required=True) + tender = fields.Str(required=True) + client = fields.Str(required=True) + alias = fields.Str() + bid_date = fields.Str(required=True) + bid_folder_url = fields.Str() + status = fields.Enum(Status, required=True) + links = fields.Nested(LinksSchema, required=True) + was_successful = fields.Bool(required=True) + success = fields.List(fields.Nested(PhaseSchema)) + failed = fields.Nested(PhaseSchema) + feedback = fields.Nested(FeedbackSchema) + last_updated = fields.Str(required=True) \ No newline at end of file diff --git a/api/schemas/feedback_schema.py b/api/schemas/feedback_schema.py index 790afab..f27a6da 100644 --- a/api/schemas/feedback_schema.py +++ b/api/schemas/feedback_schema.py @@ -1,5 +1,11 @@ +from marshmallow import Schema, fields + # Schema for Feedback object class Feedback: def __init__(self,description, url): self.description = description - self.url = url \ No newline at end of file + self.url = url + +class FeedbackSchema(Schema): + description = fields.Str(required=True) + url = fields.Str(required=True) \ No newline at end of file diff --git a/api/schemas/links_schema.py b/api/schemas/links_schema.py new file mode 100644 index 0000000..16a9243 --- /dev/null +++ b/api/schemas/links_schema.py @@ -0,0 +1,11 @@ +from marshmallow import Schema, fields + +# Schema for links object +class Links(): + def __init__(self, id): + self.self = f"/bids/{id}" + self.questions = f"/bids/{id}/questions" + +class LinksSchema(Schema): + self = fields.Str(required=True) + questions = fields.Str(required=True) \ No newline at end of file diff --git a/api/schemas/phase_schema.py b/api/schemas/phase_schema.py index 0e8d842..376b9a7 100644 --- a/api/schemas/phase_schema.py +++ b/api/schemas/phase_schema.py @@ -1,7 +1,15 @@ +from marshmallow import Schema, fields + # Schema for phaseInfo object -class PhaseInfo: +class Phase: def __init__(self, phase, has_score, score=None, out_of=None): self.phase = phase self.has_score = has_score self.score = score - self.out_of = out_of \ No newline at end of file + self.out_of = out_of + +class PhaseSchema(Schema): + phase = fields.Int(required=True) + has_score = fields.Bool(required=True) + score = fields.Int() + out_of = fields.Int() \ No newline at end of file diff --git a/api/schemas/status_schema.py b/api/schemas/status_enum.py similarity index 100% rename from api/schemas/status_schema.py rename to api/schemas/status_enum.py diff --git a/db.txt b/db.txt index a0ac831..1d1f97d 100644 --- a/db.txt +++ b/db.txt @@ -1 +1 @@ -{'id': UUID('2404d08e-596a-45d5-917b-f081a6e31701'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 2, 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/2404d08e-596a-45d5-917b-f081a6e31701', 'questions': '/bids/2404d08e-596a-45d5-917b-f081a6e31701/questions'}, 'was_successful': False, 'success': [{'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, {'phase': 4, 'has_score': True, 'score': 50, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': '', 'last_updated': '2023-06-28T16:04:32.853309'}{'id': UUID('a728bac2-ff61-4a66-a215-be0d0593457d'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 2, 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/a728bac2-ff61-4a66-a215-be0d0593457d', 'questions': '/bids/a728bac2-ff61-4a66-a215-be0d0593457d/questions'}, 'was_successful': False, 'success': [{'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, {'phase': 4, 'has_score': True, 'score': 50, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': , 'last_updated': '2023-06-28T16:46:44.003149'}{'id': UUID('0d7e7577-04c0-4261-9334-44d876fc949f'), 'tender': 'test_tender', 'client': 'test_client', 'alias': 2, 'bid_date': '2023-06-21T00:00:00', 'bid_folder_url': 'test_folder_url', 'status': 'in_progress', 'links': {'self': '/bids/0d7e7577-04c0-4261-9334-44d876fc949f', 'questions': '/bids/0d7e7577-04c0-4261-9334-44d876fc949f/questions'}, 'was_successful': False, 'success': [{'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, {'phase': 4, 'has_score': True, 'score': 50, 'out_of': 100}], 'failed': {'phase': 3, 'has_score': True, 'score': 50, 'out_of': 100}, 'feedback': {'description': 'Description of feedback', 'url': 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback'}, 'last_updated': '2023-06-28T16:47:09.130934'} \ No newline at end of file +{'bid_folder_url': 'test_folder_url', 'status': 'IN_PROGRESS', 'failed': {}, 'last_updated': '2023-06-29T14:17:36.861088', 'links': {'questions': '/bids/e74c0216-f31c-4fb8-a83f-d7d16bfd0bf0/questions', 'self': '/bids/e74c0216-f31c-4fb8-a83f-d7d16bfd0bf0'}, 'alias': 'TDSE', 'id': 'e74c0216-f31c-4fb8-a83f-d7d16bfd0bf0', 'success': [{'score': 50, 'phase': 3, 'has_score': True, 'out_of': 100}, {'score': 50, 'phase': 4, 'has_score': True, 'out_of': 100}], 'feedback': {'url': 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback', 'description': 'Description of feedback'}, 'was_successful': False, 'client': 'test_client', 'tender': 'test_tender', 'bid_date': '2023-06-21T00:00:00'}{'id': '8badb6d7-b21c-491e-81ef-588b1b7cff2a', 'alias': 'TDSE', 'tender': 'test_tender', 'bid_folder_url': 'test_folder_url', 'was_successful': False, 'client': 'test_client', 'failed': {'score': 50, 'out_of': 100, 'has_score': True, 'phase': 3}, 'feedback': {'description': 'Description of feedback', 'url': 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback'}, 'last_updated': '2023-06-29T14:18:34.141923', 'bid_date': '2023-06-21T00:00:00', 'status': 'IN_PROGRESS', 'links': {'questions': '/bids/8badb6d7-b21c-491e-81ef-588b1b7cff2a/questions', 'self': '/bids/8badb6d7-b21c-491e-81ef-588b1b7cff2a'}, 'success': [{'score': 50, 'out_of': 100, 'has_score': True, 'phase': 3}, {'score': 50, 'out_of': 100, 'has_score': True, 'phase': 4}]} \ No newline at end of file diff --git a/test_request.http b/test_request.http index 2455b24..2088905 100644 --- a/test_request.http +++ b/test_request.http @@ -4,7 +4,7 @@ Content-Type: application/json { "tender": "test_tender", "client": "test_client", - "alias": 2, + "alias": "TDSE", "bid_date": "21-06-2023", "bid_folder_url": "test_folder_url" } \ No newline at end of file From 1b48bc2ebc41b4b3a9d1ef1c150648f57fa1956a Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 30 Jun 2023 14:32:50 +0100 Subject: [PATCH 22/97] refactor: handling ValidationError with marshmallow and outputting custom error response --- api/controllers/bid_controller.py | 16 +++--- api/models/bid_models.py | 50 +++---------------- api/schemas/400_error.py | 0 api/schemas/bid_request.py | 27 ++++++++++ api/schemas/bid_schema.py | 25 +++++----- api/schemas/feedback_schema.py | 2 +- api/schemas/links_schema.py | 2 +- api/schemas/phase_schema.py | 6 +-- db.txt | 2 +- request_examples/all_fields.http | 32 ++++++++++++ request_examples/missing_mandatory_field.http | 31 ++++++++++++ static/swagger_config.yml | 3 ++ test_request.http | 10 ---- 13 files changed, 129 insertions(+), 77 deletions(-) create mode 100644 api/schemas/400_error.py create mode 100644 api/schemas/bid_request.py create mode 100644 request_examples/all_fields.http create mode 100644 request_examples/missing_mandatory_field.http delete mode 100644 test_request.http diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 02506f3..ba0a5b8 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -1,7 +1,6 @@ - -from flask import Blueprint -from marshmallow import ValidationError +from flask import Blueprint, jsonify from api.models.bid_models import get_bids, create_bid +from api.schemas.bid_request import BidRequestSchema from helpers.helpers import save_in_memory bid = Blueprint('bid', __name__) @@ -13,6 +12,11 @@ def get_all_bids(): @bid.route("/bids", methods=["POST"]) def post_bid(): - response = create_bid() - return response - + try: + bid_document = create_bid() + # Serialize to a JSON-encoded string + data = BidRequestSchema().dumps(bid_document) + save_in_memory('./db.txt', data) + return data, 200 + except Exception as e: + return jsonify({"error": str(e)}), 400 diff --git a/api/models/bid_models.py b/api/models/bid_models.py index 0de84cc..80d31c9 100644 --- a/api/models/bid_models.py +++ b/api/models/bid_models.py @@ -1,51 +1,15 @@ -from flask import request, jsonify -from api.schemas.bid_schema import Bid, BidSchema -from api.schemas.phase_schema import Phase -from api.schemas.feedback_schema import Feedback -from helpers.helpers import save_in_memory +from flask import request +from api.schemas.bid_schema import BidModel +from api.schemas.bid_request import BidRequestSchema +from api.schemas.phase_schema import PhaseModel +from api.schemas.feedback_schema import FeedbackModel def get_bids(): f = open('./db.txt','r') bids = f.read() f.close() return bids, 200 - def create_bid(): - # mandatory_fields = ['tender', 'client', 'bid_date'] - # if not request.is_json: - # return 'Invalid JSON', 400 - - # for field in mandatory_fields: - # if field not in request.json: - # return 'Missing mandatory field: %s' % field, 400 - - # BidSchema object - bid_document = Bid( - tender= request.json['tender'], - client= request.json['client'], - alias= request.json.get('alias', ''), - bid_date= request.json['bid_date'], - bid_folder_url= request.json.get('bid_folder_url', ''), - feedback= request.json.get('feedback', '') - ) - # Add successful phase info to success list - successPhases= [Phase(phase=3, has_score=True, score=50, out_of=100), Phase(phase=4, has_score=True, score=50, out_of=100)] - for phase in successPhases: - bid_document.addSuccessPhase(phase) - - # Add failed phase info - failedPhase = bid_document.setFailedPhase(Phase(phase=3, has_score=True, score=50, out_of=100)) - - # Add feedback info - feedback = Feedback(description="Description of feedback", url="https://organisation.sharepoint.com/Docs/dummyfolder/feedback") - bid_document.addFeedback(feedback) - - # Serialize bid_document object - schema = BidSchema() - data = schema.dump(bid_document) - - # Save data in memory - save_in_memory('./db.txt', data) - - return data, 201 \ No newline at end of file + bid= BidRequestSchema().load(request.json) + return bid \ No newline at end of file diff --git a/api/schemas/400_error.py b/api/schemas/400_error.py new file mode 100644 index 0000000..e69de29 diff --git a/api/schemas/bid_request.py b/api/schemas/bid_request.py new file mode 100644 index 0000000..fbe8f2b --- /dev/null +++ b/api/schemas/bid_request.py @@ -0,0 +1,27 @@ +from marshmallow import Schema, fields, post_load +from .bid_schema import BidModel +from .links_schema import LinksModel, LinksSchema +from .phase_schema import PhaseModel, PhaseSchema +from .feedback_schema import FeedbackModel, FeedbackSchema +from .status_enum import Status + +# Marshmallow schema for request body +class BidRequestSchema(Schema): + id = fields.UUID() + tender = fields.Str(required=True, error_messages={"required": {"message": "Missing mandatory field"}}) + client = fields.Str(required=True, error_messages={"required": {"message": "Missing mandatory field"}}) + alias = fields.Str() + bid_date = fields.Str(required=True, error_messages={"required": {"message": "Missing mandatory field"}}) + bid_folder_url = fields.Str() + status = fields.Enum(Status, by_value=True) + links = fields.Nested(LinksSchema) + was_successful = fields.Bool() + success = fields.List(fields.Nested(PhaseSchema)) + failed = fields.Nested(PhaseSchema) + feedback = fields.Nested(FeedbackSchema) + last_updated = fields.DateTime() + + # Creates a Bid instance after processing + @post_load + def makeBid(self, data, **kwargs): + return BidModel(**data) \ No newline at end of file diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py index 49bf994..54141e1 100644 --- a/api/schemas/bid_schema.py +++ b/api/schemas/bid_schema.py @@ -1,27 +1,28 @@ from uuid import uuid4 from datetime import datetime -from marshmallow import Schema, fields -from .links_schema import Links, LinksSchema -from .phase_schema import Phase, PhaseSchema -from .feedback_schema import Feedback, FeedbackSchema +from marshmallow import Schema, fields, post_load +from .links_schema import LinksModel, LinksSchema +from .phase_schema import PhaseModel, PhaseSchema +from .feedback_schema import FeedbackModel, FeedbackSchema from .status_enum import Status # Description: Schema for the bid object -class Bid(): +class BidModel(): def __init__(self, tender, client, bid_date, alias=None, bid_folder_url=None, feedback=None, failed=None, was_successful=False, success=[]): self.id = uuid4() self.tender = tender self.client = client self.alias = alias - self.bid_date = datetime.strptime(bid_date, '%d-%m-%Y').isoformat() # DD-MM-YYYY + self.bid_date = datetime.strptime(bid_date, '%d-%m-%Y') # DD-MM-YYYY self.bid_folder_url = bid_folder_url self.status = Status.IN_PROGRESS # enum: "deleted", "in_progress" or "completed" - self.links = Links(self.id) + self.links = LinksModel(self.id) self.was_successful = was_successful self.success = success self.failed = failed self.feedback = feedback - self.last_updated = datetime.now().isoformat() + self.last_updated = datetime.now() + def addSuccessPhase(self, phase): self.success.append(phase) @@ -41,16 +42,16 @@ def setStatus(self, status): # Marshmallow schema class BidSchema(Schema): - id = fields.Str(required=True) + id = fields.UUID(required=True) tender = fields.Str(required=True) client = fields.Str(required=True) alias = fields.Str() - bid_date = fields.Str(required=True) + bid_date = fields.Date(required=True) bid_folder_url = fields.Str() - status = fields.Enum(Status, required=True) + status = fields.Enum(Status, by_value=True, required=True) links = fields.Nested(LinksSchema, required=True) was_successful = fields.Bool(required=True) success = fields.List(fields.Nested(PhaseSchema)) failed = fields.Nested(PhaseSchema) feedback = fields.Nested(FeedbackSchema) - last_updated = fields.Str(required=True) \ No newline at end of file + last_updated = fields.DateTime(required=True) \ No newline at end of file diff --git a/api/schemas/feedback_schema.py b/api/schemas/feedback_schema.py index f27a6da..1e7acdb 100644 --- a/api/schemas/feedback_schema.py +++ b/api/schemas/feedback_schema.py @@ -1,7 +1,7 @@ from marshmallow import Schema, fields # Schema for Feedback object -class Feedback: +class FeedbackModel: def __init__(self,description, url): self.description = description self.url = url diff --git a/api/schemas/links_schema.py b/api/schemas/links_schema.py index 16a9243..d0c223d 100644 --- a/api/schemas/links_schema.py +++ b/api/schemas/links_schema.py @@ -1,7 +1,7 @@ from marshmallow import Schema, fields # Schema for links object -class Links(): +class LinksModel(): def __init__(self, id): self.self = f"/bids/{id}" self.questions = f"/bids/{id}/questions" diff --git a/api/schemas/phase_schema.py b/api/schemas/phase_schema.py index 376b9a7..d555efc 100644 --- a/api/schemas/phase_schema.py +++ b/api/schemas/phase_schema.py @@ -1,7 +1,7 @@ from marshmallow import Schema, fields # Schema for phaseInfo object -class Phase: +class PhaseModel: def __init__(self, phase, has_score, score=None, out_of=None): self.phase = phase self.has_score = has_score @@ -11,5 +11,5 @@ def __init__(self, phase, has_score, score=None, out_of=None): class PhaseSchema(Schema): phase = fields.Int(required=True) has_score = fields.Bool(required=True) - score = fields.Int() - out_of = fields.Int() \ No newline at end of file + score = fields.Int(strict=True) + out_of = fields.Int(strict=True) \ No newline at end of file diff --git a/db.txt b/db.txt index 1d1f97d..1c2571f 100644 --- a/db.txt +++ b/db.txt @@ -1 +1 @@ -{'bid_folder_url': 'test_folder_url', 'status': 'IN_PROGRESS', 'failed': {}, 'last_updated': '2023-06-29T14:17:36.861088', 'links': {'questions': '/bids/e74c0216-f31c-4fb8-a83f-d7d16bfd0bf0/questions', 'self': '/bids/e74c0216-f31c-4fb8-a83f-d7d16bfd0bf0'}, 'alias': 'TDSE', 'id': 'e74c0216-f31c-4fb8-a83f-d7d16bfd0bf0', 'success': [{'score': 50, 'phase': 3, 'has_score': True, 'out_of': 100}, {'score': 50, 'phase': 4, 'has_score': True, 'out_of': 100}], 'feedback': {'url': 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback', 'description': 'Description of feedback'}, 'was_successful': False, 'client': 'test_client', 'tender': 'test_tender', 'bid_date': '2023-06-21T00:00:00'}{'id': '8badb6d7-b21c-491e-81ef-588b1b7cff2a', 'alias': 'TDSE', 'tender': 'test_tender', 'bid_folder_url': 'test_folder_url', 'was_successful': False, 'client': 'test_client', 'failed': {'score': 50, 'out_of': 100, 'has_score': True, 'phase': 3}, 'feedback': {'description': 'Description of feedback', 'url': 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback'}, 'last_updated': '2023-06-29T14:18:34.141923', 'bid_date': '2023-06-21T00:00:00', 'status': 'IN_PROGRESS', 'links': {'questions': '/bids/8badb6d7-b21c-491e-81ef-588b1b7cff2a/questions', 'self': '/bids/8badb6d7-b21c-491e-81ef-588b1b7cff2a'}, 'success': [{'score': 50, 'out_of': 100, 'has_score': True, 'phase': 3}, {'score': 50, 'out_of': 100, 'has_score': True, 'phase': 4}]} \ No newline at end of file +{"feedback": null, "was_successful": false, "bid_date": "2023-06-21 00:00:00", "success": [], "last_updated": "2023-06-30T10:46:35.038544", "bid_folder_url": null, "alias": null, "tender": "test_tender", "client": "test_client", "id": "f0c82c9f-4308-459e-a2ce-d7b7eb4abb80", "failed": null, "links": {"questions": "/bids/f0c82c9f-4308-459e-a2ce-d7b7eb4abb80/questions", "self": "/bids/f0c82c9f-4308-459e-a2ce-d7b7eb4abb80"}, "status": "in_progress"}{"last_updated": "2023-06-30T11:06:42.198542", "client": "Office for National Statistics", "was_successful": false, "success": [{"phase": 1, "out_of": 36, "has_score": true, "score": 28}, {"phase": 2, "has_score": false}], "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "links": {"self": "/bids/a64b131b-63c5-4003-9226-5ca33155bab2", "questions": "/bids/a64b131b-63c5-4003-9226-5ca33155bab2/questions"}, "status": "in_progress", "failed": {"phase": 3, "out_of": 36, "has_score": true, "score": 20}, "tender": "Business Intelligence and Data Warehousing", "alias": "ONS", "id": "a64b131b-63c5-4003-9226-5ca33155bab2", "feedback": {"description": "Feedback from client in detail", "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback"}, "bid_date": "2023-06-21 00:00:00"}{"was_successful": false, "id": "265538c5-85cc-4d76-affb-b302b9d5a5dd", "status": "in_progress", "failed": {"score": 20, "phase": 3, "has_score": true, "out_of": 36}, "tender": "Business Intelligence and Data Warehousing", "last_updated": "2023-06-30T11:10:23.464798", "alias": "ONS", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "success": [{"score": 28, "phase": 1, "has_score": true, "out_of": 36}, {"phase": 2, "has_score": false}], "bid_date": "2023-06-21 00:00:00", "links": {"self": "/bids/265538c5-85cc-4d76-affb-b302b9d5a5dd", "questions": "/bids/265538c5-85cc-4d76-affb-b302b9d5a5dd/questions"}, "feedback": {"url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", "description": "Feedback from client in detail"}, "client": "Office for National Statistics"}{"was_successful": false, "id": "b4323b7d-4775-4fd3-aedb-2253baec1986", "status": "in_progress", "failed": {"score": 20, "phase": 3, "has_score": true, "out_of": 36}, "tender": "Business Intelligence and Data Warehousing", "last_updated": "2023-06-30T11:21:15.618412", "alias": "ONS", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "success": [{"score": 28, "phase": 1, "has_score": true, "out_of": 36}, {"phase": 2, "has_score": false}], "bid_date": "2023-06-21 00:00:00", "links": {"self": "/bids/b4323b7d-4775-4fd3-aedb-2253baec1986", "questions": "/bids/b4323b7d-4775-4fd3-aedb-2253baec1986/questions"}, "feedback": {"url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", "description": "Feedback from client in detail"}, "client": "Office for National Statistics"}{}{}{}{}{"status": "in_progress", "success": [{"score": 28, "phase": 1, "out_of": 36, "has_score": true}, {"phase": 2, "has_score": false}], "bid_date": "2023-06-21 00:00:00", "client": "Office for National Statistics", "links": {"questions": "/bids/ed7fbe1d-4c29-433a-bf4c-2bbb7fe664cf/questions", "self": "/bids/ed7fbe1d-4c29-433a-bf4c-2bbb7fe664cf"}, "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "failed": {"score": 20, "phase": 3, "out_of": 36, "has_score": true}, "was_successful": false, "last_updated": "2023-06-30T12:21:52.684258", "tender": "Business Intelligence and Data Warehousing", "alias": "ONS", "feedback": {"description": "Feedback from client in detail", "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback"}, "id": "ed7fbe1d-4c29-433a-bf4c-2bbb7fe664cf"}{}{'bid_folder_url': 'https://organisation.sharepoint.com/Docs/dummyfolder', 'client': 'Office for National Statistics', 'failed': {'phase': 3, 'out_of': 36, 'has_score': True, 'score': 20}, 'bid_date': '2023-06-21 00:00:00', 'id': '77becc93-f933-49a9-92b0-817d7615971a', 'links': {'self': '/bids/77becc93-f933-49a9-92b0-817d7615971a', 'questions': '/bids/77becc93-f933-49a9-92b0-817d7615971a/questions'}, 'was_successful': False, 'status': 'in_progress', 'feedback': {'description': 'Feedback from client in detail', 'url': 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback'}, 'tender': 'Business Intelligence and Data Warehousing', 'success': [{'phase': 1, 'out_of': 36, 'has_score': True, 'score': 28}, {'phase': 2, 'has_score': False}], 'alias': 'ONS', 'last_updated': '2023-06-30T14:05:22.864455'}{'client': 'Office for National Statistics', 'alias': 'ONS', 'failed': {'score': 20, 'out_of': 36, 'has_score': True, 'phase': 3}, 'bid_date': '2023-06-21 00:00:00', 'status': 'in_progress', 'bid_folder_url': 'https://organisation.sharepoint.com/Docs/dummyfolder', 'feedback': {'description': 'Feedback from client in detail', 'url': 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback'}, 'links': {'self': '/bids/76a31020-0663-4172-9f28-fe3b0c5e8f8f', 'questions': '/bids/76a31020-0663-4172-9f28-fe3b0c5e8f8f/questions'}, 'was_successful': False, 'tender': 'Business Intelligence and Data Warehousing', 'last_updated': '2023-06-30T14:06:11.867540', 'id': '76a31020-0663-4172-9f28-fe3b0c5e8f8f', 'success': [{'score': 28, 'out_of': 36, 'has_score': True, 'phase': 1}, {'has_score': False, 'phase': 2}]}{'last_updated': '2023-06-30T14:24:59.590777', 'status': 'in_progress', 'bid_date': '2023-06-21 00:00:00', 'tender': 'Business Intelligence and Data Warehousing', 'client': 'Office for National Statistics', 'id': '4ca2e044-16ef-4eb5-bbd6-c90b93a92fed', 'feedback': {'url': 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback', 'description': 'Feedback from client in detail'}, 'alias': 'ONS', 'failed': {'phase': 3, 'out_of': 36, 'score': 20, 'has_score': True}, 'bid_folder_url': 'https://organisation.sharepoint.com/Docs/dummyfolder', 'links': {'questions': '/bids/4ca2e044-16ef-4eb5-bbd6-c90b93a92fed/questions', 'self': '/bids/4ca2e044-16ef-4eb5-bbd6-c90b93a92fed'}, 'success': [{'phase': 1, 'out_of': 36, 'score': 28, 'has_score': True}, {'phase': 2, 'has_score': False}], 'was_successful': False}{"last_updated": "2023-06-30T14:26:02.449526", "alias": "ONS", "links": {"self": "/bids/07ba99d0-0ac6-4230-a479-d6f9a3291800", "questions": "/bids/07ba99d0-0ac6-4230-a479-d6f9a3291800/questions"}, "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "tender": "Business Intelligence and Data Warehousing", "status": "in_progress", "success": [{"has_score": true, "phase": 1, "out_of": 36, "score": 28}, {"has_score": false, "phase": 2}], "failed": {"has_score": true, "phase": 3, "out_of": 36, "score": 20}, "feedback": {"url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", "description": "Feedback from client in detail"}, "bid_date": "2023-06-21 00:00:00", "was_successful": false, "id": "07ba99d0-0ac6-4230-a479-d6f9a3291800", "client": "Office for National Statistics"}{"last_updated": "2023-06-30T14:26:06.054507", "alias": "ONS", "links": {"self": "/bids/da990886-af06-40db-bb0c-135a8623144e", "questions": "/bids/da990886-af06-40db-bb0c-135a8623144e/questions"}, "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "tender": "Business Intelligence and Data Warehousing", "status": "in_progress", "success": [{"has_score": true, "phase": 1, "out_of": 36, "score": 28}, {"has_score": false, "phase": 2}], "failed": {"has_score": true, "phase": 3, "out_of": 36, "score": 20}, "feedback": {"url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", "description": "Feedback from client in detail"}, "bid_date": "2023-06-21 00:00:00", "was_successful": false, "id": "da990886-af06-40db-bb0c-135a8623144e", "client": "Office for National Statistics"}{"bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "last_updated": "2023-06-30T14:26:47.750826", "links": {"self": "/bids/e567482f-6e18-47b7-8e32-f720934c2879", "questions": "/bids/e567482f-6e18-47b7-8e32-f720934c2879/questions"}, "client": "Office for National Statistics", "was_successful": false, "alias": "ONS", "feedback": {"url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", "description": "Feedback from client in detail"}, "bid_date": "2023-06-21 00:00:00", "id": "e567482f-6e18-47b7-8e32-f720934c2879", "success": [{"score": 28, "out_of": 36, "has_score": true, "phase": 1}, {"has_score": false, "phase": 2}], "failed": {"score": 20, "out_of": 36, "has_score": true, "phase": 3}, "status": "in_progress", "tender": "Business Intelligence and Data Warehousing"}{"bid_date": "2023-06-21 00:00:00", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "id": "33fa9363-c08b-4db9-9ac0-2dc051f8b9bc", "client": "Office for National Statistics", "status": "in_progress", "feedback": {"description": "Feedback from client in detail", "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback"}, "alias": "ONS", "was_successful": false, "last_updated": "2023-06-30T14:31:38.313631", "tender": "Business Intelligence and Data Warehousing", "success": [{"out_of": 36, "has_score": true, "phase": 1, "score": 28}, {"has_score": false, "phase": 2}], "links": {"self": "/bids/33fa9363-c08b-4db9-9ac0-2dc051f8b9bc", "questions": "/bids/33fa9363-c08b-4db9-9ac0-2dc051f8b9bc/questions"}, "failed": {"out_of": 36, "has_score": true, "phase": 3, "score": 20}} \ No newline at end of file diff --git a/request_examples/all_fields.http b/request_examples/all_fields.http new file mode 100644 index 0000000..9404e9a --- /dev/null +++ b/request_examples/all_fields.http @@ -0,0 +1,32 @@ +POST http://localhost:3000/api/bids HTTP/1.1 +Content-Type: application/json + +{ + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "success": [ + { + "phase": 1, + "has_score": true, + "score": 28, + "out_of": 36 + }, + { + "phase": 2, + "has_score": false + } + ], + "failed": { + "phase": 3, + "has_score": true, + "score": 20, + "out_of": 36 + } +} \ No newline at end of file diff --git a/request_examples/missing_mandatory_field.http b/request_examples/missing_mandatory_field.http new file mode 100644 index 0000000..a06e817 --- /dev/null +++ b/request_examples/missing_mandatory_field.http @@ -0,0 +1,31 @@ +POST http://localhost:3000/api/bids HTTP/1.1 +Content-Type: application/json + +{ + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "success": [ + { + "phase": 1, + "has_score": true, + "score": 28, + "out_of": 36 + }, + { + "phase": 2, + "has_score": false + } + ], + "failed": { + "phase": 3, + "has_score": true, + "score": 20, + "out_of": 36 + } +} \ No newline at end of file diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 12df7f1..c2354da 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -98,6 +98,9 @@ components: tender: type: string example: 'Business Intelligence and Data Warehousing' + bid_folder_url: + type: string + example: 'https://organisation.sharepoint.com/Docs/dummyfolder' last_updated: type: string example: "2023-06-27T14:05:17.623827" diff --git a/test_request.http b/test_request.http deleted file mode 100644 index 2088905..0000000 --- a/test_request.http +++ /dev/null @@ -1,10 +0,0 @@ -POST http://localhost:3000/api/bids HTTP/1.1 -Content-Type: application/json - -{ - "tender": "test_tender", - "client": "test_client", - "alias": "TDSE", - "bid_date": "21-06-2023", - "bid_folder_url": "test_folder_url" -} \ No newline at end of file From ed395ce72c5d6c3a9f8fe6202b5af0d47f6c37c5 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 30 Jun 2023 15:20:04 +0100 Subject: [PATCH 23/97] refactor: added test request bodies to check validation --- api/controllers/bid_controller.py | 15 +++++++------ api/models/bid_models.py | 6 ++---- api/schemas/400_error.py | 0 api/schemas/bid_request.py | 10 ++++----- api/schemas/bid_schema.py | 4 ++-- api/schemas/phase_schema.py | 2 +- db.txt | 2 +- request_examples/invalid_int.http | 32 ++++++++++++++++++++++++++++ request_examples/invalid_string.http | 32 ++++++++++++++++++++++++++++ request_examples/invalid_url.http | 32 ++++++++++++++++++++++++++++ static/swagger_config.yml | 16 +++++++------- 11 files changed, 124 insertions(+), 27 deletions(-) delete mode 100644 api/schemas/400_error.py create mode 100644 request_examples/invalid_int.http create mode 100644 request_examples/invalid_string.http create mode 100644 request_examples/invalid_url.http diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index ba0a5b8..d2a2905 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -1,6 +1,7 @@ from flask import Blueprint, jsonify +from marshmallow import ValidationError from api.models.bid_models import get_bids, create_bid -from api.schemas.bid_request import BidRequestSchema +from api.schemas.bid_schema import BidSchema from helpers.helpers import save_in_memory bid = Blueprint('bid', __name__) @@ -12,11 +13,13 @@ def get_all_bids(): @bid.route("/bids", methods=["POST"]) def post_bid(): + # Create bid document and return error if input validation fails try: bid_document = create_bid() - # Serialize to a JSON-encoded string - data = BidRequestSchema().dumps(bid_document) - save_in_memory('./db.txt', data) - return data, 200 - except Exception as e: + except ValidationError as e: return jsonify({"error": str(e)}), 400 + + # Serialize to a JSON-encoded string + data = BidSchema().dumps(bid_document) + save_in_memory('./db.txt', data) + return data, 200 \ No newline at end of file diff --git a/api/models/bid_models.py b/api/models/bid_models.py index 80d31c9..528e2e7 100644 --- a/api/models/bid_models.py +++ b/api/models/bid_models.py @@ -1,8 +1,5 @@ from flask import request -from api.schemas.bid_schema import BidModel from api.schemas.bid_request import BidRequestSchema -from api.schemas.phase_schema import PhaseModel -from api.schemas.feedback_schema import FeedbackModel def get_bids(): f = open('./db.txt','r') @@ -11,5 +8,6 @@ def get_bids(): return bids, 200 def create_bid(): - bid= BidRequestSchema().load(request.json) + # De-serialize request body and validate against Marshmallow schema + bid = BidRequestSchema().load(request.json) return bid \ No newline at end of file diff --git a/api/schemas/400_error.py b/api/schemas/400_error.py deleted file mode 100644 index e69de29..0000000 diff --git a/api/schemas/bid_request.py b/api/schemas/bid_request.py index fbe8f2b..247bb23 100644 --- a/api/schemas/bid_request.py +++ b/api/schemas/bid_request.py @@ -1,8 +1,8 @@ from marshmallow import Schema, fields, post_load from .bid_schema import BidModel -from .links_schema import LinksModel, LinksSchema -from .phase_schema import PhaseModel, PhaseSchema -from .feedback_schema import FeedbackModel, FeedbackSchema +from .links_schema import LinksSchema +from .phase_schema import PhaseSchema +from .feedback_schema import FeedbackSchema from .status_enum import Status # Marshmallow schema for request body @@ -11,8 +11,8 @@ class BidRequestSchema(Schema): tender = fields.Str(required=True, error_messages={"required": {"message": "Missing mandatory field"}}) client = fields.Str(required=True, error_messages={"required": {"message": "Missing mandatory field"}}) alias = fields.Str() - bid_date = fields.Str(required=True, error_messages={"required": {"message": "Missing mandatory field"}}) - bid_folder_url = fields.Str() + bid_date = fields.Date(format='%d-%m-%Y', required=True, error_messages={"required": {"message": "Missing mandatory field"}}) + bid_folder_url = fields.URL() status = fields.Enum(Status, by_value=True) links = fields.Nested(LinksSchema) was_successful = fields.Bool() diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py index 54141e1..9220ac1 100644 --- a/api/schemas/bid_schema.py +++ b/api/schemas/bid_schema.py @@ -1,5 +1,5 @@ from uuid import uuid4 -from datetime import datetime +from datetime import datetime, date from marshmallow import Schema, fields, post_load from .links_schema import LinksModel, LinksSchema from .phase_schema import PhaseModel, PhaseSchema @@ -13,7 +13,7 @@ def __init__(self, tender, client, bid_date, alias=None, bid_folder_url=None, fe self.tender = tender self.client = client self.alias = alias - self.bid_date = datetime.strptime(bid_date, '%d-%m-%Y') # DD-MM-YYYY + self.bid_date = bid_date self.bid_folder_url = bid_folder_url self.status = Status.IN_PROGRESS # enum: "deleted", "in_progress" or "completed" self.links = LinksModel(self.id) diff --git a/api/schemas/phase_schema.py b/api/schemas/phase_schema.py index d555efc..e621914 100644 --- a/api/schemas/phase_schema.py +++ b/api/schemas/phase_schema.py @@ -9,7 +9,7 @@ def __init__(self, phase, has_score, score=None, out_of=None): self.out_of = out_of class PhaseSchema(Schema): - phase = fields.Int(required=True) + phase = fields.Int(required=True, strict=True) has_score = fields.Bool(required=True) score = fields.Int(strict=True) out_of = fields.Int(strict=True) \ No newline at end of file diff --git a/db.txt b/db.txt index 1c2571f..f12693f 100644 --- a/db.txt +++ b/db.txt @@ -1 +1 @@ -{"feedback": null, "was_successful": false, "bid_date": "2023-06-21 00:00:00", "success": [], "last_updated": "2023-06-30T10:46:35.038544", "bid_folder_url": null, "alias": null, "tender": "test_tender", "client": "test_client", "id": "f0c82c9f-4308-459e-a2ce-d7b7eb4abb80", "failed": null, "links": {"questions": "/bids/f0c82c9f-4308-459e-a2ce-d7b7eb4abb80/questions", "self": "/bids/f0c82c9f-4308-459e-a2ce-d7b7eb4abb80"}, "status": "in_progress"}{"last_updated": "2023-06-30T11:06:42.198542", "client": "Office for National Statistics", "was_successful": false, "success": [{"phase": 1, "out_of": 36, "has_score": true, "score": 28}, {"phase": 2, "has_score": false}], "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "links": {"self": "/bids/a64b131b-63c5-4003-9226-5ca33155bab2", "questions": "/bids/a64b131b-63c5-4003-9226-5ca33155bab2/questions"}, "status": "in_progress", "failed": {"phase": 3, "out_of": 36, "has_score": true, "score": 20}, "tender": "Business Intelligence and Data Warehousing", "alias": "ONS", "id": "a64b131b-63c5-4003-9226-5ca33155bab2", "feedback": {"description": "Feedback from client in detail", "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback"}, "bid_date": "2023-06-21 00:00:00"}{"was_successful": false, "id": "265538c5-85cc-4d76-affb-b302b9d5a5dd", "status": "in_progress", "failed": {"score": 20, "phase": 3, "has_score": true, "out_of": 36}, "tender": "Business Intelligence and Data Warehousing", "last_updated": "2023-06-30T11:10:23.464798", "alias": "ONS", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "success": [{"score": 28, "phase": 1, "has_score": true, "out_of": 36}, {"phase": 2, "has_score": false}], "bid_date": "2023-06-21 00:00:00", "links": {"self": "/bids/265538c5-85cc-4d76-affb-b302b9d5a5dd", "questions": "/bids/265538c5-85cc-4d76-affb-b302b9d5a5dd/questions"}, "feedback": {"url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", "description": "Feedback from client in detail"}, "client": "Office for National Statistics"}{"was_successful": false, "id": "b4323b7d-4775-4fd3-aedb-2253baec1986", "status": "in_progress", "failed": {"score": 20, "phase": 3, "has_score": true, "out_of": 36}, "tender": "Business Intelligence and Data Warehousing", "last_updated": "2023-06-30T11:21:15.618412", "alias": "ONS", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "success": [{"score": 28, "phase": 1, "has_score": true, "out_of": 36}, {"phase": 2, "has_score": false}], "bid_date": "2023-06-21 00:00:00", "links": {"self": "/bids/b4323b7d-4775-4fd3-aedb-2253baec1986", "questions": "/bids/b4323b7d-4775-4fd3-aedb-2253baec1986/questions"}, "feedback": {"url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", "description": "Feedback from client in detail"}, "client": "Office for National Statistics"}{}{}{}{}{"status": "in_progress", "success": [{"score": 28, "phase": 1, "out_of": 36, "has_score": true}, {"phase": 2, "has_score": false}], "bid_date": "2023-06-21 00:00:00", "client": "Office for National Statistics", "links": {"questions": "/bids/ed7fbe1d-4c29-433a-bf4c-2bbb7fe664cf/questions", "self": "/bids/ed7fbe1d-4c29-433a-bf4c-2bbb7fe664cf"}, "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "failed": {"score": 20, "phase": 3, "out_of": 36, "has_score": true}, "was_successful": false, "last_updated": "2023-06-30T12:21:52.684258", "tender": "Business Intelligence and Data Warehousing", "alias": "ONS", "feedback": {"description": "Feedback from client in detail", "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback"}, "id": "ed7fbe1d-4c29-433a-bf4c-2bbb7fe664cf"}{}{'bid_folder_url': 'https://organisation.sharepoint.com/Docs/dummyfolder', 'client': 'Office for National Statistics', 'failed': {'phase': 3, 'out_of': 36, 'has_score': True, 'score': 20}, 'bid_date': '2023-06-21 00:00:00', 'id': '77becc93-f933-49a9-92b0-817d7615971a', 'links': {'self': '/bids/77becc93-f933-49a9-92b0-817d7615971a', 'questions': '/bids/77becc93-f933-49a9-92b0-817d7615971a/questions'}, 'was_successful': False, 'status': 'in_progress', 'feedback': {'description': 'Feedback from client in detail', 'url': 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback'}, 'tender': 'Business Intelligence and Data Warehousing', 'success': [{'phase': 1, 'out_of': 36, 'has_score': True, 'score': 28}, {'phase': 2, 'has_score': False}], 'alias': 'ONS', 'last_updated': '2023-06-30T14:05:22.864455'}{'client': 'Office for National Statistics', 'alias': 'ONS', 'failed': {'score': 20, 'out_of': 36, 'has_score': True, 'phase': 3}, 'bid_date': '2023-06-21 00:00:00', 'status': 'in_progress', 'bid_folder_url': 'https://organisation.sharepoint.com/Docs/dummyfolder', 'feedback': {'description': 'Feedback from client in detail', 'url': 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback'}, 'links': {'self': '/bids/76a31020-0663-4172-9f28-fe3b0c5e8f8f', 'questions': '/bids/76a31020-0663-4172-9f28-fe3b0c5e8f8f/questions'}, 'was_successful': False, 'tender': 'Business Intelligence and Data Warehousing', 'last_updated': '2023-06-30T14:06:11.867540', 'id': '76a31020-0663-4172-9f28-fe3b0c5e8f8f', 'success': [{'score': 28, 'out_of': 36, 'has_score': True, 'phase': 1}, {'has_score': False, 'phase': 2}]}{'last_updated': '2023-06-30T14:24:59.590777', 'status': 'in_progress', 'bid_date': '2023-06-21 00:00:00', 'tender': 'Business Intelligence and Data Warehousing', 'client': 'Office for National Statistics', 'id': '4ca2e044-16ef-4eb5-bbd6-c90b93a92fed', 'feedback': {'url': 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback', 'description': 'Feedback from client in detail'}, 'alias': 'ONS', 'failed': {'phase': 3, 'out_of': 36, 'score': 20, 'has_score': True}, 'bid_folder_url': 'https://organisation.sharepoint.com/Docs/dummyfolder', 'links': {'questions': '/bids/4ca2e044-16ef-4eb5-bbd6-c90b93a92fed/questions', 'self': '/bids/4ca2e044-16ef-4eb5-bbd6-c90b93a92fed'}, 'success': [{'phase': 1, 'out_of': 36, 'score': 28, 'has_score': True}, {'phase': 2, 'has_score': False}], 'was_successful': False}{"last_updated": "2023-06-30T14:26:02.449526", "alias": "ONS", "links": {"self": "/bids/07ba99d0-0ac6-4230-a479-d6f9a3291800", "questions": "/bids/07ba99d0-0ac6-4230-a479-d6f9a3291800/questions"}, "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "tender": "Business Intelligence and Data Warehousing", "status": "in_progress", "success": [{"has_score": true, "phase": 1, "out_of": 36, "score": 28}, {"has_score": false, "phase": 2}], "failed": {"has_score": true, "phase": 3, "out_of": 36, "score": 20}, "feedback": {"url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", "description": "Feedback from client in detail"}, "bid_date": "2023-06-21 00:00:00", "was_successful": false, "id": "07ba99d0-0ac6-4230-a479-d6f9a3291800", "client": "Office for National Statistics"}{"last_updated": "2023-06-30T14:26:06.054507", "alias": "ONS", "links": {"self": "/bids/da990886-af06-40db-bb0c-135a8623144e", "questions": "/bids/da990886-af06-40db-bb0c-135a8623144e/questions"}, "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "tender": "Business Intelligence and Data Warehousing", "status": "in_progress", "success": [{"has_score": true, "phase": 1, "out_of": 36, "score": 28}, {"has_score": false, "phase": 2}], "failed": {"has_score": true, "phase": 3, "out_of": 36, "score": 20}, "feedback": {"url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", "description": "Feedback from client in detail"}, "bid_date": "2023-06-21 00:00:00", "was_successful": false, "id": "da990886-af06-40db-bb0c-135a8623144e", "client": "Office for National Statistics"}{"bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "last_updated": "2023-06-30T14:26:47.750826", "links": {"self": "/bids/e567482f-6e18-47b7-8e32-f720934c2879", "questions": "/bids/e567482f-6e18-47b7-8e32-f720934c2879/questions"}, "client": "Office for National Statistics", "was_successful": false, "alias": "ONS", "feedback": {"url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", "description": "Feedback from client in detail"}, "bid_date": "2023-06-21 00:00:00", "id": "e567482f-6e18-47b7-8e32-f720934c2879", "success": [{"score": 28, "out_of": 36, "has_score": true, "phase": 1}, {"has_score": false, "phase": 2}], "failed": {"score": 20, "out_of": 36, "has_score": true, "phase": 3}, "status": "in_progress", "tender": "Business Intelligence and Data Warehousing"}{"bid_date": "2023-06-21 00:00:00", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "id": "33fa9363-c08b-4db9-9ac0-2dc051f8b9bc", "client": "Office for National Statistics", "status": "in_progress", "feedback": {"description": "Feedback from client in detail", "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback"}, "alias": "ONS", "was_successful": false, "last_updated": "2023-06-30T14:31:38.313631", "tender": "Business Intelligence and Data Warehousing", "success": [{"out_of": 36, "has_score": true, "phase": 1, "score": 28}, {"has_score": false, "phase": 2}], "links": {"self": "/bids/33fa9363-c08b-4db9-9ac0-2dc051f8b9bc", "questions": "/bids/33fa9363-c08b-4db9-9ac0-2dc051f8b9bc/questions"}, "failed": {"out_of": 36, "has_score": true, "phase": 3, "score": 20}} \ No newline at end of file +{"status": "in_progress", "alias": "ONS", "bid_date": "2023-06-21", "client": "Office for National Statistics", "tender": "Business Intelligence and Data Warehousing", "last_updated": "2023-06-30T14:43:27.022464", "feedback": {"description": "Feedback from client in detail", "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback"}, "id": "6cb57630-5fd6-404a-a29f-a1aaf821fd61", "links": {"self": "/bids/6cb57630-5fd6-404a-a29f-a1aaf821fd61", "questions": "/bids/6cb57630-5fd6-404a-a29f-a1aaf821fd61/questions"}, "was_successful": false, "success": [{"phase": 1, "has_score": true, "score": 28, "out_of": 36}, {"phase": 2, "has_score": false}], "failed": {"phase": 3, "has_score": true, "score": 20, "out_of": 36}, "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder"}{"id": "2bf95109-ffe5-4b69-abbd-6f356f1f7103", "links": {"self": "/bids/2bf95109-ffe5-4b69-abbd-6f356f1f7103", "questions": "/bids/2bf95109-ffe5-4b69-abbd-6f356f1f7103/questions"}, "bid_date": "2023-06-21", "tender": "Business Intelligence and Data Warehousing", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "was_successful": false, "failed": {"score": 20, "out_of": 36, "has_score": true, "phase": 3}, "client": "Office for National Statistics", "feedback": {"url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", "description": "Feedback from client in detail"}, "status": "in_progress", "last_updated": "2023-06-30T14:57:52.772927", "alias": "ONS", "success": [{"score": 28, "out_of": 36, "has_score": true, "phase": 1}, {"has_score": false, "phase": 2}]}{"client": "Office for National Statistics", "status": "in_progress", "id": "274083b5-d660-467f-a58a-243941eac574", "alias": "ONS", "links": {"self": "/bids/274083b5-d660-467f-a58a-243941eac574", "questions": "/bids/274083b5-d660-467f-a58a-243941eac574/questions"}, "failed": {"has_score": true, "out_of": 36, "phase": 3, "score": 20}, "bid_date": "2023-06-21", "success": [{"has_score": true, "out_of": 36, "phase": 1, "score": 28}, {"has_score": false, "phase": 2}], "feedback": {"url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", "description": "Feedback from client in detail"}, "bid_folder_url": "test", "tender": "Business Intelligence and Data Warehousing", "last_updated": "2023-06-30T15:00:40.137631", "was_successful": false}{"alias": "ONS", "id": "ba083965-3737-4fad-8a8d-6a40cbcab5bb", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "client": "Office for National Statistics", "bid_date": "2023-06-21", "last_updated": "2023-06-30T15:13:13.123372", "was_successful": false, "feedback": {"url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", "description": "Feedback from client in detail"}, "links": {"self": "/bids/ba083965-3737-4fad-8a8d-6a40cbcab5bb", "questions": "/bids/ba083965-3737-4fad-8a8d-6a40cbcab5bb/questions"}, "tender": "Business Intelligence and Data Warehousing", "failed": {"score": 20, "has_score": true, "phase": 3, "out_of": 36}, "status": "in_progress", "success": [{"score": 28, "has_score": true, "phase": 1, "out_of": 36}, {"has_score": false, "phase": 2}]}{"alias": "ONS", "id": "00072bff-cb8e-45ef-ab09-c7860b719bf2", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "client": "Office for National Statistics", "bid_date": "2023-06-21", "last_updated": "2023-06-30T15:15:34.931648", "was_successful": false, "feedback": {"url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", "description": "Feedback from client in detail"}, "links": {"self": "/bids/00072bff-cb8e-45ef-ab09-c7860b719bf2", "questions": "/bids/00072bff-cb8e-45ef-ab09-c7860b719bf2/questions"}, "tender": "Business Intelligence and Data Warehousing", "failed": {"score": 20, "has_score": true, "phase": 3, "out_of": 36}, "status": "in_progress", "success": [{"score": 28, "has_score": true, "phase": 1, "out_of": 36}, {"has_score": false, "phase": 2}]} \ No newline at end of file diff --git a/request_examples/invalid_int.http b/request_examples/invalid_int.http new file mode 100644 index 0000000..61683fe --- /dev/null +++ b/request_examples/invalid_int.http @@ -0,0 +1,32 @@ +POST http://localhost:3000/api/bids HTTP/1.1 +Content-Type: application/json + +{ + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "success": [ + { + "phase": "ONE", + "has_score": true, + "score": 28, + "out_of": 36 + }, + { + "phase": 2, + "has_score": false + } + ], + "failed": { + "phase": 3, + "has_score": true, + "score": 20, + "out_of": 36 + } +} \ No newline at end of file diff --git a/request_examples/invalid_string.http b/request_examples/invalid_string.http new file mode 100644 index 0000000..cdee4c8 --- /dev/null +++ b/request_examples/invalid_string.http @@ -0,0 +1,32 @@ +POST http://localhost:3000/api/bids HTTP/1.1 +Content-Type: application/json + +{ + "tender": 42, + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "success": [ + { + "phase": 1, + "has_score": true, + "score": 28, + "out_of": 36 + }, + { + "phase": 2, + "has_score": false + } + ], + "failed": { + "phase": 3, + "has_score": true, + "score": 20, + "out_of": 36 + } +} \ No newline at end of file diff --git a/request_examples/invalid_url.http b/request_examples/invalid_url.http new file mode 100644 index 0000000..fee23b9 --- /dev/null +++ b/request_examples/invalid_url.http @@ -0,0 +1,32 @@ +POST http://localhost:3000/api/bids HTTP/1.1 +Content-Type: application/json + +{ + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "not a URL", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "success": [ + { + "phase": 1, + "has_score": true, + "score": 28, + "out_of": 36 + }, + { + "phase": 2, + "has_score": false + } + ], + "failed": { + "phase": 3, + "has_score": true, + "score": 20, + "out_of": 36 + } +} \ No newline at end of file diff --git a/static/swagger_config.yml b/static/swagger_config.yml index c2354da..1049749 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -180,14 +180,14 @@ components: description: Feedback from client (if provided) type: object required: - - feedback_url - - feedback_description + - url + - description properties: - feedback_url: + url: description: Link to feedback type: string example: 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback' - feedback_description: + description: description: Summary of feedback type: string example: 'Feedback from client in detail' @@ -253,8 +253,8 @@ components: tender: 'Business Intelligence and Data Warehousing' bid_folder_url: 'https://organisation.sharepoint.com/Docs/dummyfolder' feedback: - feedback_url: 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback' - feedback_description: 'Feedback from client in detail' + url: 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback' + description: 'Feedback from client in detail' client: 'Office for National Statistics' alias: 'ONS' bid_date: '21-06-2023' @@ -263,8 +263,8 @@ components: value: bid_folder_url: 'https://organisation.sharepoint.com/Docs/dummyfolder' feedback: - feedback_url: 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback' - feedback_description: 'Feedback from client in detail' + url: 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback' + description: 'Feedback from client in detail' client: 'Office for National Statistics' alias: 'ONS' bid_date: '21-06-2023' From a430451336e13b3968cb210dca575a5c86ad3f0b Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 30 Jun 2023 15:48:12 +0100 Subject: [PATCH 24/97] refactor: removed unused imports --- api/schemas/bid_schema.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py index 9220ac1..c5574eb 100644 --- a/api/schemas/bid_schema.py +++ b/api/schemas/bid_schema.py @@ -1,9 +1,9 @@ from uuid import uuid4 -from datetime import datetime, date -from marshmallow import Schema, fields, post_load +from datetime import datetime +from marshmallow import Schema, fields from .links_schema import LinksModel, LinksSchema -from .phase_schema import PhaseModel, PhaseSchema -from .feedback_schema import FeedbackModel, FeedbackSchema +from .phase_schema import PhaseSchema +from .feedback_schema import FeedbackSchema from .status_enum import Status # Description: Schema for the bid object From 22e30029357e9ac34abec4da7cd29f4e2266b95a Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 3 Jul 2023 13:25:41 +0100 Subject: [PATCH 25/97] refactor: moved class declarations to models folder; updated swagger spec --- README.md | 2 +- TODO.md | 1 + api/controllers/bid_controller.py | 17 +++----- api/models/bid_model.py | 38 +++++++++++++++++ api/models/bid_models.py | 13 ------ api/models/feedback_model.py | 5 +++ api/models/links_model.py | 5 +++ api/models/phase_model.py | 7 ++++ api/{schemas => models}/status_enum.py | 0 .../{bid_request.py => bid_request_schema.py} | 4 +- api/schemas/bid_schema.py | 40 +----------------- api/schemas/feedback_schema.py | 6 --- api/schemas/links_schema.py | 6 --- api/schemas/phase_schema.py | 8 ---- app.py | 2 +- db.txt | 1 - request_examples/all_fields.http | 2 +- request_examples/invalid_int.http | 2 +- request_examples/invalid_string.http | 2 +- request_examples/invalid_url.http | 2 +- request_examples/missing_mandatory_field.http | 2 +- static/swagger_config.yml | 41 +++++++++++++++---- 22 files changed, 105 insertions(+), 101 deletions(-) create mode 100644 TODO.md create mode 100644 api/models/bid_model.py delete mode 100644 api/models/bid_models.py create mode 100644 api/models/feedback_model.py create mode 100644 api/models/links_model.py create mode 100644 api/models/phase_model.py rename api/{schemas => models}/status_enum.py (100%) rename api/schemas/{bid_request.py => bid_request_schema.py} (93%) diff --git a/README.md b/README.md index 1402680..4afac8a 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ This API provides a simple "Hello World" message. ```bash python app/app.py ``` -8. The API will be available at http://localhost:3000/api/bids +8. The API will be available at http://localhost:8080/api/bids -------------- diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..c146f07 --- /dev/null +++ b/TODO.md @@ -0,0 +1 @@ +- SWAGGER: how to do multiple examples of 400 response \ No newline at end of file diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index d2a2905..fd42605 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -1,25 +1,20 @@ -from flask import Blueprint, jsonify +from flask import Blueprint, jsonify, request from marshmallow import ValidationError -from api.models.bid_models import get_bids, create_bid from api.schemas.bid_schema import BidSchema +from api.schemas.bid_request_schema import BidRequestSchema from helpers.helpers import save_in_memory bid = Blueprint('bid', __name__) -@bid.route("/bids", methods=["GET"]) -def get_all_bids(): - response = get_bids() - return response - @bid.route("/bids", methods=["POST"]) def post_bid(): # Create bid document and return error if input validation fails try: - bid_document = create_bid() + bid_document = BidRequestSchema().load(request.json) except ValidationError as e: - return jsonify({"error": str(e)}), 400 + return jsonify({"Error": str(e)}), 400 # Serialize to a JSON-encoded string - data = BidSchema().dumps(bid_document) + data = BidSchema().dump(bid_document) save_in_memory('./db.txt', data) - return data, 200 \ No newline at end of file + return data, 201 \ No newline at end of file diff --git a/api/models/bid_model.py b/api/models/bid_model.py new file mode 100644 index 0000000..7e439e9 --- /dev/null +++ b/api/models/bid_model.py @@ -0,0 +1,38 @@ +from uuid import uuid4 +from datetime import datetime +from .links_model import LinksModel +from api.models.status_enum import Status + +# Description: Schema for the bid object +class BidModel(): + def __init__(self, tender, client, bid_date, alias=None, bid_folder_url=None, feedback=None, failed=None, was_successful=False, success=[]): + self.id = uuid4() + self.tender = tender + self.client = client + self.alias = alias + self.bid_date = bid_date + self.bid_folder_url = bid_folder_url + self.status = Status.IN_PROGRESS # enum: "deleted", "in_progress" or "completed" + self.links = LinksModel(self.id) + self.was_successful = was_successful + self.success = success + self.failed = failed + self.feedback = feedback + self.last_updated = datetime.now() + + + def addSuccessPhase(self, phase): + self.success.append(phase) + + def setFailedPhase(self, phase): + self.was_successful = False + self.failed = phase + + def addFeedback(self, feedback): + self.feedback = feedback + + def setStatus(self, status): + if isinstance(status, Status): + self.status = status.value + else: + raise ValueError("Invalid status. Please provide a valid Status enum value") \ No newline at end of file diff --git a/api/models/bid_models.py b/api/models/bid_models.py deleted file mode 100644 index 528e2e7..0000000 --- a/api/models/bid_models.py +++ /dev/null @@ -1,13 +0,0 @@ -from flask import request -from api.schemas.bid_request import BidRequestSchema - -def get_bids(): - f = open('./db.txt','r') - bids = f.read() - f.close() - return bids, 200 - -def create_bid(): - # De-serialize request body and validate against Marshmallow schema - bid = BidRequestSchema().load(request.json) - return bid \ No newline at end of file diff --git a/api/models/feedback_model.py b/api/models/feedback_model.py new file mode 100644 index 0000000..17ab1ed --- /dev/null +++ b/api/models/feedback_model.py @@ -0,0 +1,5 @@ +# Schema for Feedback object +class FeedbackModel: + def __init__(self,description, url): + self.description = description + self.url = url \ No newline at end of file diff --git a/api/models/links_model.py b/api/models/links_model.py new file mode 100644 index 0000000..83f3827 --- /dev/null +++ b/api/models/links_model.py @@ -0,0 +1,5 @@ +# Schema for links object +class LinksModel(): + def __init__(self, id): + self.self = f"/bids/{id}" + self.questions = f"/bids/{id}/questions" diff --git a/api/models/phase_model.py b/api/models/phase_model.py new file mode 100644 index 0000000..e32662f --- /dev/null +++ b/api/models/phase_model.py @@ -0,0 +1,7 @@ +# Schema for phaseInfo object +class PhaseModel: + def __init__(self, phase, has_score, score=None, out_of=None): + self.phase = phase + self.has_score = has_score + self.score = score + self.out_of = out_of diff --git a/api/schemas/status_enum.py b/api/models/status_enum.py similarity index 100% rename from api/schemas/status_enum.py rename to api/models/status_enum.py diff --git a/api/schemas/bid_request.py b/api/schemas/bid_request_schema.py similarity index 93% rename from api/schemas/bid_request.py rename to api/schemas/bid_request_schema.py index 247bb23..ae2bcf8 100644 --- a/api/schemas/bid_request.py +++ b/api/schemas/bid_request_schema.py @@ -1,9 +1,9 @@ from marshmallow import Schema, fields, post_load -from .bid_schema import BidModel +from api.models.bid_model import BidModel from .links_schema import LinksSchema from .phase_schema import PhaseSchema from .feedback_schema import FeedbackSchema -from .status_enum import Status +from ..models.status_enum import Status # Marshmallow schema for request body class BidRequestSchema(Schema): diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py index c5574eb..24ccb0c 100644 --- a/api/schemas/bid_schema.py +++ b/api/schemas/bid_schema.py @@ -1,44 +1,8 @@ -from uuid import uuid4 -from datetime import datetime from marshmallow import Schema, fields -from .links_schema import LinksModel, LinksSchema +from .links_schema import LinksSchema from .phase_schema import PhaseSchema from .feedback_schema import FeedbackSchema -from .status_enum import Status - -# Description: Schema for the bid object -class BidModel(): - def __init__(self, tender, client, bid_date, alias=None, bid_folder_url=None, feedback=None, failed=None, was_successful=False, success=[]): - self.id = uuid4() - self.tender = tender - self.client = client - self.alias = alias - self.bid_date = bid_date - self.bid_folder_url = bid_folder_url - self.status = Status.IN_PROGRESS # enum: "deleted", "in_progress" or "completed" - self.links = LinksModel(self.id) - self.was_successful = was_successful - self.success = success - self.failed = failed - self.feedback = feedback - self.last_updated = datetime.now() - - - def addSuccessPhase(self, phase): - self.success.append(phase) - - def setFailedPhase(self, phase): - self.was_successful = False - self.failed = phase - - def addFeedback(self, feedback): - self.feedback = feedback - - def setStatus(self, status): - if isinstance(status, Status): - self.status = status.value - else: - raise ValueError("Invalid status. Please provide a valid Status enum value") +from ..models.status_enum import Status # Marshmallow schema class BidSchema(Schema): diff --git a/api/schemas/feedback_schema.py b/api/schemas/feedback_schema.py index 1e7acdb..1f55f6e 100644 --- a/api/schemas/feedback_schema.py +++ b/api/schemas/feedback_schema.py @@ -1,11 +1,5 @@ from marshmallow import Schema, fields -# Schema for Feedback object -class FeedbackModel: - def __init__(self,description, url): - self.description = description - self.url = url - class FeedbackSchema(Schema): description = fields.Str(required=True) url = fields.Str(required=True) \ No newline at end of file diff --git a/api/schemas/links_schema.py b/api/schemas/links_schema.py index d0c223d..4a711e2 100644 --- a/api/schemas/links_schema.py +++ b/api/schemas/links_schema.py @@ -1,11 +1,5 @@ from marshmallow import Schema, fields -# Schema for links object -class LinksModel(): - def __init__(self, id): - self.self = f"/bids/{id}" - self.questions = f"/bids/{id}/questions" - class LinksSchema(Schema): self = fields.Str(required=True) questions = fields.Str(required=True) \ No newline at end of file diff --git a/api/schemas/phase_schema.py b/api/schemas/phase_schema.py index e621914..ce65bce 100644 --- a/api/schemas/phase_schema.py +++ b/api/schemas/phase_schema.py @@ -1,13 +1,5 @@ from marshmallow import Schema, fields -# Schema for phaseInfo object -class PhaseModel: - def __init__(self, phase, has_score, score=None, out_of=None): - self.phase = phase - self.has_score = has_score - self.score = score - self.out_of = out_of - class PhaseSchema(Schema): phase = fields.Int(required=True, strict=True) has_score = fields.Bool(required=True) diff --git a/app.py b/app.py index fadd3da..3ce7ba0 100644 --- a/app.py +++ b/app.py @@ -21,4 +21,4 @@ if __name__ == '__main__': - app.run(debug=True, port=3000) \ No newline at end of file + app.run(debug=True, port=8080) \ No newline at end of file diff --git a/db.txt b/db.txt index f12693f..e69de29 100644 --- a/db.txt +++ b/db.txt @@ -1 +0,0 @@ -{"status": "in_progress", "alias": "ONS", "bid_date": "2023-06-21", "client": "Office for National Statistics", "tender": "Business Intelligence and Data Warehousing", "last_updated": "2023-06-30T14:43:27.022464", "feedback": {"description": "Feedback from client in detail", "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback"}, "id": "6cb57630-5fd6-404a-a29f-a1aaf821fd61", "links": {"self": "/bids/6cb57630-5fd6-404a-a29f-a1aaf821fd61", "questions": "/bids/6cb57630-5fd6-404a-a29f-a1aaf821fd61/questions"}, "was_successful": false, "success": [{"phase": 1, "has_score": true, "score": 28, "out_of": 36}, {"phase": 2, "has_score": false}], "failed": {"phase": 3, "has_score": true, "score": 20, "out_of": 36}, "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder"}{"id": "2bf95109-ffe5-4b69-abbd-6f356f1f7103", "links": {"self": "/bids/2bf95109-ffe5-4b69-abbd-6f356f1f7103", "questions": "/bids/2bf95109-ffe5-4b69-abbd-6f356f1f7103/questions"}, "bid_date": "2023-06-21", "tender": "Business Intelligence and Data Warehousing", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "was_successful": false, "failed": {"score": 20, "out_of": 36, "has_score": true, "phase": 3}, "client": "Office for National Statistics", "feedback": {"url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", "description": "Feedback from client in detail"}, "status": "in_progress", "last_updated": "2023-06-30T14:57:52.772927", "alias": "ONS", "success": [{"score": 28, "out_of": 36, "has_score": true, "phase": 1}, {"has_score": false, "phase": 2}]}{"client": "Office for National Statistics", "status": "in_progress", "id": "274083b5-d660-467f-a58a-243941eac574", "alias": "ONS", "links": {"self": "/bids/274083b5-d660-467f-a58a-243941eac574", "questions": "/bids/274083b5-d660-467f-a58a-243941eac574/questions"}, "failed": {"has_score": true, "out_of": 36, "phase": 3, "score": 20}, "bid_date": "2023-06-21", "success": [{"has_score": true, "out_of": 36, "phase": 1, "score": 28}, {"has_score": false, "phase": 2}], "feedback": {"url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", "description": "Feedback from client in detail"}, "bid_folder_url": "test", "tender": "Business Intelligence and Data Warehousing", "last_updated": "2023-06-30T15:00:40.137631", "was_successful": false}{"alias": "ONS", "id": "ba083965-3737-4fad-8a8d-6a40cbcab5bb", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "client": "Office for National Statistics", "bid_date": "2023-06-21", "last_updated": "2023-06-30T15:13:13.123372", "was_successful": false, "feedback": {"url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", "description": "Feedback from client in detail"}, "links": {"self": "/bids/ba083965-3737-4fad-8a8d-6a40cbcab5bb", "questions": "/bids/ba083965-3737-4fad-8a8d-6a40cbcab5bb/questions"}, "tender": "Business Intelligence and Data Warehousing", "failed": {"score": 20, "has_score": true, "phase": 3, "out_of": 36}, "status": "in_progress", "success": [{"score": 28, "has_score": true, "phase": 1, "out_of": 36}, {"has_score": false, "phase": 2}]}{"alias": "ONS", "id": "00072bff-cb8e-45ef-ab09-c7860b719bf2", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "client": "Office for National Statistics", "bid_date": "2023-06-21", "last_updated": "2023-06-30T15:15:34.931648", "was_successful": false, "feedback": {"url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", "description": "Feedback from client in detail"}, "links": {"self": "/bids/00072bff-cb8e-45ef-ab09-c7860b719bf2", "questions": "/bids/00072bff-cb8e-45ef-ab09-c7860b719bf2/questions"}, "tender": "Business Intelligence and Data Warehousing", "failed": {"score": 20, "has_score": true, "phase": 3, "out_of": 36}, "status": "in_progress", "success": [{"score": 28, "has_score": true, "phase": 1, "out_of": 36}, {"has_score": false, "phase": 2}]} \ No newline at end of file diff --git a/request_examples/all_fields.http b/request_examples/all_fields.http index 9404e9a..5b80433 100644 --- a/request_examples/all_fields.http +++ b/request_examples/all_fields.http @@ -1,4 +1,4 @@ -POST http://localhost:3000/api/bids HTTP/1.1 +POST http://localhost:8080/api/bids HTTP/1.1 Content-Type: application/json { diff --git a/request_examples/invalid_int.http b/request_examples/invalid_int.http index 61683fe..d2a6c42 100644 --- a/request_examples/invalid_int.http +++ b/request_examples/invalid_int.http @@ -1,4 +1,4 @@ -POST http://localhost:3000/api/bids HTTP/1.1 +POST http://localhost:8080/api/bids HTTP/1.1 Content-Type: application/json { diff --git a/request_examples/invalid_string.http b/request_examples/invalid_string.http index cdee4c8..e8c2756 100644 --- a/request_examples/invalid_string.http +++ b/request_examples/invalid_string.http @@ -1,4 +1,4 @@ -POST http://localhost:3000/api/bids HTTP/1.1 +POST http://localhost:8080/api/bids HTTP/1.1 Content-Type: application/json { diff --git a/request_examples/invalid_url.http b/request_examples/invalid_url.http index fee23b9..aeae3b0 100644 --- a/request_examples/invalid_url.http +++ b/request_examples/invalid_url.http @@ -1,4 +1,4 @@ -POST http://localhost:3000/api/bids HTTP/1.1 +POST http://localhost:8080/api/bids HTTP/1.1 Content-Type: application/json { diff --git a/request_examples/missing_mandatory_field.http b/request_examples/missing_mandatory_field.http index a06e817..3425e3f 100644 --- a/request_examples/missing_mandatory_field.http +++ b/request_examples/missing_mandatory_field.http @@ -1,4 +1,4 @@ -POST http://localhost:3000/api/bids HTTP/1.1 +POST http://localhost:8080/api/bids HTTP/1.1 Content-Type: application/json { diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 1049749..39fdd90 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -22,7 +22,7 @@ info: # -------------------------------------------- # Server servers: - - url: 'http://localhost:3000/api/' + - url: 'http://localhost:8080/api/' description: Local server # -------------------------------------------- # Tags @@ -195,12 +195,12 @@ components: QuestionsLink: description: A link to a collection of questions for a bid type: string - example: 'https://localhost:3000/api//bids/96d69775-29af-46b1-aaf4-bfbdb1543412/questions' + example: 'https://localhost:8080/api//bids/96d69775-29af-46b1-aaf4-bfbdb1543412/questions' # -------------------------------------------- SelfLink: description: A link to the current resource type: string - example: 'https://localhost:3000/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428' + example: 'https://localhost:8080/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428' # -------------------------------------------- BidRequestBody: type: object @@ -258,6 +258,25 @@ components: client: 'Office for National Statistics' alias: 'ONS' bid_date: '21-06-2023' + success: [ + { + "phase": 1, + "has_score": true, + "score": 28, + "out_of": 36 + }, + { + "phase": 2, + "has_score": false + } + ] + failed: { + "phase": 3, + "has_score": true, + "score": 20, + "out_of": 36 + } + was_successful: false 400 Bad Request: summary: 400 Bad Request value: @@ -274,15 +293,19 @@ components: BadRequest: description: Bad Request Error content: - text/plain: + application/json: schema: - type: string - example: Missing mandatory field + type: object + example: { + "Error": "{'': ['']}" + } InternalServerError: description: Internal Server Error content: - text/plain: + application/json: schema: - type: string - example: Could not connect to a database + type: object + example: { + "Error": "{'': ['']}" + } # -------------------------------------------- \ No newline at end of file From 29ac93017e12b804f72794663420306f6cce8bfe Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 3 Jul 2023 14:24:30 +0100 Subject: [PATCH 26/97] refactor: updated readme with swagger instructions --- README.md | 13 +++++++------ api/controllers/bid_controller.py | 4 ++++ requirements.txt | 1 + 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4afac8a..ba681c8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # tdse-accessForce-bids-api # API Documentation -This API provides a simple "Hello World" message. +This API provides an endpoint to post a new bid document. ## Prerequisites @@ -47,16 +47,17 @@ This API provides a simple "Hello World" message. ``` 8. The API will be available at http://localhost:8080/api/bids - -------------- -**Note:** If this is part of your training, you should look for guidance by your mentor of how to progress this project. Your coach can use the [generic project rest API doc](/training/generic-projects/rest-api/README.md) to setup your initial project that will cover iteration 1. +## Accessing API Documentation (Swagger Specification) --------------- +1. Run the following command to start the API: -Return to the [internal projects](https://github.com/methods/tdse-projects/blob/main/internal/README.md) for additional options and information. + ```bash + python app/app.py + ``` +2. The Swagger Specification will be available at http://localhost:8080/api/docs --------------- ### Contributing to this project diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index fd42605..48cc382 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -6,6 +6,10 @@ bid = Blueprint('bid', __name__) +@bid.route("/bids", methods=["GET"]) +def get_bids(): + return "Under construction", 200 + @bid.route("/bids", methods=["POST"]) def post_bid(): # Create bid document and return error if input validation fails diff --git a/requirements.txt b/requirements.txt index 775a778..7bd1654 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ Werkzeug==2.3.6 pip==23.1.2 pytest==7.3.2 flask_swagger_ui==4.11.1 +marshmallow From 042e4f2637d10d6782f855547d6257e944d82c1f Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 3 Jul 2023 17:02:32 +0100 Subject: [PATCH 27/97] feat: connect to mongodb instance; refactored post Co-authored-by: Julio Velezmoro --- README.md | 54 +++++++++++++++++++++++++++++++ api/controllers/bid_controller.py | 21 ++++++++---- api/models/bid_model.py | 4 +-- api/schemas/bid_request_schema.py | 2 +- api/schemas/bid_schema.py | 2 +- dbconfig/__init__.py | 0 dbconfig/mongo_setup.py | 10 ++++++ helpers/helpers.py | 9 +++--- requirements.txt | 2 ++ 9 files changed, 89 insertions(+), 15 deletions(-) create mode 100644 dbconfig/__init__.py create mode 100644 dbconfig/mongo_setup.py diff --git a/README.md b/README.md index ba681c8..a04c5d4 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ This API provides an endpoint to post a new bid document. - Python 3.x - Flask +- Homebrew ## Running the API @@ -59,6 +60,59 @@ This API provides an endpoint to post a new bid document. 2. The Swagger Specification will be available at http://localhost:8080/api/docs +-------------- + +## Installing and running an instance of MongoDB on your local machine (MacOS) + +### To install on Windows please see [here](https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-windows/) + +1. Install Homebrew if not already installed. You can check if it is installed by running the following command: + + ```bash + brew --version + ``` +2. Install MongoDB by running the following commands: + + ```bash + brew tap mongodb/brew + brew install mongodb-community + ``` +3. Create the data directory by running the following command: + + ```bash + mkdir -p /data/db + ``` +4. To run MongoDB (i.e. the mongod process) as a macOS service, run: + + ```bash + brew services start mongodb-community@6.0 + ``` +5. Run the following command to stop the MongoDB instance, as needed: + + ```bash + brew services stop mongodb-community@6.0 + ``` +6. To verify that MongoDB is running, run: + + ```bash + brew services list + ``` +You should see the service `mongodb-community` listed as `started`. + +7. To begin using MongoDB, connect mongosh to the running instance. From a new terminal, issue the following: + + ```bash + mongosh + ``` +8. Run the following command to exit the MongoDB shell: + + ```bash + exit + ``` +OPTIONAL - Download MongoDB Compass to view the database in a GUI. You can download it from [here](https://www.mongodb.com/try/download/compass) + +-------------- + ### Contributing to this project See [CONTRIBUTING](https://github.com/methods/tdse-accessForce-bids-api/blob/main/CONTRIBUTING.md) for details diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 48cc382..4977f6f 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -2,7 +2,10 @@ from marshmallow import ValidationError from api.schemas.bid_schema import BidSchema from api.schemas.bid_request_schema import BidRequestSchema -from helpers.helpers import save_in_memory +from dbconfig.mongo_setup import dbConnection +from pymongo.errors import ConnectionFailure +from helpers.helpers import showConnectionError + bid = Blueprint('bid', __name__) @@ -14,11 +17,17 @@ def get_bids(): def post_bid(): # Create bid document and return error if input validation fails try: + db = dbConnection() bid_document = BidRequestSchema().load(request.json) + # Serialize to a JSON-encoded string + data = BidSchema().dump(bid_document) + # Insert document into database collection + bids = db['bids'] + bids.insert_one(data) + return data, 201 except ValidationError as e: return jsonify({"Error": str(e)}), 400 - - # Serialize to a JSON-encoded string - data = BidSchema().dump(bid_document) - save_in_memory('./db.txt', data) - return data, 201 \ No newline at end of file + except ConnectionFailure: + return showConnectionError() + + diff --git a/api/models/bid_model.py b/api/models/bid_model.py index 7e439e9..7d6c339 100644 --- a/api/models/bid_model.py +++ b/api/models/bid_model.py @@ -6,14 +6,14 @@ # Description: Schema for the bid object class BidModel(): def __init__(self, tender, client, bid_date, alias=None, bid_folder_url=None, feedback=None, failed=None, was_successful=False, success=[]): - self.id = uuid4() + self._id = uuid4() self.tender = tender self.client = client self.alias = alias self.bid_date = bid_date self.bid_folder_url = bid_folder_url self.status = Status.IN_PROGRESS # enum: "deleted", "in_progress" or "completed" - self.links = LinksModel(self.id) + self.links = LinksModel(self._id) self.was_successful = was_successful self.success = success self.failed = failed diff --git a/api/schemas/bid_request_schema.py b/api/schemas/bid_request_schema.py index ae2bcf8..863ccbb 100644 --- a/api/schemas/bid_request_schema.py +++ b/api/schemas/bid_request_schema.py @@ -7,7 +7,7 @@ # Marshmallow schema for request body class BidRequestSchema(Schema): - id = fields.UUID() + _id = fields.UUID() tender = fields.Str(required=True, error_messages={"required": {"message": "Missing mandatory field"}}) client = fields.Str(required=True, error_messages={"required": {"message": "Missing mandatory field"}}) alias = fields.Str() diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py index 24ccb0c..ae393fb 100644 --- a/api/schemas/bid_schema.py +++ b/api/schemas/bid_schema.py @@ -6,7 +6,7 @@ # Marshmallow schema class BidSchema(Schema): - id = fields.UUID(required=True) + _id = fields.UUID(required=True) tender = fields.Str(required=True) client = fields.Str(required=True) alias = fields.Str() diff --git a/dbconfig/__init__.py b/dbconfig/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dbconfig/mongo_setup.py b/dbconfig/mongo_setup.py new file mode 100644 index 0000000..fee1f08 --- /dev/null +++ b/dbconfig/mongo_setup.py @@ -0,0 +1,10 @@ +from pymongo import MongoClient + +MONGO_URI = "mongodb://localhost:27017/bids" + +# Create a new client and connect to the server +def dbConnection(): + client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=10000) + db = client["bids"] + return db + \ No newline at end of file diff --git a/helpers/helpers.py b/helpers/helpers.py index 04ecab2..12bde5e 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -1,5 +1,4 @@ -# Save data in memory -def save_in_memory(file, document): - f=open(file,'a') - f.write(str(document)) - f.close \ No newline at end of file +from flask import jsonify + +def showConnectionError(): + return jsonify({"Error": "Could not connect to database"}), 500 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7bd1654..cb694ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,5 @@ pip==23.1.2 pytest==7.3.2 flask_swagger_ui==4.11.1 marshmallow +pymongo +certifi \ No newline at end of file From b0990876d9db19e6ac390ab7b6a0053bfb69cd25 Mon Sep 17 00:00:00 2001 From: Pira Tejasakulsin <47509562+piratejas@users.noreply.github.com> Date: Mon, 3 Jul 2023 17:08:46 +0100 Subject: [PATCH 28/97] Update README.md --- README.md | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a04c5d4..8486860 100644 --- a/README.md +++ b/README.md @@ -77,34 +77,29 @@ This API provides an endpoint to post a new bid document. brew tap mongodb/brew brew install mongodb-community ``` -3. Create the data directory by running the following command: - - ```bash - mkdir -p /data/db - ``` -4. To run MongoDB (i.e. the mongod process) as a macOS service, run: +3. To run MongoDB (i.e. the mongod process) as a macOS service, run: ```bash brew services start mongodb-community@6.0 ``` -5. Run the following command to stop the MongoDB instance, as needed: +4. Run the following command to stop the MongoDB instance, as needed: ```bash brew services stop mongodb-community@6.0 ``` -6. To verify that MongoDB is running, run: +5. To verify that MongoDB is running, run: ```bash brew services list ``` -You should see the service `mongodb-community` listed as `started`. + You should see the service `mongodb-community` listed as `started`. -7. To begin using MongoDB, connect mongosh to the running instance. From a new terminal, issue the following: +6. To begin using MongoDB, connect mongosh to the running instance. From a new terminal, issue the following: ```bash mongosh ``` -8. Run the following command to exit the MongoDB shell: +7. Run the following command to exit the MongoDB shell: ```bash exit From 3de2be9193e35a23cb32c1e03ed70c7209e0097c Mon Sep 17 00:00:00 2001 From: Pira Tejasakulsin <47509562+piratejas@users.noreply.github.com> Date: Mon, 3 Jul 2023 17:09:45 +0100 Subject: [PATCH 29/97] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8486860..3d39cde 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ This API provides an endpoint to post a new bid document. ```bash mongosh ``` -7. Run the following command to exit the MongoDB shell: +7. To exit the MongoDB shell, run the following command: ```bash exit From efc5e002cfe2d4af408eceec5732baa08fdee595 Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 3 Jul 2023 17:52:03 +0100 Subject: [PATCH 30/97] refactor: added information to readme; changed db name to bidsAPI --- README.md | 20 ++++++++++++-------- dbconfig/mongo_setup.py | 4 ++-- static/swagger_config.yml | 2 +- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 3d39cde..13af8ab 100644 --- a/README.md +++ b/README.md @@ -82,24 +82,28 @@ This API provides an endpoint to post a new bid document. ```bash brew services start mongodb-community@6.0 ``` -4. Run the following command to stop the MongoDB instance, as needed: - - ```bash - brew services stop mongodb-community@6.0 - ``` -5. To verify that MongoDB is running, run: +4. To verify that MongoDB is running, run: ```bash brew services list ``` You should see the service `mongodb-community` listed as `started`. +5. Run the following command to stop the MongoDB instance, as needed: -6. To begin using MongoDB, connect mongosh to the running instance. From a new terminal, issue the following: + ```bash + brew services stop mongodb-community@6.0 + ``` +6. To begin using MongoDB, connect the MongoDB shell (mongosh) to the running instance. From a new terminal, issue the following: ```bash mongosh ``` -7. To exit the MongoDB shell, run the following command: +7. To create a new database called `bidsAPI`, run: + + ```bash + use bidsAPI + ``` +8. To exit the MongoDB shell, run the following command: ```bash exit diff --git a/dbconfig/mongo_setup.py b/dbconfig/mongo_setup.py index fee1f08..97a116d 100644 --- a/dbconfig/mongo_setup.py +++ b/dbconfig/mongo_setup.py @@ -1,10 +1,10 @@ from pymongo import MongoClient -MONGO_URI = "mongodb://localhost:27017/bids" +MONGO_URI = "mongodb://localhost:27017/bidsAPI" # Create a new client and connect to the server def dbConnection(): client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=10000) - db = client["bids"] + db = client["bidsAPI"] return db \ No newline at end of file diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 39fdd90..64bbd33 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -306,6 +306,6 @@ components: schema: type: object example: { - "Error": "{'': ['']}" + "Error": "Could not connect to database" } # -------------------------------------------- \ No newline at end of file From 15c2663cccf1b9bed18892d4e824b4af05ed1854 Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 4 Jul 2023 12:19:28 +0100 Subject: [PATCH 31/97] refactor: updated swagger _id fields Co-authored-by: Julio Velezmoro --- static/swagger_config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 64bbd33..311bda8 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -87,7 +87,7 @@ components: description: Bid document type: object required: - - id + - _id - tender - client - bid_date @@ -132,7 +132,7 @@ components: $ref: '#/components/schemas/QuestionsLink' self: $ref: '#/components/schemas/SelfLink' - id: + _id: type: string format: uuid example: "UUID('471fea1f-705c-4851-9a5b-df7bc2651428')" From 927bbb96187b4ab6b50a29b74fa186760e1904fe Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 4 Jul 2023 12:20:52 +0100 Subject: [PATCH 32/97] wip: tests for post route mocking db response --- tests/test_bid.py | 266 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 233 insertions(+), 33 deletions(-) diff --git a/tests/test_bid.py b/tests/test_bid.py index ae0802e..e90278c 100644 --- a/tests/test_bid.py +++ b/tests/test_bid.py @@ -1,6 +1,14 @@ from flask import Flask from datetime import datetime import pytest +import json +from unittest.mock import patch, MagicMock +from bson import ObjectId +from pymongo.errors import ConnectionFailure +from marshmallow import ValidationError +from flask import jsonify +from dbconfig.mongo_setup import dbConnection + from api.controllers.bid_controller import bid @@ -12,44 +20,236 @@ def client(): with app.test_client() as client: yield client -# Case 1: Valid data -def test_post_is_valid(client): - data = { - "tender": "Sample Tender", - "client": "Sample Client", - "bid_date": "20-06-2023", - "alias": "Sample Alias", - "bid_folder_url": "https://example.com/bid", - "feedback":{ - "feedback_description": "Sample feedback", - "feedback_url": "https://example.com/feedback" +def test_post_bid(client): + # Mock the necessary objects and methods + request_data = { + "tender": "Business Intelligence and request_data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "success": [ + { + "phase": 1, + "has_score": True, + "score": 28, + "out_of": 36 + }, + { + "phase": 2, + "has_score": False + } + ], + "failed": { + "phase": 3, + "has_score": True, + "score": 20, + "out_of": 36 + } } + mock_db = MagicMock() + mock_db_connection = MagicMock(return_value=mock_db) + mock_bids = MagicMock() + # mock_bids.insert_one.return_value = ObjectId("60e8b7a57cdef32e1cfe3a1b") + + # Patch the required methods and objects with the mocks + with patch("dbconfig.mongo_setup.dbConnection", mock_db_connection), \ + patch("api.controllers.bid_controller.BidSchema.dump", return_value=request_data), \ + patch("api.controllers.bid_controller.BidSchema.load", return_value=request_data), \ + patch("api.controllers.bid_controller.BidSchema.validate", return_value=True), \ + patch("dbconfig.mongo_setup.dbConnection.db['bids']", mock_bids): + # Make a POST request to the API endpoint + response = client.post("api/bids", json=request_data) + + # Assert the response + actual_response = json.loads(response.data) + expected_response = { + "_id": f"{actual_response['_id']}", + "alias": "ONS", + "bid_date": "2023-06-21", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "Office for National Statistics", + "failed": { + "has_score": True, + "out_of": 36, + "phase": 3, + "score": 20 + }, + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "last_updated": f"{actual_response['last_updated']}", + "links": { + "questions": f"/bids/{actual_response['_id']}/questions", + "self": f"/bids/{actual_response['_id']}" + }, + "status": "in_progress", + "success": [ + { + "has_score": True, + "out_of": 36, + "phase": 1, + "score": 28 + }, + { + "has_score": False, + "phase": 2 + } + ], + "tender": "Business Intelligence and Data Warehousing", + "was_successful": False } - response = client.post("api/bids", json=data) + + # assert response.status_code == 201 + # assert actual_response == expected_response + + # # Assert that the necessary methods were called assert response.status_code == 201 assert response.get_json() is not None - assert response.get_json().get("id") is not None - assert response.get_json().get("tender") == data.get("tender") - assert response.get_json().get("client") == data.get("client") - assert response.get_json().get("bid_date") == datetime.strptime(data.get("bid_date"), "%d-%m-%Y").isoformat() - assert response.get_json().get("alias") == data.get("alias") - assert response.get_json().get("bid_folder_url") == data.get("bid_folder_url") + assert response.get_json().get("_id") is not None + assert response.get_json().get("tender") == request_data.get("tender") + assert response.get_json().get("client") == request_data.get("client") + assert response.get_json().get("bid_date") == datetime.strptime(request_data.get("bid_date"), "%d-%m-%Y").isoformat() + assert response.get_json().get("alias") == request_data.get("alias") + assert response.get_json().get("bid_folder_url") == request_data.get("bid_folder_url") assert response.get_json().get("feedback") is not None - assert response.get_json().get("feedback_description") == data.get("feedback_description") - assert response.get_json().get("feedback_url") == data.get("feedback_url") - -# Case 2: Missing mandatory fields -def test_field_missing(client): - data = { - "client": "Sample Client", - "bid_date": "20-06-2023" + assert response.get_json().get("feedback_description") == request_data.get("feedback_description") + assert response.get_json().get("feedback_url") == request_data.get("feedback_url") + + # # Assert that the necessary methods were called + # mock_db_connection.assert_called_once() + # mock_bids.insert_one.assert_called_once_with(request_data) + +def test_post_bid_validation_error(client): + # Mock the necessary objects and methods + request_data = { + "tender": 42, + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "last_updated": "2023-06-27T14:05:17.623827", + "failed": { + "phase": 2, + "score": 22, + "has_score": True, + "out_of": 36 + }, + "feedback": { + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", + "description": "Feedback from client in detail" + }, + "success": [ + { + "phase": 2, + "score": 22, + "has_score": True, + "out_of": 36 } - response = client.post("api/bids", json=data) - assert response.status_code == 400 - assert response.get_json().get("error") == "Missing mandatory field: tender" + ], + "alias": "ONS", + "client": "Office for National Statistics", + "links": { + "questions": "https://localhost:8080/api//bids/96d69775-29af-46b1-aaf4-bfbdb1543412/questions", + "self": "https://localhost:8080/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428" + }, + "_id": "UUID('471fea1f-705c-4851-9a5b-df7bc2651428')", + "bid_date": "2023-06-21T00:00:00", + "status": "in_progress", + "was_successful": True +} -# Case 3: Invalid JSON -def test_post_is_invalid(client): - response = client.post("api/bids", data="Invalid JSON") + # Patch the required methods and objects with the mocks + with patch("api.controllers.bid_controller.BidRequestSchema.load", side_effect=ValidationError("Invalid data")): + # Make a POST request to the API endpoint + response = client.post("api/bids", json=request_data) + + # Assert the response assert response.status_code == 400 - assert response.get_json().get("error") == "Invalid JSON" \ No newline at end of file + assert json.loads(response.data) == {"Error": "Invalid data"} + +def test_post_bid_connection_failure(client): + # Mock the necessary objects and methods + request_data = { + "tender": "Business Intelligence and Data Warehousing", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "last_updated": "2023-06-27T14:05:17.623827", + "failed": { + "phase": 2, + "score": 22, + "has_score": True, + "out_of": 36 + }, + "feedback": { + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", + "description": "Feedback from client in detail" + }, + "success": [ + { + "phase": 2, + "score": 22, + "has_score": True, + "out_of": 36 + } + ], + "alias": "ONS", + "client": "Office for National Statistics", + "links": { + "questions": "https://localhost:8080/api//bids/96d69775-29af-46b1-aaf4-bfbdb1543412/questions", + "self": "https://localhost:8080/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428" + }, + "_id": "UUID('471fea1f-705c-4851-9a5b-df7bc2651428')", + "bid_date": "2023-06-21T00:00:00", + "status": "in_progress", + "was_successful": True +} + mock_db_connection = MagicMock(side_effect=ConnectionFailure) + + # Patch the required methods and objects with the mocks + with patch("api.controllers.bid_controller.dbConnection", mock_db_connection), \ + patch("api.controllers.bid_controller.BidSchema.dump", return_value=request_data): + # Make a POST request to the API endpoint + response = client.post("api/bids", json=request_data) + + # Assert the response + assert response.status_code == 500 + assert json.loads(response.data) == {"Error": "Could not connect to database"} + +# Note: The above tests assume you have a Flask test client available as `client`. + + +# # Case 1: Valid data +# def test_post_is_valid(client): +# data = { +# "tender": "Sample Tender", +# "client": "Sample Client", +# "bid_date": "20-06-2023", +# "alias": "Sample Alias", +# "bid_folder_url": "https://example.com/bid", +# "feedback":{ +# "feedback_description": "Sample feedback", +# "feedback_url": "https://example.com/feedback" +# } +# } + + +# # Case 2: Missing mandatory fields +# def test_field_missing(client): +# data = { +# "client": "Sample Client", +# "bid_date": "20-06-2023" +# } +# response = client.post("api/bids", json=data) +# assert response.status_code == 400 +# assert response.get_json().get("error") == "Missing mandatory field: tender" + +# # Case 3: Invalid JSON +# def test_post_is_invalid(client): +# response = client.post("api/bids", data="Invalid JSON") +# assert response.status_code == 400 +# assert response.get_json().get("error") == "Invalid JSON" + +if __name__ == "__main__": + pytest.main() \ No newline at end of file From 4b3df652e5da864a50189ebc4aa79cd6e06d013d Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 4 Jul 2023 14:30:20 +0100 Subject: [PATCH 33/97] feat: added swagger spec for get_all_bids endpoint --- TODO.md | 3 +- request_examples/all_fields.http | 6 +-- request_examples/invalid_int.http | 6 +-- request_examples/invalid_string.http | 6 +-- request_examples/invalid_url.http | 6 +-- request_examples/missing_mandatory_field.http | 6 +-- static/swagger_config.yml | 40 ++++++++++++------- 7 files changed, 33 insertions(+), 40 deletions(-) diff --git a/TODO.md b/TODO.md index c146f07..a0f7a1b 100644 --- a/TODO.md +++ b/TODO.md @@ -1 +1,2 @@ -- SWAGGER: how to do multiple examples of 400 response \ No newline at end of file +- SWAGGER: how to do multiple examples of 400 response +- QUESTION: error responses as text/plain or application/json? \ No newline at end of file diff --git a/request_examples/all_fields.http b/request_examples/all_fields.http index 5b80433..50ab757 100644 --- a/request_examples/all_fields.http +++ b/request_examples/all_fields.http @@ -17,14 +17,10 @@ Content-Type: application/json "has_score": true, "score": 28, "out_of": 36 - }, - { - "phase": 2, - "has_score": false } ], "failed": { - "phase": 3, + "phase": 2, "has_score": true, "score": 20, "out_of": 36 diff --git a/request_examples/invalid_int.http b/request_examples/invalid_int.http index d2a6c42..7c879a4 100644 --- a/request_examples/invalid_int.http +++ b/request_examples/invalid_int.http @@ -17,14 +17,10 @@ Content-Type: application/json "has_score": true, "score": 28, "out_of": 36 - }, - { - "phase": 2, - "has_score": false } ], "failed": { - "phase": 3, + "phase": 2, "has_score": true, "score": 20, "out_of": 36 diff --git a/request_examples/invalid_string.http b/request_examples/invalid_string.http index e8c2756..698f822 100644 --- a/request_examples/invalid_string.http +++ b/request_examples/invalid_string.http @@ -17,14 +17,10 @@ Content-Type: application/json "has_score": true, "score": 28, "out_of": 36 - }, - { - "phase": 2, - "has_score": false } ], "failed": { - "phase": 3, + "phase": 2, "has_score": true, "score": 20, "out_of": 36 diff --git a/request_examples/invalid_url.http b/request_examples/invalid_url.http index aeae3b0..d806f3c 100644 --- a/request_examples/invalid_url.http +++ b/request_examples/invalid_url.http @@ -17,14 +17,10 @@ Content-Type: application/json "has_score": true, "score": 28, "out_of": 36 - }, - { - "phase": 2, - "has_score": false } ], "failed": { - "phase": 3, + "phase": 2, "has_score": true, "score": 20, "out_of": 36 diff --git a/request_examples/missing_mandatory_field.http b/request_examples/missing_mandatory_field.http index 3425e3f..9d39d7a 100644 --- a/request_examples/missing_mandatory_field.http +++ b/request_examples/missing_mandatory_field.http @@ -16,14 +16,10 @@ Content-Type: application/json "has_score": true, "score": 28, "out_of": 36 - }, - { - "phase": 2, - "has_score": false } ], "failed": { - "phase": 3, + "phase": 2, "has_score": true, "score": 20, "out_of": 36 diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 311bda8..3d49d56 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -46,16 +46,29 @@ paths: tags: - bids summary: Returns all bids - description: Returns all bids + description: A JSON with item count and array of all bids responses: '200': # status code - description: A JSON array of bids + description: Successful operation content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/Bid' + type: object + properties: + total_count: + type: integer + example: 1 + items: + type: array + items: + $ref: '#/components/schemas/Bid' + '500': + description: Internal server error + content: + text/plain: + schema: + type: string + example: Internal server error # -------------------------------------------- post: tags: @@ -139,15 +152,15 @@ components: bid_date: type: string format: ISO-8601 - example: '2023-06-21T00:00:00' + example: '2023-06-21' status: type: string description: Bid Status example: in_progress enum: - - IN_PROGRESS - - DELETED - - COMPLETED + - in_progress + - deleted + - completed was_successful: type: boolean example: true @@ -162,6 +175,9 @@ components: phase: description: Phase of bid type: integer + enum: + - 1 + - 2 example: 2 score: description: Score achieved at phase @@ -264,14 +280,10 @@ components: "has_score": true, "score": 28, "out_of": 36 - }, - { - "phase": 2, - "has_score": false } ] failed: { - "phase": 3, + "phase": 2, "has_score": true, "score": 20, "out_of": 36 From ebe66dd91d2f73c22067e11e563451d3b79bd450 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Tue, 4 Jul 2023 14:43:37 +0100 Subject: [PATCH 34/97] feat: swagger spec created for get bid/id --- static/swag.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 static/swag.md diff --git a/static/swag.md b/static/swag.md new file mode 100644 index 0000000..844cf58 --- /dev/null +++ b/static/swag.md @@ -0,0 +1,45 @@ + /bids/{bid_id}: +# -------------------------------------------- + get: + tags: + - bids + summary: Returns a single bid + description: Returns a single bid + operationId: get_bid + parameters: + - name: bid_id + in: path + description: ID of bid to return + required: true + schema: + type: string + format: uuid + responses: + '200': + description: A single bid + content: + application/json: + schema: + $ref: '#/components/schemas/Bid' + '404': + description: Bid not found + content: + application/json: + schema: + type: object + example: { + "Error": "Bid not found" + } + '500': + $ref: '#/components/responses/InternalServerError' +# -------------------------------------------- + + + + +# -------------------------------------------- +# Schemas + _id: + type: string + format: uuid + example: "9af94206-adff-476c-b2f9-ed7be3468944" \ No newline at end of file From 3c7432769332fd4d5a404a314e36e716453ea0f8 Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 4 Jul 2023 15:57:36 +0100 Subject: [PATCH 35/97] feat: incorporated julio changes to swag --- static/swag.md | 45 --------------------------------------- static/swagger_config.yml | 37 +++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 46 deletions(-) delete mode 100644 static/swag.md diff --git a/static/swag.md b/static/swag.md deleted file mode 100644 index 844cf58..0000000 --- a/static/swag.md +++ /dev/null @@ -1,45 +0,0 @@ - /bids/{bid_id}: -# -------------------------------------------- - get: - tags: - - bids - summary: Returns a single bid - description: Returns a single bid - operationId: get_bid - parameters: - - name: bid_id - in: path - description: ID of bid to return - required: true - schema: - type: string - format: uuid - responses: - '200': - description: A single bid - content: - application/json: - schema: - $ref: '#/components/schemas/Bid' - '404': - description: Bid not found - content: - application/json: - schema: - type: object - example: { - "Error": "Bid not found" - } - '500': - $ref: '#/components/responses/InternalServerError' -# -------------------------------------------- - - - - -# -------------------------------------------- -# Schemas - _id: - type: string - format: uuid - example: "9af94206-adff-476c-b2f9-ed7be3468944" \ No newline at end of file diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 3d49d56..2c512e4 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -90,6 +90,41 @@ paths: $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalServerError' +# -------------------------------------------- + /bids/{bid_id}: +# -------------------------------------------- + get: + tags: + - bids + summary: Returns a single bid + description: Returns a single bid + operationId: get_bid + parameters: + - name: bid_id + in: path + description: ID of bid to return + required: true + schema: + type: string + format: uuid + responses: + '200': + description: A single bid + content: + application/json: + schema: + $ref: '#/components/schemas/Bid' + '404': + description: Bid not found + content: + application/json: + schema: + type: object + example: { + "Error": "Bid not found" + } + '500': + $ref: '#/components/responses/InternalServerError' # -------------------------------------------- # Components components: @@ -148,7 +183,7 @@ components: _id: type: string format: uuid - example: "UUID('471fea1f-705c-4851-9a5b-df7bc2651428')" + example: "471fea1f-705c-4851-9a5b-df7bc2651428" bid_date: type: string format: ISO-8601 From 6d2e3f8f33ea5920d5f1a9c60d8eeadda0cd41a4 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Tue, 4 Jul 2023 17:17:04 +0100 Subject: [PATCH 36/97] feat: phase schema validation and enum added --- api/schemas/bid_request_schema.py | 36 +++++++++++++++++++++++++++++-- api/schemas/phase_schema.py | 31 +++++++++++++++++++++----- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/api/schemas/bid_request_schema.py b/api/schemas/bid_request_schema.py index 863ccbb..9ced984 100644 --- a/api/schemas/bid_request_schema.py +++ b/api/schemas/bid_request_schema.py @@ -1,4 +1,4 @@ -from marshmallow import Schema, fields, post_load +from marshmallow import Schema, fields, post_load, validates, ValidationError from api.models.bid_model import BidModel from .links_schema import LinksSchema from .phase_schema import PhaseSchema @@ -15,12 +15,44 @@ class BidRequestSchema(Schema): bid_folder_url = fields.URL() status = fields.Enum(Status, by_value=True) links = fields.Nested(LinksSchema) - was_successful = fields.Bool() + was_successful = fields.Boolean() success = fields.List(fields.Nested(PhaseSchema)) failed = fields.Nested(PhaseSchema) feedback = fields.Nested(FeedbackSchema) last_updated = fields.DateTime() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.context["failed_phase_values"] = set() + @validates("success") + def validate_success(self, value): + phase_values = set() + for phase in value: + phase_value = phase.get("phase") + if phase_value in phase_values: + raise ValidationError("Phase value already exists in 'success' list and cannot be repeated.") + phase_values.add(phase_value) + + @validates("failed") + def validate_failed(self, value): + phase_value = value.get("phase") + if phase_value in self.context.get("success_phase_values", set()): + raise ValidationError("Phase value already exists in 'success' list and cannot be repeated.") + self.context["failed_phase_values"].add(phase_value) + + @validates("success") + def validate_success_and_failed(self, value): + success_phase_values = set() + failed_phase_values = self.context.get("failed_phase_values", set()) + for phase in value: + phase_value = phase.get("phase") + if phase_value in failed_phase_values: + raise ValidationError("Phase value already exists in 'failed' section and cannot be repeated.") + success_phase_values.add(phase_value) + + + # Creates a Bid instance after processing @post_load def makeBid(self, data, **kwargs): diff --git a/api/schemas/phase_schema.py b/api/schemas/phase_schema.py index ce65bce..7c649d0 100644 --- a/api/schemas/phase_schema.py +++ b/api/schemas/phase_schema.py @@ -1,7 +1,28 @@ -from marshmallow import Schema, fields +from marshmallow import Schema, fields, validates, ValidationError +from enum import Enum, unique + +@unique +class Phase(Enum): + PHASE_1 = 1 + PHASE_2 = 2 class PhaseSchema(Schema): - phase = fields.Int(required=True, strict=True) - has_score = fields.Bool(required=True) - score = fields.Int(strict=True) - out_of = fields.Int(strict=True) \ No newline at end of file + phase = fields.Integer(required=True) + has_score = fields.Boolean(required=True) + score = fields.Integer() + out_of = fields.Integer() + + @validates("phase") + def validate_phase(self, value): + if value not in [e.value for e in Phase]: + raise ValidationError("Invalid phase value. Allowed values are 1 or 2.") + + @validates("score") + def validate_score(self, value): + if self.context.get("has_score") and value is None: + raise ValidationError("Score is mandatory when has_score is set to true.") + + @validates("out_of") + def validate_out_of(self, value): + if self.context.get("has_score") and value is None: + raise ValidationError("Out_of is mandatory when has_score is set to true.") From f43b88bee3092426b843da2001292bbe0a77f672 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Wed, 5 Jul 2023 10:56:34 +0100 Subject: [PATCH 37/97] fix: validation for the phase schema Co-authored-by: Pira Tejasakulsin --- api/models/feedback_model.py | 5 ---- api/models/phase_model.py | 7 ----- api/models/status_enum.py | 3 ++- api/schemas/bid_request_schema.py | 4 --- api/schemas/phase_schema.py | 21 ++++++--------- request_examples/all_fields.http | 4 +-- .../missing_score_when_has_score_is_true.http | 27 +++++++++++++++++++ 7 files changed, 39 insertions(+), 32 deletions(-) delete mode 100644 api/models/feedback_model.py delete mode 100644 api/models/phase_model.py create mode 100644 request_examples/missing_score_when_has_score_is_true.http diff --git a/api/models/feedback_model.py b/api/models/feedback_model.py deleted file mode 100644 index 17ab1ed..0000000 --- a/api/models/feedback_model.py +++ /dev/null @@ -1,5 +0,0 @@ -# Schema for Feedback object -class FeedbackModel: - def __init__(self,description, url): - self.description = description - self.url = url \ No newline at end of file diff --git a/api/models/phase_model.py b/api/models/phase_model.py deleted file mode 100644 index e32662f..0000000 --- a/api/models/phase_model.py +++ /dev/null @@ -1,7 +0,0 @@ -# Schema for phaseInfo object -class PhaseModel: - def __init__(self, phase, has_score, score=None, out_of=None): - self.phase = phase - self.has_score = has_score - self.score = score - self.out_of = out_of diff --git a/api/models/status_enum.py b/api/models/status_enum.py index ce0f2c7..592e02a 100644 --- a/api/models/status_enum.py +++ b/api/models/status_enum.py @@ -1,6 +1,7 @@ -from enum import Enum +from enum import Enum, unique # Enum for status +@unique class Status(Enum): DELETED = "deleted" IN_PROGRESS = "in_progress" diff --git a/api/schemas/bid_request_schema.py b/api/schemas/bid_request_schema.py index 9ced984..46c359a 100644 --- a/api/schemas/bid_request_schema.py +++ b/api/schemas/bid_request_schema.py @@ -7,19 +7,15 @@ # Marshmallow schema for request body class BidRequestSchema(Schema): - _id = fields.UUID() tender = fields.Str(required=True, error_messages={"required": {"message": "Missing mandatory field"}}) client = fields.Str(required=True, error_messages={"required": {"message": "Missing mandatory field"}}) alias = fields.Str() bid_date = fields.Date(format='%d-%m-%Y', required=True, error_messages={"required": {"message": "Missing mandatory field"}}) bid_folder_url = fields.URL() - status = fields.Enum(Status, by_value=True) - links = fields.Nested(LinksSchema) was_successful = fields.Boolean() success = fields.List(fields.Nested(PhaseSchema)) failed = fields.Nested(PhaseSchema) feedback = fields.Nested(FeedbackSchema) - last_updated = fields.DateTime() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/api/schemas/phase_schema.py b/api/schemas/phase_schema.py index 7c649d0..c723821 100644 --- a/api/schemas/phase_schema.py +++ b/api/schemas/phase_schema.py @@ -1,4 +1,4 @@ -from marshmallow import Schema, fields, validates, ValidationError +from marshmallow import Schema, fields, validates_schema, ValidationError from enum import Enum, unique @unique @@ -7,22 +7,17 @@ class Phase(Enum): PHASE_2 = 2 class PhaseSchema(Schema): - phase = fields.Integer(required=True) + phase = fields.Enum(Phase, required=True, by_value=True) has_score = fields.Boolean(required=True) score = fields.Integer() out_of = fields.Integer() - @validates("phase") - def validate_phase(self, value): - if value not in [e.value for e in Phase]: - raise ValidationError("Invalid phase value. Allowed values are 1 or 2.") - - @validates("score") - def validate_score(self, value): - if self.context.get("has_score") and value is None: + @validates_schema + def validate_score(self, data, **kwargs): + if data["has_score"] is True and "score" not in data: raise ValidationError("Score is mandatory when has_score is set to true.") - @validates("out_of") - def validate_out_of(self, value): - if self.context.get("has_score") and value is None: + @validates_schema + def validate_out_of(self, data, **kwargs): + if data["has_score"] is True and "out_of" not in data: raise ValidationError("Out_of is mandatory when has_score is set to true.") diff --git a/request_examples/all_fields.http b/request_examples/all_fields.http index 50ab757..dd5094e 100644 --- a/request_examples/all_fields.http +++ b/request_examples/all_fields.http @@ -15,8 +15,8 @@ Content-Type: application/json { "phase": 1, "has_score": true, - "score": 28, - "out_of": 36 + "out_of": 36, + "score": 30 } ], "failed": { diff --git a/request_examples/missing_score_when_has_score_is_true.http b/request_examples/missing_score_when_has_score_is_true.http new file mode 100644 index 0000000..9e96a2a --- /dev/null +++ b/request_examples/missing_score_when_has_score_is_true.http @@ -0,0 +1,27 @@ +POST http://localhost:8080/api/bids HTTP/1.1 +Content-Type: application/json + +{ + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "success": [ + { + "phase": 1, + "has_score": true, + "out_of": 36 + } + ], + "failed": { + "phase": 2, + "has_score": true, + "score": 20, + "out_of": 36 + } +} \ No newline at end of file From cca05b23a44bfe0275f8bc60b9b29d850930c15a Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Wed, 5 Jul 2023 12:11:28 +0100 Subject: [PATCH 38/97] feat: get all bids from MongoDB --- api/controllers/bid_controller.py | 14 ++++++++++---- request_examples/get_all.http | 1 + static/swagger_config.yml | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 request_examples/get_all.http diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 4977f6f..5a7eaac 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -11,7 +11,15 @@ @bid.route("/bids", methods=["GET"]) def get_bids(): - return "Under construction", 200 + # Get all bids from database collection + try: + db = dbConnection() + bids = list(db['bids'].find({})) + # Serialize to a JSON-encoded string + return jsonify(bids), 200 + + except ConnectionFailure: + return showConnectionError() @bid.route("/bids", methods=["POST"]) def post_bid(): @@ -28,6 +36,4 @@ def post_bid(): except ValidationError as e: return jsonify({"Error": str(e)}), 400 except ConnectionFailure: - return showConnectionError() - - + return showConnectionError() \ No newline at end of file diff --git a/request_examples/get_all.http b/request_examples/get_all.http new file mode 100644 index 0000000..712676d --- /dev/null +++ b/request_examples/get_all.http @@ -0,0 +1 @@ +GET http://localhost:8080/api/bids HTTP/1.1 diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 2c512e4..d66c4b0 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -67,8 +67,8 @@ paths: content: text/plain: schema: - type: string - example: Internal server error + type: object + example: {"Error": "Could not connect to database"} # -------------------------------------------- post: tags: From f0cca03f1e19c3bfc47bbc2598e813609cfc6e46 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Wed, 5 Jul 2023 13:09:56 +0100 Subject: [PATCH 39/97] fix: format return bids --- api/controllers/bid_controller.py | 3 +-- static/swagger_config.yml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 5a7eaac..d12851c 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -15,8 +15,7 @@ def get_bids(): try: db = dbConnection() bids = list(db['bids'].find({})) - # Serialize to a JSON-encoded string - return jsonify(bids), 200 + return {'total_count': len(bids), 'items': bids}, 200 except ConnectionFailure: return showConnectionError() diff --git a/static/swagger_config.yml b/static/swagger_config.yml index d66c4b0..3f65e43 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -65,7 +65,7 @@ paths: '500': description: Internal server error content: - text/plain: + application/json: schema: type: object example: {"Error": "Could not connect to database"} From 61a0331ae077e970e7aab9a37914e55188956b06 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 5 Jul 2023 14:27:00 +0100 Subject: [PATCH 40/97] feat: added get by id and handling error responses --- api/controllers/bid_controller.py | 15 +++++++++++++-- helpers/helpers.py | 5 ++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 4977f6f..0d6ca95 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -3,8 +3,8 @@ from api.schemas.bid_schema import BidSchema from api.schemas.bid_request_schema import BidRequestSchema from dbconfig.mongo_setup import dbConnection -from pymongo.errors import ConnectionFailure -from helpers.helpers import showConnectionError +from pymongo.errors import ConnectionFailure, PyMongoError +from helpers.helpers import showConnectionError, showNotFoundError bid = Blueprint('bid', __name__) @@ -13,6 +13,17 @@ def get_bids(): return "Under construction", 200 +@bid.route("/bids/", methods=["GET"]) +def get_bid_by_id(bid_id): + try: + db = dbConnection() + data = db['bids'].find_one({"_id": bid_id}) + if data is None: + return showNotFoundError() + return data, 200 + except ConnectionFailure: + return showConnectionError() + @bid.route("/bids", methods=["POST"]) def post_bid(): # Create bid document and return error if input validation fails diff --git a/helpers/helpers.py b/helpers/helpers.py index 12bde5e..3cdd35b 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -1,4 +1,7 @@ from flask import jsonify def showConnectionError(): - return jsonify({"Error": "Could not connect to database"}), 500 \ No newline at end of file + return jsonify({"Error": "Could not connect to database"}), 500 + +def showNotFoundError(): + return jsonify({"Error": "Not found"}), 404 \ No newline at end of file From f4a0bf408c37483dd482d92920443a9a8fe491ce Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 5 Jul 2023 14:53:38 +0100 Subject: [PATCH 41/97] refactor: removed unused imports and added comments --- api/controllers/bid_controller.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 0d6ca95..dc0dc6c 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -3,7 +3,7 @@ from api.schemas.bid_schema import BidSchema from api.schemas.bid_request_schema import BidRequestSchema from dbconfig.mongo_setup import dbConnection -from pymongo.errors import ConnectionFailure, PyMongoError +from pymongo.errors import ConnectionFailure from helpers.helpers import showConnectionError, showNotFoundError @@ -15,29 +15,34 @@ def get_bids(): @bid.route("/bids/", methods=["GET"]) def get_bid_by_id(bid_id): + # Returns bid document where _id is equal to bid_id argument try: db = dbConnection() data = db['bids'].find_one({"_id": bid_id}) + # Return 404 response if not found / returns None if data is None: return showNotFoundError() return data, 200 + # Return 500 response in case of connection failure except ConnectionFailure: return showConnectionError() @bid.route("/bids", methods=["POST"]) def post_bid(): - # Create bid document and return error if input validation fails + # Create bid document and inserts it into collection try: db = dbConnection() bid_document = BidRequestSchema().load(request.json) - # Serialize to a JSON-encoded string + # Serialize to a JSON object data = BidSchema().dump(bid_document) # Insert document into database collection bids = db['bids'] bids.insert_one(data) return data, 201 + # Return 400 response if inout validation fails except ValidationError as e: return jsonify({"Error": str(e)}), 400 + # Return 500 response in case of connection failure except ConnectionFailure: return showConnectionError() From 31a5ad70da8b169d578a89891111de2999eb60f4 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Wed, 5 Jul 2023 18:08:01 +0100 Subject: [PATCH 42/97] test: get all bids and check connection failure --- api/controllers/bid_controller.py | 5 ++--- tests/test_get_bids.py | 34 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 tests/test_get_bids.py diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index d12851c..8e47954 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -15,11 +15,10 @@ def get_bids(): try: db = dbConnection() bids = list(db['bids'].find({})) - return {'total_count': len(bids), 'items': bids}, 200 - + return {'total_count': len(bids), 'items': bids}, 200 except ConnectionFailure: return showConnectionError() - + @bid.route("/bids", methods=["POST"]) def post_bid(): # Create bid document and return error if input validation fails diff --git a/tests/test_get_bids.py b/tests/test_get_bids.py new file mode 100644 index 0000000..6172dbf --- /dev/null +++ b/tests/test_get_bids.py @@ -0,0 +1,34 @@ +from flask import Flask +import pytest +from api.controllers.bid_controller import bid +from pymongo.errors import ConnectionFailure +from unittest.mock import patch, MagicMock + + +@pytest.fixture +def client(): + app = Flask(__name__) + app.register_blueprint(bid, url_prefix='/api') + with app.test_client() as client: + yield client + + +def test_get_bids(client): + # Mock the behavior of dbConnection and find methods + with patch('api.controllers.bid_controller.dbConnection') as mock_dbConnection: + # Create a MagicMock object to simulate the behavior of the find method + mock_find = MagicMock(return_value=[]) + # Set the return value of db['bids'].find to the MagicMock object + mock_dbConnection.return_value.__getitem__.return_value.find = mock_find + + response = client.get('/api/bids') + assert response.status_code == 200 + assert response.json == {'total_count': 0, 'items': []} + + +def test_get_bids_connection_error(client): + # Mock the behavior of dbConnection to raise ConnectionFailure + with patch('api.controllers.bid_controller.dbConnection', side_effect=ConnectionFailure): + response = client.get('/api/bids') + assert response.status_code == 500 + assert response.json == {"Error": "Could not connect to database"} \ No newline at end of file From b5daebf4ef353b24d05077daa149a095012c48f9 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Thu, 6 Jul 2023 10:00:23 +0100 Subject: [PATCH 43/97] test: post bid test --- tests/test_bid.py | 255 ----------------------------------------- tests/test_get_bids.py | 6 - tests/test_post_bid.py | 197 +++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+), 261 deletions(-) delete mode 100644 tests/test_bid.py create mode 100644 tests/test_post_bid.py diff --git a/tests/test_bid.py b/tests/test_bid.py deleted file mode 100644 index e90278c..0000000 --- a/tests/test_bid.py +++ /dev/null @@ -1,255 +0,0 @@ -from flask import Flask -from datetime import datetime -import pytest -import json -from unittest.mock import patch, MagicMock -from bson import ObjectId -from pymongo.errors import ConnectionFailure -from marshmallow import ValidationError -from flask import jsonify -from dbconfig.mongo_setup import dbConnection - - -from api.controllers.bid_controller import bid - - -@pytest.fixture -def client(): - app = Flask(__name__) - app.register_blueprint(bid, url_prefix='/api') - with app.test_client() as client: - yield client - -def test_post_bid(client): - # Mock the necessary objects and methods - request_data = { - "tender": "Business Intelligence and request_data Warehousing", - "client": "Office for National Statistics", - "bid_date": "21-06-2023", - "alias": "ONS", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "feedback": { - "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" - }, - "success": [ - { - "phase": 1, - "has_score": True, - "score": 28, - "out_of": 36 - }, - { - "phase": 2, - "has_score": False - } - ], - "failed": { - "phase": 3, - "has_score": True, - "score": 20, - "out_of": 36 - } - } - mock_db = MagicMock() - mock_db_connection = MagicMock(return_value=mock_db) - mock_bids = MagicMock() - # mock_bids.insert_one.return_value = ObjectId("60e8b7a57cdef32e1cfe3a1b") - - # Patch the required methods and objects with the mocks - with patch("dbconfig.mongo_setup.dbConnection", mock_db_connection), \ - patch("api.controllers.bid_controller.BidSchema.dump", return_value=request_data), \ - patch("api.controllers.bid_controller.BidSchema.load", return_value=request_data), \ - patch("api.controllers.bid_controller.BidSchema.validate", return_value=True), \ - patch("dbconfig.mongo_setup.dbConnection.db['bids']", mock_bids): - # Make a POST request to the API endpoint - response = client.post("api/bids", json=request_data) - - # Assert the response - actual_response = json.loads(response.data) - expected_response = { - "_id": f"{actual_response['_id']}", - "alias": "ONS", - "bid_date": "2023-06-21", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "client": "Office for National Statistics", - "failed": { - "has_score": True, - "out_of": 36, - "phase": 3, - "score": 20 - }, - "feedback": { - "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" - }, - "last_updated": f"{actual_response['last_updated']}", - "links": { - "questions": f"/bids/{actual_response['_id']}/questions", - "self": f"/bids/{actual_response['_id']}" - }, - "status": "in_progress", - "success": [ - { - "has_score": True, - "out_of": 36, - "phase": 1, - "score": 28 - }, - { - "has_score": False, - "phase": 2 - } - ], - "tender": "Business Intelligence and Data Warehousing", - "was_successful": False - } - - # assert response.status_code == 201 - # assert actual_response == expected_response - - # # Assert that the necessary methods were called - assert response.status_code == 201 - assert response.get_json() is not None - assert response.get_json().get("_id") is not None - assert response.get_json().get("tender") == request_data.get("tender") - assert response.get_json().get("client") == request_data.get("client") - assert response.get_json().get("bid_date") == datetime.strptime(request_data.get("bid_date"), "%d-%m-%Y").isoformat() - assert response.get_json().get("alias") == request_data.get("alias") - assert response.get_json().get("bid_folder_url") == request_data.get("bid_folder_url") - assert response.get_json().get("feedback") is not None - assert response.get_json().get("feedback_description") == request_data.get("feedback_description") - assert response.get_json().get("feedback_url") == request_data.get("feedback_url") - - # # Assert that the necessary methods were called - # mock_db_connection.assert_called_once() - # mock_bids.insert_one.assert_called_once_with(request_data) - -def test_post_bid_validation_error(client): - # Mock the necessary objects and methods - request_data = { - "tender": 42, - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "last_updated": "2023-06-27T14:05:17.623827", - "failed": { - "phase": 2, - "score": 22, - "has_score": True, - "out_of": 36 - }, - "feedback": { - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", - "description": "Feedback from client in detail" - }, - "success": [ - { - "phase": 2, - "score": 22, - "has_score": True, - "out_of": 36 - } - ], - "alias": "ONS", - "client": "Office for National Statistics", - "links": { - "questions": "https://localhost:8080/api//bids/96d69775-29af-46b1-aaf4-bfbdb1543412/questions", - "self": "https://localhost:8080/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428" - }, - "_id": "UUID('471fea1f-705c-4851-9a5b-df7bc2651428')", - "bid_date": "2023-06-21T00:00:00", - "status": "in_progress", - "was_successful": True -} - - # Patch the required methods and objects with the mocks - with patch("api.controllers.bid_controller.BidRequestSchema.load", side_effect=ValidationError("Invalid data")): - # Make a POST request to the API endpoint - response = client.post("api/bids", json=request_data) - - # Assert the response - assert response.status_code == 400 - assert json.loads(response.data) == {"Error": "Invalid data"} - -def test_post_bid_connection_failure(client): - # Mock the necessary objects and methods - request_data = { - "tender": "Business Intelligence and Data Warehousing", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "last_updated": "2023-06-27T14:05:17.623827", - "failed": { - "phase": 2, - "score": 22, - "has_score": True, - "out_of": 36 - }, - "feedback": { - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", - "description": "Feedback from client in detail" - }, - "success": [ - { - "phase": 2, - "score": 22, - "has_score": True, - "out_of": 36 - } - ], - "alias": "ONS", - "client": "Office for National Statistics", - "links": { - "questions": "https://localhost:8080/api//bids/96d69775-29af-46b1-aaf4-bfbdb1543412/questions", - "self": "https://localhost:8080/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428" - }, - "_id": "UUID('471fea1f-705c-4851-9a5b-df7bc2651428')", - "bid_date": "2023-06-21T00:00:00", - "status": "in_progress", - "was_successful": True -} - mock_db_connection = MagicMock(side_effect=ConnectionFailure) - - # Patch the required methods and objects with the mocks - with patch("api.controllers.bid_controller.dbConnection", mock_db_connection), \ - patch("api.controllers.bid_controller.BidSchema.dump", return_value=request_data): - # Make a POST request to the API endpoint - response = client.post("api/bids", json=request_data) - - # Assert the response - assert response.status_code == 500 - assert json.loads(response.data) == {"Error": "Could not connect to database"} - -# Note: The above tests assume you have a Flask test client available as `client`. - - -# # Case 1: Valid data -# def test_post_is_valid(client): -# data = { -# "tender": "Sample Tender", -# "client": "Sample Client", -# "bid_date": "20-06-2023", -# "alias": "Sample Alias", -# "bid_folder_url": "https://example.com/bid", -# "feedback":{ -# "feedback_description": "Sample feedback", -# "feedback_url": "https://example.com/feedback" -# } -# } - - -# # Case 2: Missing mandatory fields -# def test_field_missing(client): -# data = { -# "client": "Sample Client", -# "bid_date": "20-06-2023" -# } -# response = client.post("api/bids", json=data) -# assert response.status_code == 400 -# assert response.get_json().get("error") == "Missing mandatory field: tender" - -# # Case 3: Invalid JSON -# def test_post_is_invalid(client): -# response = client.post("api/bids", data="Invalid JSON") -# assert response.status_code == 400 -# assert response.get_json().get("error") == "Invalid JSON" - -if __name__ == "__main__": - pytest.main() \ No newline at end of file diff --git a/tests/test_get_bids.py b/tests/test_get_bids.py index 6172dbf..80e4ff7 100644 --- a/tests/test_get_bids.py +++ b/tests/test_get_bids.py @@ -26,9 +26,3 @@ def test_get_bids(client): assert response.json == {'total_count': 0, 'items': []} -def test_get_bids_connection_error(client): - # Mock the behavior of dbConnection to raise ConnectionFailure - with patch('api.controllers.bid_controller.dbConnection', side_effect=ConnectionFailure): - response = client.get('/api/bids') - assert response.status_code == 500 - assert response.json == {"Error": "Could not connect to database"} \ No newline at end of file diff --git a/tests/test_post_bid.py b/tests/test_post_bid.py new file mode 100644 index 0000000..35b937c --- /dev/null +++ b/tests/test_post_bid.py @@ -0,0 +1,197 @@ +import pytest +from flask import Flask +from unittest.mock import patch, MagicMock +from pymongo.errors import ConnectionFailure + +from api.controllers.bid_controller import bid + + +@pytest.fixture +def client(): + app = Flask(__name__) + app.register_blueprint(bid, url_prefix='/api') + with app.test_client() as client: + yield client + +# Case 1: Successful post +def test_post_is_successful(client): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "success": [ + { + "phase": 1, + "has_score": True, + "out_of": 36, + "score": 30 + } + ], + "failed": { + "phase": 2, + "has_score": True, + "score": 20, + "out_of": 36 + } + } + + # Mock the behavior of dbConnection + with patch('api.controllers.bid_controller.dbConnection') as mock_dbConnection: + # Create a MagicMock object to simulate the behavior of the insert_one method + mock_insert_one = MagicMock() + # Set the return value of db['bids'].insert_one to the MagicMock object + mock_dbConnection.return_value.__getitem__.return_value.insert_one = mock_insert_one + + response = client.post("api/bids", json=data) + assert response.status_code == 201 + # Check that the insert_one method was called with the correct argument + assert response.get_json() == mock_insert_one.call_args[0][0] + +# Case 2: Missing mandatory fields +def test_field_missing(client): + data = { + "client": "Sample Client", + "bid_date": "20-06-2023" + } + response = client.post("api/bids", json=data) + # Check that the response status code is 400 + assert response.status_code == 400 + # Check that the response body contains the correct error message with the missing field (tender) + assert response.get_json() == { + 'Error': "{'tender': {'message': 'Missing mandatory field'}}"} + +# Case 3: Connection error +def test_get_bids_connection_error(client): + # Mock the behavior of dbConnection to raise ConnectionFailure + with patch('api.controllers.bid_controller.dbConnection', side_effect=ConnectionFailure): + response = client.get('/api/bids') + assert response.status_code == 500 + assert response.json == {"Error": "Could not connect to database"} + +# Case 4: Neither success nor failed fields phase can be more than 2 +def test_phase_greater_than_2(client): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "success": [ + { + "phase": 1, + "has_score": True, + "out_of": 36, + "score": 30 + } + ], + "failed": { + "phase": 3, + "has_score": True, + "score": 20, + "out_of": 36 + } + } + + # Mock the behavior of dbConnection + with patch('api.controllers.bid_controller.dbConnection') as mock_dbConnection: + # Create a MagicMock object to simulate the behavior of the insert_one method + mock_insert_one = MagicMock() + # Set the return value of db['bids'].insert_one to the MagicMock object + mock_dbConnection.return_value.__getitem__.return_value.insert_one = mock_insert_one + + response = client.post("api/bids", json=data) + assert response.status_code == 400 + # Check that the insert_one method was called with the correct argument + assert response.get_json() == { + 'Error': "{'failed': {'phase': ['Must be one of: 1, 2.']}}"} + +# Case 5: Neither success nor failed fields can have the same phase +def test_same_phase(client): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "success": [ + { + "phase": 1, + "has_score": True, + "out_of": 36, + "score": 30 + } + ], + "failed": { + "phase": 1, + "has_score": True, + "score": 20, + "out_of": 36 + } + } + # Mock the behavior of dbConnection + with patch('api.controllers.bid_controller.dbConnection') as mock_dbConnection: + # Create a MagicMock object to simulate the behavior of the insert_one method + mock_insert_one = MagicMock() + # Set the return value of db['bids'].insert_one to the MagicMock object + mock_dbConnection.return_value.__getitem__.return_value.insert_one = mock_insert_one + + response = client.post("api/bids", json=data) + assert response.status_code == 400 + # Check that the insert_one method was called with the correct argument + assert response.get_json() == { + 'Error': '{\'success\': ["Phase value already exists in \'failed\' section and cannot be repeated."]}'} + +# Case 6: Success can not have the same phase in the list +def test_success_same_phase(client): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "success": [ + { + "phase": 1, + "has_score": True, + "out_of": 36, + "score": 30 + }, + { + "phase": 1, + "has_score": True, + "out_of": 50, + "score": 60 + } + ], + } + + # Mock the behavior of dbConnection + with patch('api.controllers.bid_controller.dbConnection') as mock_dbConnection: + # Create a MagicMock object to simulate the behavior of the insert_one method + mock_insert_one = MagicMock() + # Set the return value of db['bids'].insert_one to the MagicMock object + mock_dbConnection.return_value.__getitem__.return_value.insert_one = mock_insert_one + + response = client.post("api/bids", json=data) + assert response.status_code == 400 + # Check that the insert_one method was called with the correct argument + assert response.get_json() == { + 'Error': '{\'success\': ["Phase value already exists in \'success\' list and cannot be repeated."]}'} From 12a47d5b6bbf8112f0d211901fa258c57882ff88 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Thu, 6 Jul 2023 11:19:20 +0100 Subject: [PATCH 44/97] test: error if you are not able to retrive bids --- api/controllers/bid_controller.py | 2 ++ tests/test_get_bids.py | 22 +++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 8e47954..71c15f8 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -18,6 +18,8 @@ def get_bids(): return {'total_count': len(bids), 'items': bids}, 200 except ConnectionFailure: return showConnectionError() + except Exception: + return jsonify({"Error": "Could not retrieve bids"}), 500 @bid.route("/bids", methods=["POST"]) def post_bid(): diff --git a/tests/test_get_bids.py b/tests/test_get_bids.py index 80e4ff7..c122640 100644 --- a/tests/test_get_bids.py +++ b/tests/test_get_bids.py @@ -12,7 +12,7 @@ def client(): with app.test_client() as client: yield client - +# Case 1: Successful get def test_get_bids(client): # Mock the behavior of dbConnection and find methods with patch('api.controllers.bid_controller.dbConnection') as mock_dbConnection: @@ -25,4 +25,24 @@ def test_get_bids(client): assert response.status_code == 200 assert response.json == {'total_count': 0, 'items': []} +# Case 2: Connection error +def test_get_bids_connection_error(client): + # Mock the behavior of dbConnection to raise ConnectionFailure + with patch('api.controllers.bid_controller.dbConnection', side_effect=ConnectionFailure): + response = client.get('/api/bids') + assert response.status_code == 500 + assert response.json == {"Error": "Could not connect to database"} + + +# Case 3: Failed to call db['bids'].find +def test_get_bids_find_error(client): + # Mock the behavior of dbConnection and find methods + with patch('api.controllers.bid_controller.dbConnection') as mock_dbConnection: + # Create a MagicMock object to simulate the behavior of the find method + mock_find = MagicMock(side_effect=Exception) + # Set the return value of db['bids'].find to the MagicMock object + mock_dbConnection.return_value.__getitem__.return_value.find = mock_find + response = client.get('/api/bids') + assert response.status_code == 500 + assert response.json == {"Error": "Could not retrieve bids"} \ No newline at end of file From a43e8ca7fa3b693dd113e3dd705514969dbf84bc Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 6 Jul 2023 12:54:53 +0100 Subject: [PATCH 45/97] test: test suite for get_bid_by_id --- api/controllers/bid_controller.py | 11 +- api/schemas/valid_bid_id_schema.py | 4 + tests/test_bid.py | 255 ----------------------------- tests/test_get_bid_by_id.py | 103 ++++++++++++ 4 files changed, 117 insertions(+), 256 deletions(-) create mode 100644 api/schemas/valid_bid_id_schema.py delete mode 100644 tests/test_bid.py create mode 100644 tests/test_get_bid_by_id.py diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index dc0dc6c..5c06652 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -2,6 +2,7 @@ from marshmallow import ValidationError from api.schemas.bid_schema import BidSchema from api.schemas.bid_request_schema import BidRequestSchema +from api.schemas.valid_bid_id_schema import valid_bid_id_schema from dbconfig.mongo_setup import dbConnection from pymongo.errors import ConnectionFailure from helpers.helpers import showConnectionError, showNotFoundError @@ -15,6 +16,12 @@ def get_bids(): @bid.route("/bids/", methods=["GET"]) def get_bid_by_id(bid_id): + # Validates query param + try: + valid_bid_id = valid_bid_id_schema.load({"bid_id": bid_id}) + bid_id = valid_bid_id["bid_id"] + except ValidationError as e: + return jsonify({"Error": str(e)}), 400 # Returns bid document where _id is equal to bid_id argument try: db = dbConnection() @@ -32,6 +39,8 @@ def post_bid(): # Create bid document and inserts it into collection try: db = dbConnection() + # Deserialize and validate request against schema + # Process input and create data model bid_document = BidRequestSchema().load(request.json) # Serialize to a JSON object data = BidSchema().dump(bid_document) @@ -39,7 +48,7 @@ def post_bid(): bids = db['bids'] bids.insert_one(data) return data, 201 - # Return 400 response if inout validation fails + # Return 400 response if input validation fails except ValidationError as e: return jsonify({"Error": str(e)}), 400 # Return 500 response in case of connection failure diff --git a/api/schemas/valid_bid_id_schema.py b/api/schemas/valid_bid_id_schema.py new file mode 100644 index 0000000..6d36296 --- /dev/null +++ b/api/schemas/valid_bid_id_schema.py @@ -0,0 +1,4 @@ +from marshmallow import Schema, fields, validate + +class valid_bid_id_schema(Schema): + bid_id = fields.UUID(validate=validate.Length(min=1), error_messages={"validate": "Bid ID must not be empty"}) \ No newline at end of file diff --git a/tests/test_bid.py b/tests/test_bid.py deleted file mode 100644 index e90278c..0000000 --- a/tests/test_bid.py +++ /dev/null @@ -1,255 +0,0 @@ -from flask import Flask -from datetime import datetime -import pytest -import json -from unittest.mock import patch, MagicMock -from bson import ObjectId -from pymongo.errors import ConnectionFailure -from marshmallow import ValidationError -from flask import jsonify -from dbconfig.mongo_setup import dbConnection - - -from api.controllers.bid_controller import bid - - -@pytest.fixture -def client(): - app = Flask(__name__) - app.register_blueprint(bid, url_prefix='/api') - with app.test_client() as client: - yield client - -def test_post_bid(client): - # Mock the necessary objects and methods - request_data = { - "tender": "Business Intelligence and request_data Warehousing", - "client": "Office for National Statistics", - "bid_date": "21-06-2023", - "alias": "ONS", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "feedback": { - "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" - }, - "success": [ - { - "phase": 1, - "has_score": True, - "score": 28, - "out_of": 36 - }, - { - "phase": 2, - "has_score": False - } - ], - "failed": { - "phase": 3, - "has_score": True, - "score": 20, - "out_of": 36 - } - } - mock_db = MagicMock() - mock_db_connection = MagicMock(return_value=mock_db) - mock_bids = MagicMock() - # mock_bids.insert_one.return_value = ObjectId("60e8b7a57cdef32e1cfe3a1b") - - # Patch the required methods and objects with the mocks - with patch("dbconfig.mongo_setup.dbConnection", mock_db_connection), \ - patch("api.controllers.bid_controller.BidSchema.dump", return_value=request_data), \ - patch("api.controllers.bid_controller.BidSchema.load", return_value=request_data), \ - patch("api.controllers.bid_controller.BidSchema.validate", return_value=True), \ - patch("dbconfig.mongo_setup.dbConnection.db['bids']", mock_bids): - # Make a POST request to the API endpoint - response = client.post("api/bids", json=request_data) - - # Assert the response - actual_response = json.loads(response.data) - expected_response = { - "_id": f"{actual_response['_id']}", - "alias": "ONS", - "bid_date": "2023-06-21", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "client": "Office for National Statistics", - "failed": { - "has_score": True, - "out_of": 36, - "phase": 3, - "score": 20 - }, - "feedback": { - "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" - }, - "last_updated": f"{actual_response['last_updated']}", - "links": { - "questions": f"/bids/{actual_response['_id']}/questions", - "self": f"/bids/{actual_response['_id']}" - }, - "status": "in_progress", - "success": [ - { - "has_score": True, - "out_of": 36, - "phase": 1, - "score": 28 - }, - { - "has_score": False, - "phase": 2 - } - ], - "tender": "Business Intelligence and Data Warehousing", - "was_successful": False - } - - # assert response.status_code == 201 - # assert actual_response == expected_response - - # # Assert that the necessary methods were called - assert response.status_code == 201 - assert response.get_json() is not None - assert response.get_json().get("_id") is not None - assert response.get_json().get("tender") == request_data.get("tender") - assert response.get_json().get("client") == request_data.get("client") - assert response.get_json().get("bid_date") == datetime.strptime(request_data.get("bid_date"), "%d-%m-%Y").isoformat() - assert response.get_json().get("alias") == request_data.get("alias") - assert response.get_json().get("bid_folder_url") == request_data.get("bid_folder_url") - assert response.get_json().get("feedback") is not None - assert response.get_json().get("feedback_description") == request_data.get("feedback_description") - assert response.get_json().get("feedback_url") == request_data.get("feedback_url") - - # # Assert that the necessary methods were called - # mock_db_connection.assert_called_once() - # mock_bids.insert_one.assert_called_once_with(request_data) - -def test_post_bid_validation_error(client): - # Mock the necessary objects and methods - request_data = { - "tender": 42, - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "last_updated": "2023-06-27T14:05:17.623827", - "failed": { - "phase": 2, - "score": 22, - "has_score": True, - "out_of": 36 - }, - "feedback": { - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", - "description": "Feedback from client in detail" - }, - "success": [ - { - "phase": 2, - "score": 22, - "has_score": True, - "out_of": 36 - } - ], - "alias": "ONS", - "client": "Office for National Statistics", - "links": { - "questions": "https://localhost:8080/api//bids/96d69775-29af-46b1-aaf4-bfbdb1543412/questions", - "self": "https://localhost:8080/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428" - }, - "_id": "UUID('471fea1f-705c-4851-9a5b-df7bc2651428')", - "bid_date": "2023-06-21T00:00:00", - "status": "in_progress", - "was_successful": True -} - - # Patch the required methods and objects with the mocks - with patch("api.controllers.bid_controller.BidRequestSchema.load", side_effect=ValidationError("Invalid data")): - # Make a POST request to the API endpoint - response = client.post("api/bids", json=request_data) - - # Assert the response - assert response.status_code == 400 - assert json.loads(response.data) == {"Error": "Invalid data"} - -def test_post_bid_connection_failure(client): - # Mock the necessary objects and methods - request_data = { - "tender": "Business Intelligence and Data Warehousing", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "last_updated": "2023-06-27T14:05:17.623827", - "failed": { - "phase": 2, - "score": 22, - "has_score": True, - "out_of": 36 - }, - "feedback": { - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", - "description": "Feedback from client in detail" - }, - "success": [ - { - "phase": 2, - "score": 22, - "has_score": True, - "out_of": 36 - } - ], - "alias": "ONS", - "client": "Office for National Statistics", - "links": { - "questions": "https://localhost:8080/api//bids/96d69775-29af-46b1-aaf4-bfbdb1543412/questions", - "self": "https://localhost:8080/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428" - }, - "_id": "UUID('471fea1f-705c-4851-9a5b-df7bc2651428')", - "bid_date": "2023-06-21T00:00:00", - "status": "in_progress", - "was_successful": True -} - mock_db_connection = MagicMock(side_effect=ConnectionFailure) - - # Patch the required methods and objects with the mocks - with patch("api.controllers.bid_controller.dbConnection", mock_db_connection), \ - patch("api.controllers.bid_controller.BidSchema.dump", return_value=request_data): - # Make a POST request to the API endpoint - response = client.post("api/bids", json=request_data) - - # Assert the response - assert response.status_code == 500 - assert json.loads(response.data) == {"Error": "Could not connect to database"} - -# Note: The above tests assume you have a Flask test client available as `client`. - - -# # Case 1: Valid data -# def test_post_is_valid(client): -# data = { -# "tender": "Sample Tender", -# "client": "Sample Client", -# "bid_date": "20-06-2023", -# "alias": "Sample Alias", -# "bid_folder_url": "https://example.com/bid", -# "feedback":{ -# "feedback_description": "Sample feedback", -# "feedback_url": "https://example.com/feedback" -# } -# } - - -# # Case 2: Missing mandatory fields -# def test_field_missing(client): -# data = { -# "client": "Sample Client", -# "bid_date": "20-06-2023" -# } -# response = client.post("api/bids", json=data) -# assert response.status_code == 400 -# assert response.get_json().get("error") == "Missing mandatory field: tender" - -# # Case 3: Invalid JSON -# def test_post_is_invalid(client): -# response = client.post("api/bids", data="Invalid JSON") -# assert response.status_code == 400 -# assert response.get_json().get("error") == "Invalid JSON" - -if __name__ == "__main__": - pytest.main() \ No newline at end of file diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py new file mode 100644 index 0000000..73c5aca --- /dev/null +++ b/tests/test_get_bid_by_id.py @@ -0,0 +1,103 @@ +from flask import Flask +import pytest +from api.controllers.bid_controller import bid +from pymongo.errors import ConnectionFailure +from unittest.mock import patch, MagicMock +from marshmallow import ValidationError + + +@pytest.fixture +def client(): + app = Flask(__name__) + app.register_blueprint(bid, url_prefix='/api') + with app.test_client() as client: + yield client + +# Case 1: Successful get_bid_by_id +def test_get_bid_by_id_success(client): + # Create MagicMock objects for mocking the necessary functions and objects + mock_db = MagicMock() # Mock the database object + mock_dbConnection = MagicMock(return_value=mock_db) # Mock the dbConnection function + mock_valid_bid_id_schema = MagicMock() # Mock the valid_bid_id_schema object + + # Set up the return value of valid_bid_id_schema.load + mock_valid_bid_id_schema.load.return_value = {'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'} + + # Create a MagicMock object for the find_one method and assign it to the appropriate attribute + mock_find_one = MagicMock(return_value={'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'tender': 'Business Intelligence and Data Warehousing'}) + mock_dbConnection.return_value.__getitem__.return_value.find_one = mock_find_one + + # Patch the necessary functions and objects with the MagicMock objects + with patch('api.controllers.bid_controller.dbConnection', mock_dbConnection), \ + patch('api.controllers.bid_controller.valid_bid_id_schema', mock_valid_bid_id_schema): + + # Call the endpoint with the desired URL + response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + + # Assertions + mock_valid_bid_id_schema.load.assert_called_once_with({'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) + mock_dbConnection.assert_called_once() + mock_db['bids'].find_one.assert_called_once_with({'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) + assert response.status_code == 200 + assert response.json == {'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'tender': 'Business Intelligence and Data Warehousing'} + +# Case 2: Connection error +def test_get_bids_connection_error(client): + mock_valid_bid_id_schema = MagicMock() # Mock the valid_bid_id_schema object + + # Set up the return value of valid_bid_id_schema.load + mock_valid_bid_id_schema.load.return_value = {'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'} + + # Mock the behavior of dbConnection to raise ConnectionFailure + with patch('api.controllers.bid_controller.dbConnection', side_effect=ConnectionFailure), \ + patch('api.controllers.bid_controller.valid_bid_id_schema', mock_valid_bid_id_schema): + response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + assert response.status_code == 500 + assert response.json == {"Error": "Could not connect to database"} + + +# Case 3: Bid not found +def test_get_bid_by_id_not_found(client): + # Create MagicMock objects for mocking the necessary functions and objects + mock_db = MagicMock() # Mock the database object + mock_dbConnection = MagicMock(return_value=mock_db) # Mock the dbConnection function + mock_valid_bid_id_schema = MagicMock() # Mock the valid_bid_id_schema object + + # Set up the return value of valid_bid_id_schema.load + mock_valid_bid_id_schema.load.return_value = {'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'} + + # Create a MagicMock object for the find_one method and assign it to the appropriate attribute + mock_find_one = MagicMock(return_value=None) # Simulate not finding the bid + mock_dbConnection.return_value.__getitem__.return_value.find_one = mock_find_one + + # Patch the necessary functions and objects with the MagicMock objects + with patch('api.controllers.bid_controller.dbConnection', mock_dbConnection), \ + patch('api.controllers.bid_controller.valid_bid_id_schema', mock_valid_bid_id_schema): + + # Call the endpoint with the desired URL + response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + + # Assertions + mock_valid_bid_id_schema.load.assert_called_once_with({'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) + mock_dbConnection.assert_called_once() + mock_db['bids'].find_one.assert_called_once_with({'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) + assert response.status_code == 404 + assert response.json == {"Error": "Not found"} + +# # Case 4: Validation error +# def test_get_bid_by_id_validation_error(client): +# # Create a MagicMock object for mocking the valid_bid_id_schema object +# mock_valid_bid_id_schema = MagicMock() + +# # Set up the side effect of valid_bid_id_schema.load to raise a ValidationError +# mock_valid_bid_id_schema.load.side_effect = ValidationError('Bid ID must not be empty') + +# # Patch the necessary functions and objects with the MagicMock object +# with patch('api.controllers.bid_controller.valid_bid_id_schema', mock_valid_bid_id_schema): +# # Call the endpoint with the desired URL +# response = client.get('/api/bids/') + +# # Assertions +# mock_valid_bid_id_schema.load.assert_called_once_with({'bid_id': ''}) +# assert response.status_code == 400 +# assert response.json == {"Error": "Bid ID must not be empty"} \ No newline at end of file From a53223513c0f0858f7963a84ca39be3b984ad59a Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 6 Jul 2023 14:09:26 +0100 Subject: [PATCH 46/97] test: added validation error test --- api/schemas/valid_bid_id_schema.py | 2 +- tests/test_get_bid_by_id.py | 34 +++++++++++++++--------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/api/schemas/valid_bid_id_schema.py b/api/schemas/valid_bid_id_schema.py index 6d36296..7e62b79 100644 --- a/api/schemas/valid_bid_id_schema.py +++ b/api/schemas/valid_bid_id_schema.py @@ -1,4 +1,4 @@ from marshmallow import Schema, fields, validate class valid_bid_id_schema(Schema): - bid_id = fields.UUID(validate=validate.Length(min=1), error_messages={"validate": "Bid ID must not be empty"}) \ No newline at end of file + bid_id = fields.UUID(validate=validate.Length(min=1), error_messages={"validate": "Invalid Bid ID"}) \ No newline at end of file diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index 73c5aca..b2baa73 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -84,20 +84,20 @@ def test_get_bid_by_id_not_found(client): assert response.status_code == 404 assert response.json == {"Error": "Not found"} -# # Case 4: Validation error -# def test_get_bid_by_id_validation_error(client): -# # Create a MagicMock object for mocking the valid_bid_id_schema object -# mock_valid_bid_id_schema = MagicMock() - -# # Set up the side effect of valid_bid_id_schema.load to raise a ValidationError -# mock_valid_bid_id_schema.load.side_effect = ValidationError('Bid ID must not be empty') - -# # Patch the necessary functions and objects with the MagicMock object -# with patch('api.controllers.bid_controller.valid_bid_id_schema', mock_valid_bid_id_schema): -# # Call the endpoint with the desired URL -# response = client.get('/api/bids/') - -# # Assertions -# mock_valid_bid_id_schema.load.assert_called_once_with({'bid_id': ''}) -# assert response.status_code == 400 -# assert response.json == {"Error": "Bid ID must not be empty"} \ No newline at end of file +# Case 4: Validation error +def test_get_bid_by_id_validation_error(client): + # Create a MagicMock object for mocking the valid_bid_id_schema object + mock_valid_bid_id_schema = MagicMock() + + # Set up the side effect of valid_bid_id_schema.load to raise a ValidationError + mock_valid_bid_id_schema.load.side_effect = ValidationError('Invalid Bid ID') + + # Patch the necessary functions and objects with the MagicMock object + with patch('api.controllers.bid_controller.valid_bid_id_schema', mock_valid_bid_id_schema): + # Call the endpoint with the desired URL + response = client.get('/api/bids/invalid_bid_id') + + # Assertions + mock_valid_bid_id_schema.load.assert_called_once_with({'bid_id': 'invalid_bid_id'}) + assert response.status_code == 400 + assert response.json == {"Error": "Invalid Bid ID"} \ No newline at end of file From 3b2c8d4a411152a1bc6f62ddb218020aa022111b Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 11 Jul 2023 14:38:46 +0100 Subject: [PATCH 47/97] fix: debugged valid_bid_id_schema --- api/controllers/bid_controller.py | 3 +- api/schemas/valid_bid_id_schema.py | 2 +- tests/test_BidRequestSchema.py | 50 ++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 tests/test_BidRequestSchema.py diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 280f88c..5dcaaa3 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -24,9 +24,10 @@ def get_bids(): @bid.route("/bids/", methods=["GET"]) def get_bid_by_id(bid_id): + print("bid_id", bid_id) # Validates query param try: - valid_bid_id = valid_bid_id_schema.load({"bid_id": bid_id}) + valid_bid_id = valid_bid_id_schema().load({"bid_id": bid_id}) bid_id = valid_bid_id["bid_id"] except ValidationError as e: return jsonify({"Error": str(e)}), 400 diff --git a/api/schemas/valid_bid_id_schema.py b/api/schemas/valid_bid_id_schema.py index 7e62b79..a776384 100644 --- a/api/schemas/valid_bid_id_schema.py +++ b/api/schemas/valid_bid_id_schema.py @@ -1,4 +1,4 @@ from marshmallow import Schema, fields, validate class valid_bid_id_schema(Schema): - bid_id = fields.UUID(validate=validate.Length(min=1), error_messages={"validate": "Invalid Bid ID"}) \ No newline at end of file + bid_id = fields.UUID(required=True) \ No newline at end of file diff --git a/tests/test_BidRequestSchema.py b/tests/test_BidRequestSchema.py new file mode 100644 index 0000000..ae3a8b4 --- /dev/null +++ b/tests/test_BidRequestSchema.py @@ -0,0 +1,50 @@ +import pytest +# from flask import jsonify, request +from api.schemas.bid_schema import BidSchema +from api.schemas.bid_request_schema import BidRequestSchema + +def test_bid_model(): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "success": [ + { + "phase": 1, + "has_score": True, + "out_of": 36, + "score": 30 + } + ], + "failed": { + "phase": 2, + "has_score": True, + "score": 20, + "out_of": 36 + } + } + bid_document = BidRequestSchema().load(data) + to_post = BidSchema().dump(bid_document) + + # Test that UUID is generated and is valid UUID + assert to_post["_id"] is not None + + # Test that status is set to in_progress + assert to_post["status"] == "in_progress" + + # Test that links object is generated and URLs are correct + id = to_post["_id"] + assert to_post["links"] is not None + assert "self" in to_post["links"] + assert to_post["links"]["self"] == f"/bids/{id}" + assert 'questions' in to_post["links"] + assert to_post["links"]["questions"] == f"/bids/{id}/questions" + + # Test that last_updated field has been added and is valid + assert to_post["last_updated"] is not None \ No newline at end of file From eab3874ad5dc25a5dadbf50f38200fc8c23bed05 Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 11 Jul 2023 16:14:47 +0100 Subject: [PATCH 48/97] test: bid model and schema validation --- api/controllers/bid_controller.py | 1 - api/schemas/feedback_schema.py | 2 +- helpers/helpers.py | 18 +++++++- request_examples/all_fields.http | 2 +- tests/test_BidRequestSchema.py | 73 ++++++++++++++++++++++++++++--- tests/test_get_bid_by_id.py | 13 +++--- 6 files changed, 93 insertions(+), 16 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 5dcaaa3..5de900e 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -24,7 +24,6 @@ def get_bids(): @bid.route("/bids/", methods=["GET"]) def get_bid_by_id(bid_id): - print("bid_id", bid_id) # Validates query param try: valid_bid_id = valid_bid_id_schema().load({"bid_id": bid_id}) diff --git a/api/schemas/feedback_schema.py b/api/schemas/feedback_schema.py index 1f55f6e..ec24452 100644 --- a/api/schemas/feedback_schema.py +++ b/api/schemas/feedback_schema.py @@ -2,4 +2,4 @@ class FeedbackSchema(Schema): description = fields.Str(required=True) - url = fields.Str(required=True) \ No newline at end of file + url = fields.URL(required=True) \ No newline at end of file diff --git a/helpers/helpers.py b/helpers/helpers.py index 3cdd35b..e9a89c6 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -1,7 +1,23 @@ from flask import jsonify +import uuid +from datetime import datetime def showConnectionError(): return jsonify({"Error": "Could not connect to database"}), 500 def showNotFoundError(): - return jsonify({"Error": "Not found"}), 404 \ No newline at end of file + return jsonify({"Error": "Not found"}), 404 + +def is_valid_uuid(string): + try: + uuid.UUID(str(string)) + return True + except ValueError: + return False + +def is_valid_isoformat(string): + try: + datetime.fromisoformat(string) + return True + except: + return False \ No newline at end of file diff --git a/request_examples/all_fields.http b/request_examples/all_fields.http index dd5094e..bcb562d 100644 --- a/request_examples/all_fields.http +++ b/request_examples/all_fields.http @@ -4,7 +4,7 @@ Content-Type: application/json { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", - "bid_date": "21-06-2023", + "bid_date": "2023-06-23", "alias": "ONS", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": { diff --git a/tests/test_BidRequestSchema.py b/tests/test_BidRequestSchema.py index ae3a8b4..ac01665 100644 --- a/tests/test_BidRequestSchema.py +++ b/tests/test_BidRequestSchema.py @@ -1,7 +1,8 @@ import pytest -# from flask import jsonify, request +from marshmallow import ValidationError from api.schemas.bid_schema import BidSchema from api.schemas.bid_request_schema import BidRequestSchema +from helpers.helpers import is_valid_uuid, is_valid_isoformat def test_bid_model(): data = { @@ -32,19 +33,79 @@ def test_bid_model(): bid_document = BidRequestSchema().load(data) to_post = BidSchema().dump(bid_document) + id = to_post["_id"] # Test that UUID is generated and is valid UUID assert to_post["_id"] is not None + assert is_valid_uuid(id) is True + # Test UUID validator + assert is_valid_uuid("99999") is False - # Test that status is set to in_progress - assert to_post["status"] == "in_progress" - # Test that links object is generated and URLs are correct - id = to_post["_id"] assert to_post["links"] is not None assert "self" in to_post["links"] assert to_post["links"]["self"] == f"/bids/{id}" assert 'questions' in to_post["links"] assert to_post["links"]["questions"] == f"/bids/{id}/questions" + # Test that status is set to in_progress + assert to_post["status"] == "in_progress" + # Test that last_updated field has been added and is valid - assert to_post["last_updated"] is not None \ No newline at end of file + assert to_post["last_updated"] is not None + assert is_valid_isoformat(to_post["last_updated"]) is True + # Test ISOformat validator + assert is_valid_isoformat("07-06-2023") is False + +def test_validate_tender(): + data = { + "tender": 42, + "client": "Office for National Statistics", + "bid_date": "21-06-2023" + } + with pytest.raises(ValidationError): + BidRequestSchema().load(data) + +def test_validate_client(): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": 42, + "bid_date": "21-06-2023" + } + with pytest.raises(ValidationError): + BidRequestSchema().load(data) + +def test_validate_bid_date(): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "2023-12-25" + } + with pytest.raises(ValidationError): + BidRequestSchema().load(data) + +def test_validate_bid_folder_url(): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "bid_folder_url": "Not a valid URL" + } + + with pytest.raises(ValidationError): + BidRequestSchema().load(data) + +def test_validate_feedback(): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": 42, + "url": "Invalid URL" + } + } + + with pytest.raises(ValidationError): + BidRequestSchema().load(data) \ No newline at end of file diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index b2baa73..3cbeab7 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -4,6 +4,7 @@ from pymongo.errors import ConnectionFailure from unittest.mock import patch, MagicMock from marshmallow import ValidationError +from api.schemas.valid_bid_id_schema import valid_bid_id_schema @pytest.fixture @@ -21,7 +22,7 @@ def test_get_bid_by_id_success(client): mock_valid_bid_id_schema = MagicMock() # Mock the valid_bid_id_schema object # Set up the return value of valid_bid_id_schema.load - mock_valid_bid_id_schema.load.return_value = {'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'} + mock_valid_bid_id_schema().load.return_value = {'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'} # Create a MagicMock object for the find_one method and assign it to the appropriate attribute mock_find_one = MagicMock(return_value={'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'tender': 'Business Intelligence and Data Warehousing'}) @@ -35,7 +36,7 @@ def test_get_bid_by_id_success(client): response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') # Assertions - mock_valid_bid_id_schema.load.assert_called_once_with({'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) + mock_valid_bid_id_schema().load.assert_called_once_with({'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) mock_dbConnection.assert_called_once() mock_db['bids'].find_one.assert_called_once_with({'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) assert response.status_code == 200 @@ -64,7 +65,7 @@ def test_get_bid_by_id_not_found(client): mock_valid_bid_id_schema = MagicMock() # Mock the valid_bid_id_schema object # Set up the return value of valid_bid_id_schema.load - mock_valid_bid_id_schema.load.return_value = {'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'} + mock_valid_bid_id_schema().load.return_value = {'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'} # Create a MagicMock object for the find_one method and assign it to the appropriate attribute mock_find_one = MagicMock(return_value=None) # Simulate not finding the bid @@ -78,7 +79,7 @@ def test_get_bid_by_id_not_found(client): response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') # Assertions - mock_valid_bid_id_schema.load.assert_called_once_with({'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) + mock_valid_bid_id_schema().load.assert_called_once_with({'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) mock_dbConnection.assert_called_once() mock_db['bids'].find_one.assert_called_once_with({'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) assert response.status_code == 404 @@ -90,7 +91,7 @@ def test_get_bid_by_id_validation_error(client): mock_valid_bid_id_schema = MagicMock() # Set up the side effect of valid_bid_id_schema.load to raise a ValidationError - mock_valid_bid_id_schema.load.side_effect = ValidationError('Invalid Bid ID') + mock_valid_bid_id_schema().load.side_effect = ValidationError('Invalid Bid ID') # Patch the necessary functions and objects with the MagicMock object with patch('api.controllers.bid_controller.valid_bid_id_schema', mock_valid_bid_id_schema): @@ -98,6 +99,6 @@ def test_get_bid_by_id_validation_error(client): response = client.get('/api/bids/invalid_bid_id') # Assertions - mock_valid_bid_id_schema.load.assert_called_once_with({'bid_id': 'invalid_bid_id'}) + mock_valid_bid_id_schema().load.assert_called_once_with({'bid_id': 'invalid_bid_id'}) assert response.status_code == 400 assert response.json == {"Error": "Invalid Bid ID"} \ No newline at end of file From e15ee6d99275aa780afb6c5aa77835ae06efce03 Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 11 Jul 2023 16:39:27 +0100 Subject: [PATCH 49/97] refactor: removed unused import --- tests/test_get_bid_by_id.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index 3cbeab7..efcfe17 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -4,7 +4,6 @@ from pymongo.errors import ConnectionFailure from unittest.mock import patch, MagicMock from marshmallow import ValidationError -from api.schemas.valid_bid_id_schema import valid_bid_id_schema @pytest.fixture From 078695d3aa14f2f2919bf3c26bde901e91097a04 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Tue, 11 Jul 2023 16:40:10 +0100 Subject: [PATCH 50/97] feat: Route for delete added Swag spec and test --- api/controllers/bid_controller.py | 38 ++++++++++++++++++++++++------- request_examples/delete.http | 1 + static/swagger_config.yml | 33 +++++++++++++++++++++++++++ tests/test_delete_bid.py | 38 +++++++++++++++++++++++++++++++ tests/test_get_bids.py | 6 ++--- 5 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 request_examples/delete.http create mode 100644 tests/test_delete_bid.py diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 280f88c..6b021e4 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -3,11 +3,13 @@ from api.schemas.bid_schema import BidSchema from api.schemas.bid_request_schema import BidRequestSchema from api.schemas.valid_bid_id_schema import valid_bid_id_schema +from api.models.status_enum import Status from dbconfig.mongo_setup import dbConnection from pymongo.errors import ConnectionFailure from helpers.helpers import showConnectionError, showNotFoundError + bid = Blueprint('bid', __name__) @bid.route("/bids", methods=["GET"]) @@ -15,8 +17,8 @@ def get_bids(): # Get all bids from database collection try: db = dbConnection() - bids = list(db['bids'].find({})) - return {'total_count': len(bids), 'items': bids}, 200 + data = list(db['bids'].find({})) + return {'total_count': len(data), 'items': data}, 200 except ConnectionFailure: return showConnectionError() except Exception: @@ -26,22 +28,43 @@ def get_bids(): def get_bid_by_id(bid_id): # Validates query param try: - valid_bid_id = valid_bid_id_schema.load({"bid_id": bid_id}) + valid_bid_id = valid_bid_id_schema().load({"bid_id": bid_id}) bid_id = valid_bid_id["bid_id"] except ValidationError as e: return jsonify({"Error": str(e)}), 400 - # Returns bid document where _id is equal to bid_id argument + + # Get bid by id from database collection try: db = dbConnection() data = db['bids'].find_one({"_id": bid_id}) # Return 404 response if not found / returns None - if data is None: + + if data is None or data['status'] == Status.DELETED.value: return showNotFoundError() return data, 200 # Return 500 response in case of connection failure except ConnectionFailure: return showConnectionError() +@bid.route("/bids/", methods=["PUT"]) +def change_status_to_deleted(bid_id): + # Validates query param + + try: + db = dbConnection() + data = db['bids'].find_one({"_id": bid_id}) + if data is None or data['status'] == Status.DELETED.value: + return showNotFoundError() + else: + db['bids'].update_one({"_id": bid_id}, {"$set": {"status": Status.DELETED.value}}) + return data, 204 + except ConnectionFailure: + return showConnectionError() + except ValidationError as e: + return jsonify({"Error": str(e)}), 400 + + + @bid.route("/bids", methods=["POST"]) def post_bid(): # Create bid document and inserts it into collection @@ -49,12 +72,11 @@ def post_bid(): db = dbConnection() # Deserialize and validate request against schema # Process input and create data model - bid_document = BidRequestSchema().load(request.json) + bid_document = BidRequestSchema().load(request.get_json()) # Serialize to a JSON object data = BidSchema().dump(bid_document) # Insert document into database collection - bids = db['bids'] - bids.insert_one(data) + db['bids'].insert_one(data) return data, 201 # Return 400 response if input validation fails except ValidationError as e: diff --git a/request_examples/delete.http b/request_examples/delete.http new file mode 100644 index 0000000..2f8f8ba --- /dev/null +++ b/request_examples/delete.http @@ -0,0 +1 @@ +PUT http://localhost:8080/api/bids/41fdf896-0581-45f0-a71d-430e3d8bd9b1 HTTP/1.1 diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 3f65e43..08d8442 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -125,6 +125,39 @@ paths: } '500': $ref: '#/components/responses/InternalServerError' + # -------------------------------------------- + put: + tags: + - bids + summary: Soft delete a bid + description: Soft delete a bid + operationId: delete_bid + parameters: + - name: bid_id + in: path + description: ID of bid to delete + required: true + schema: + type: string + format: uuid + responses: + # return 204 (No Content) + '204': + description: Bid deleted + content: + noContent: {} + + '404': + description: Bid not found + content: + application/json: + schema: + type: object + example: { + "Error": "Not found" + } + '500': + $ref: '#/components/responses/InternalServerError' # -------------------------------------------- # Components components: diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py new file mode 100644 index 0000000..6396d93 --- /dev/null +++ b/tests/test_delete_bid.py @@ -0,0 +1,38 @@ +from flask import Flask +import pytest +from api.controllers.bid_controller import bid +from pymongo.errors import ConnectionFailure +from unittest.mock import patch, MagicMock + + + + +@pytest.fixture +def client(): + app = Flask(__name__) + app.register_blueprint(bid, url_prefix='/api') + with app.test_client() as client: + yield client + +# Case 1: Successful delete a bid by changing status to deleted +def test_delete_bid_success(client): + with patch('api.controllers.bid_controller.dbConnection') as mock_dbConnection: + mock_db = MagicMock() + mock_dbConnection.return_value = mock_db + mock_db['bids'].update_one = MagicMock() + mock_db['bids'].update_one.return_value = {'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'status': 'deleted'} + response = client.put('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + assert response.status_code == 204 + assert response.get_json() == None + + +# Case 2: Failed to call database +def test_delete_bid_find_error(client): + with patch('api.controllers.bid_controller.dbConnection') as mock_dbConnection: + mock_db = MagicMock() + mock_dbConnection.return_value = mock_db + mock_db['bids'].update_one = MagicMock(side_effect=ConnectionFailure) + response = client.put('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + assert response.status_code == 500 + assert response.get_json() == {"Error": "Could not connect to database"} + \ No newline at end of file diff --git a/tests/test_get_bids.py b/tests/test_get_bids.py index c122640..e23f638 100644 --- a/tests/test_get_bids.py +++ b/tests/test_get_bids.py @@ -23,7 +23,7 @@ def test_get_bids(client): response = client.get('/api/bids') assert response.status_code == 200 - assert response.json == {'total_count': 0, 'items': []} + assert response.get_json() == {'total_count': 0, 'items': []} # Case 2: Connection error def test_get_bids_connection_error(client): @@ -31,7 +31,7 @@ def test_get_bids_connection_error(client): with patch('api.controllers.bid_controller.dbConnection', side_effect=ConnectionFailure): response = client.get('/api/bids') assert response.status_code == 500 - assert response.json == {"Error": "Could not connect to database"} + assert response.get_json() == {"Error": "Could not connect to database"} # Case 3: Failed to call db['bids'].find @@ -45,4 +45,4 @@ def test_get_bids_find_error(client): response = client.get('/api/bids') assert response.status_code == 500 - assert response.json == {"Error": "Could not retrieve bids"} \ No newline at end of file + assert response.get_json() == {"Error": "Could not retrieve bids"} \ No newline at end of file From 87c5c2c00c143485ea4a49ae277c2aae435cad8e Mon Sep 17 00:00:00 2001 From: Pira Tejasakulsin <47509562+piratejas@users.noreply.github.com> Date: Tue, 11 Jul 2023 16:52:32 +0100 Subject: [PATCH 51/97] Update all_fields.http Changed back test request --- request_examples/all_fields.http | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/request_examples/all_fields.http b/request_examples/all_fields.http index bcb562d..7f0a2e3 100644 --- a/request_examples/all_fields.http +++ b/request_examples/all_fields.http @@ -4,7 +4,7 @@ Content-Type: application/json { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", - "bid_date": "2023-06-23", + "bid_date": "23-06-2023", "alias": "ONS", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": { @@ -25,4 +25,4 @@ Content-Type: application/json "score": 20, "out_of": 36 } -} \ No newline at end of file +} From cc8904f3d8535d24fd807dae24f3faa8ed076671 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Wed, 12 Jul 2023 11:08:58 +0100 Subject: [PATCH 52/97] test: delete by id validation error --- api/controllers/bid_controller.py | 9 +++++++-- api/schemas/valid_bid_id_schema.py | 2 +- tests/test_delete_bid.py | 21 ++++++++++++++++++++- tests/test_get_bid_by_id.py | 8 ++++---- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 6b021e4..5ca1beb 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -39,7 +39,7 @@ def get_bid_by_id(bid_id): data = db['bids'].find_one({"_id": bid_id}) # Return 404 response if not found / returns None - if data is None or data['status'] == Status.DELETED.value: + if data is None: return showNotFoundError() return data, 200 # Return 500 response in case of connection failure @@ -48,7 +48,12 @@ def get_bid_by_id(bid_id): @bid.route("/bids/", methods=["PUT"]) def change_status_to_deleted(bid_id): - # Validates query param + # Validates query param + try: + valid_bid_id = valid_bid_id_schema().load({"bid_id": bid_id}) + bid_id = valid_bid_id["bid_id"] + except ValidationError as e: + return jsonify({"Error": str(e)}), 400 try: db = dbConnection() diff --git a/api/schemas/valid_bid_id_schema.py b/api/schemas/valid_bid_id_schema.py index a776384..c6b8fee 100644 --- a/api/schemas/valid_bid_id_schema.py +++ b/api/schemas/valid_bid_id_schema.py @@ -1,4 +1,4 @@ from marshmallow import Schema, fields, validate class valid_bid_id_schema(Schema): - bid_id = fields.UUID(required=True) \ No newline at end of file + bid_id = fields.Str(required=True, validate=validate.Length(min=1)) \ No newline at end of file diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py index 6396d93..a0e6da6 100644 --- a/tests/test_delete_bid.py +++ b/tests/test_delete_bid.py @@ -3,6 +3,7 @@ from api.controllers.bid_controller import bid from pymongo.errors import ConnectionFailure from unittest.mock import patch, MagicMock +from marshmallow import ValidationError @@ -35,4 +36,22 @@ def test_delete_bid_find_error(client): response = client.put('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} - \ No newline at end of file + + +# Case 3: Validation error +def test_delete_bid_by_id_validation_error(client): + # Create a MagicMock object for mocking the valid_bid_id_schema object + mock_valid_bid_id_schema = MagicMock() + + # Set up the side effect of valid_bid_id_schema.load to raise a ValidationError + mock_valid_bid_id_schema().load.side_effect = ValidationError('Invalid Bid ID') + + # Patch the necessary functions and objects with the MagicMock object + with patch('api.controllers.bid_controller.valid_bid_id_schema', mock_valid_bid_id_schema): + # Call the endpoint with the desired URL + response = client.get('/api/bids/invalid_bid_id') + + # Assertions + mock_valid_bid_id_schema().load.assert_called_once_with({'bid_id': 'invalid_bid_id'}) + assert response.status_code == 400 + assert response.get_json() == {"Error": "Invalid Bid ID"} \ No newline at end of file diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index efcfe17..34adc78 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -39,7 +39,7 @@ def test_get_bid_by_id_success(client): mock_dbConnection.assert_called_once() mock_db['bids'].find_one.assert_called_once_with({'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) assert response.status_code == 200 - assert response.json == {'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'tender': 'Business Intelligence and Data Warehousing'} + assert response.get_json() == {'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'tender': 'Business Intelligence and Data Warehousing'} # Case 2: Connection error def test_get_bids_connection_error(client): @@ -53,7 +53,7 @@ def test_get_bids_connection_error(client): patch('api.controllers.bid_controller.valid_bid_id_schema', mock_valid_bid_id_schema): response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') assert response.status_code == 500 - assert response.json == {"Error": "Could not connect to database"} + assert response.get_json() == {"Error": "Could not connect to database"} # Case 3: Bid not found @@ -82,7 +82,7 @@ def test_get_bid_by_id_not_found(client): mock_dbConnection.assert_called_once() mock_db['bids'].find_one.assert_called_once_with({'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) assert response.status_code == 404 - assert response.json == {"Error": "Not found"} + assert response.get_json() == {"Error": "Not found"} # Case 4: Validation error def test_get_bid_by_id_validation_error(client): @@ -100,4 +100,4 @@ def test_get_bid_by_id_validation_error(client): # Assertions mock_valid_bid_id_schema().load.assert_called_once_with({'bid_id': 'invalid_bid_id'}) assert response.status_code == 400 - assert response.json == {"Error": "Invalid Bid ID"} \ No newline at end of file + assert response.get_json() == {"Error": "Invalid Bid ID"} \ No newline at end of file From 06adef29baabe4cdc67d40a41808f1163ca99558 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 12 Jul 2023 12:24:45 +0100 Subject: [PATCH 53/97] feat: added query to get_all to filter by status not equals to deleted --- api/controllers/bid_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 5ca1beb..86bb965 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -17,7 +17,7 @@ def get_bids(): # Get all bids from database collection try: db = dbConnection() - data = list(db['bids'].find({})) + data = list(db['bids'].find({"status": {"$ne": Status.DELETED.value}})) return {'total_count': len(data), 'items': data}, 200 except ConnectionFailure: return showConnectionError() From 49ad68a1433b77d085f62d53e047482a763de10c Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Thu, 13 Jul 2023 10:34:26 +0100 Subject: [PATCH 54/97] refactor: testing without magicMock --- api/controllers/bid_controller.py | 8 +- api/models/bid_model.py | 17 ----- request_examples/delete.http | 2 +- tests/test_delete_bid.py | 62 ++++++--------- tests/test_get_bid_by_id.py | 103 +++++++++---------------- tests/test_get_bids.py | 50 ++++++------- tests/test_post_bid.py | 120 +++++++++++++++--------------- 7 files changed, 146 insertions(+), 216 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 5ca1beb..a259768 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -36,9 +36,8 @@ def get_bid_by_id(bid_id): # Get bid by id from database collection try: db = dbConnection() - data = db['bids'].find_one({"_id": bid_id}) + data = db['bids'].find_one({"_id": bid_id , "status": {"$ne": Status.DELETED.value}}) # Return 404 response if not found / returns None - if data is None: return showNotFoundError() return data, 200 @@ -57,8 +56,8 @@ def change_status_to_deleted(bid_id): try: db = dbConnection() - data = db['bids'].find_one({"_id": bid_id}) - if data is None or data['status'] == Status.DELETED.value: + data = db['bids'].find_one({"_id": bid_id , "status": {"$ne": Status.DELETED.value}}) + if data is None: return showNotFoundError() else: db['bids'].update_one({"_id": bid_id}, {"$set": {"status": Status.DELETED.value}}) @@ -75,7 +74,6 @@ def post_bid(): # Create bid document and inserts it into collection try: db = dbConnection() - # Deserialize and validate request against schema # Process input and create data model bid_document = BidRequestSchema().load(request.get_json()) # Serialize to a JSON object diff --git a/api/models/bid_model.py b/api/models/bid_model.py index 7d6c339..444e904 100644 --- a/api/models/bid_model.py +++ b/api/models/bid_model.py @@ -19,20 +19,3 @@ def __init__(self, tender, client, bid_date, alias=None, bid_folder_url=None, fe self.failed = failed self.feedback = feedback self.last_updated = datetime.now() - - - def addSuccessPhase(self, phase): - self.success.append(phase) - - def setFailedPhase(self, phase): - self.was_successful = False - self.failed = phase - - def addFeedback(self, feedback): - self.feedback = feedback - - def setStatus(self, status): - if isinstance(status, Status): - self.status = status.value - else: - raise ValueError("Invalid status. Please provide a valid Status enum value") \ No newline at end of file diff --git a/request_examples/delete.http b/request_examples/delete.http index 2f8f8ba..06aee60 100644 --- a/request_examples/delete.http +++ b/request_examples/delete.http @@ -1 +1 @@ -PUT http://localhost:8080/api/bids/41fdf896-0581-45f0-a71d-430e3d8bd9b1 HTTP/1.1 +GET http://localhost:8080/api/bids/3706739b-a88c-4c6c-8b4f-869038d09e4a HTTP/1.1 diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py index a0e6da6..e3b52bd 100644 --- a/tests/test_delete_bid.py +++ b/tests/test_delete_bid.py @@ -2,56 +2,42 @@ import pytest from api.controllers.bid_controller import bid from pymongo.errors import ConnectionFailure -from unittest.mock import patch, MagicMock +from unittest.mock import patch from marshmallow import ValidationError - - - @pytest.fixture def client(): app = Flask(__name__) app.register_blueprint(bid, url_prefix='/api') with app.test_client() as client: yield client - -# Case 1: Successful delete a bid by changing status to deleted -def test_delete_bid_success(client): - with patch('api.controllers.bid_controller.dbConnection') as mock_dbConnection: - mock_db = MagicMock() - mock_dbConnection.return_value = mock_db - mock_db['bids'].update_one = MagicMock() - mock_db['bids'].update_one.return_value = {'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'status': 'deleted'} - response = client.put('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') - assert response.status_code == 204 - assert response.get_json() == None +# Case 1: Successful delete a bid by changing status to deleted +@patch('api.controllers.bid_controller.dbConnection') +def test_delete_bid_success(mock_dbConnection, client): + mock_db = mock_dbConnection.return_value + mock_db['bids'].update_one.return_value = { + '_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', + 'status': 'deleted' + } + response = client.put('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + assert response.status_code == 204 + assert response.get_json() is None # Case 2: Failed to call database -def test_delete_bid_find_error(client): - with patch('api.controllers.bid_controller.dbConnection') as mock_dbConnection: - mock_db = MagicMock() - mock_dbConnection.return_value = mock_db - mock_db['bids'].update_one = MagicMock(side_effect=ConnectionFailure) - response = client.put('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') - assert response.status_code == 500 - assert response.get_json() == {"Error": "Could not connect to database"} - +@patch('api.controllers.bid_controller.dbConnection') +def test_delete_bid_find_error(mock_dbConnection, client): + mock_db = mock_dbConnection.return_value + mock_db['bids'].update_one.side_effect = ConnectionFailure + response = client.put('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + assert response.status_code == 500 + assert response.get_json() == {"Error": "Could not connect to database"} # Case 3: Validation error -def test_delete_bid_by_id_validation_error(client): - # Create a MagicMock object for mocking the valid_bid_id_schema object - mock_valid_bid_id_schema = MagicMock() - - # Set up the side effect of valid_bid_id_schema.load to raise a ValidationError - mock_valid_bid_id_schema().load.side_effect = ValidationError('Invalid Bid ID') - - # Patch the necessary functions and objects with the MagicMock object - with patch('api.controllers.bid_controller.valid_bid_id_schema', mock_valid_bid_id_schema): - # Call the endpoint with the desired URL - response = client.get('/api/bids/invalid_bid_id') - - # Assertions - mock_valid_bid_id_schema().load.assert_called_once_with({'bid_id': 'invalid_bid_id'}) +@patch('api.controllers.bid_controller.valid_bid_id_schema.load') +def test_get_bid_by_id_validation_error(mock_valid_bid_id_schema_load, client): + mock_valid_bid_id_schema_load.side_effect = ValidationError('Invalid Bid ID') + response = client.get('/api/bids/invalid_bid_id') + mock_valid_bid_id_schema_load.assert_called_once_with({'bid_id': 'invalid_bid_id'}) assert response.status_code == 400 assert response.get_json() == {"Error": "Invalid Bid ID"} \ No newline at end of file diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index 34adc78..1cb4f1b 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -2,7 +2,7 @@ import pytest from api.controllers.bid_controller import bid from pymongo.errors import ConnectionFailure -from unittest.mock import patch, MagicMock +from unittest.mock import patch from marshmallow import ValidationError @@ -14,90 +14,57 @@ def client(): yield client # Case 1: Successful get_bid_by_id -def test_get_bid_by_id_success(client): - # Create MagicMock objects for mocking the necessary functions and objects - mock_db = MagicMock() # Mock the database object - mock_dbConnection = MagicMock(return_value=mock_db) # Mock the dbConnection function - mock_valid_bid_id_schema = MagicMock() # Mock the valid_bid_id_schema object +@patch('api.controllers.bid_controller.dbConnection') +def test_get_bid_by_id_success(mock_dbConnection, client): + mock_db = mock_dbConnection.return_value + mock_db['bids'].find_one.return_value = { + '_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', + 'tender': 'Business Intelligence and Data Warehousing' + } - # Set up the return value of valid_bid_id_schema.load - mock_valid_bid_id_schema().load.return_value = {'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'} + with patch('api.controllers.bid_controller.valid_bid_id_schema.load') as mock_valid_bid_id_schema_load: + mock_valid_bid_id_schema_load.return_value = {'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'} - # Create a MagicMock object for the find_one method and assign it to the appropriate attribute - mock_find_one = MagicMock(return_value={'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'tender': 'Business Intelligence and Data Warehousing'}) - mock_dbConnection.return_value.__getitem__.return_value.find_one = mock_find_one - - # Patch the necessary functions and objects with the MagicMock objects - with patch('api.controllers.bid_controller.dbConnection', mock_dbConnection), \ - patch('api.controllers.bid_controller.valid_bid_id_schema', mock_valid_bid_id_schema): - - # Call the endpoint with the desired URL response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') - # Assertions - mock_valid_bid_id_schema().load.assert_called_once_with({'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) + mock_valid_bid_id_schema_load.assert_called_once_with({'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) mock_dbConnection.assert_called_once() - mock_db['bids'].find_one.assert_called_once_with({'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) + mock_db['bids'].find_one.assert_called_once_with({'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'status': {'$ne': 'deleted'}}) assert response.status_code == 200 - assert response.get_json() == {'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'tender': 'Business Intelligence and Data Warehousing'} + assert response.get_json() == { + '_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', + 'tender': 'Business Intelligence and Data Warehousing' + } # Case 2: Connection error -def test_get_bids_connection_error(client): - mock_valid_bid_id_schema = MagicMock() # Mock the valid_bid_id_schema object - - # Set up the return value of valid_bid_id_schema.load - mock_valid_bid_id_schema.load.return_value = {'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'} - - # Mock the behavior of dbConnection to raise ConnectionFailure - with patch('api.controllers.bid_controller.dbConnection', side_effect=ConnectionFailure), \ - patch('api.controllers.bid_controller.valid_bid_id_schema', mock_valid_bid_id_schema): - response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') - assert response.status_code == 500 - assert response.get_json() == {"Error": "Could not connect to database"} - +@patch('api.controllers.bid_controller.dbConnection', side_effect=ConnectionFailure) +def test_get_bids_connection_error(mock_dbConnection, client): + response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + assert response.status_code == 500 + assert response.get_json() == {"Error": "Could not connect to database"} # Case 3: Bid not found -def test_get_bid_by_id_not_found(client): - # Create MagicMock objects for mocking the necessary functions and objects - mock_db = MagicMock() # Mock the database object - mock_dbConnection = MagicMock(return_value=mock_db) # Mock the dbConnection function - mock_valid_bid_id_schema = MagicMock() # Mock the valid_bid_id_schema object +@patch('api.controllers.bid_controller.dbConnection') +def test_get_bid_by_id_not_found(mock_dbConnection, client): + mock_db = mock_dbConnection.return_value + mock_db['bids'].find_one.return_value = None - # Set up the return value of valid_bid_id_schema.load - mock_valid_bid_id_schema().load.return_value = {'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'} + with patch('api.controllers.bid_controller.valid_bid_id_schema.load') as mock_valid_bid_id_schema_load: + mock_valid_bid_id_schema_load.return_value = {'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'} - # Create a MagicMock object for the find_one method and assign it to the appropriate attribute - mock_find_one = MagicMock(return_value=None) # Simulate not finding the bid - mock_dbConnection.return_value.__getitem__.return_value.find_one = mock_find_one - - # Patch the necessary functions and objects with the MagicMock objects - with patch('api.controllers.bid_controller.dbConnection', mock_dbConnection), \ - patch('api.controllers.bid_controller.valid_bid_id_schema', mock_valid_bid_id_schema): - - # Call the endpoint with the desired URL response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') - # Assertions - mock_valid_bid_id_schema().load.assert_called_once_with({'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) + mock_valid_bid_id_schema_load.assert_called_once_with({'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) mock_dbConnection.assert_called_once() - mock_db['bids'].find_one.assert_called_once_with({'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) + mock_db['bids'].find_one.assert_called_once_with({'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'status': {'$ne': 'deleted'}}) assert response.status_code == 404 assert response.get_json() == {"Error": "Not found"} # Case 4: Validation error -def test_get_bid_by_id_validation_error(client): - # Create a MagicMock object for mocking the valid_bid_id_schema object - mock_valid_bid_id_schema = MagicMock() - - # Set up the side effect of valid_bid_id_schema.load to raise a ValidationError - mock_valid_bid_id_schema().load.side_effect = ValidationError('Invalid Bid ID') - - # Patch the necessary functions and objects with the MagicMock object - with patch('api.controllers.bid_controller.valid_bid_id_schema', mock_valid_bid_id_schema): - # Call the endpoint with the desired URL - response = client.get('/api/bids/invalid_bid_id') - - # Assertions - mock_valid_bid_id_schema().load.assert_called_once_with({'bid_id': 'invalid_bid_id'}) +@patch('api.controllers.bid_controller.valid_bid_id_schema.load') +def test_get_bid_by_id_validation_error(mock_valid_bid_id_schema_load, client): + mock_valid_bid_id_schema_load.side_effect = ValidationError('Invalid Bid ID') + response = client.get('/api/bids/invalid_bid_id') + mock_valid_bid_id_schema_load.assert_called_once_with({'bid_id': 'invalid_bid_id'}) assert response.status_code == 400 - assert response.get_json() == {"Error": "Invalid Bid ID"} \ No newline at end of file + assert response.get_json() == {"Error": "Invalid Bid ID"} diff --git a/tests/test_get_bids.py b/tests/test_get_bids.py index e23f638..7f7d2de 100644 --- a/tests/test_get_bids.py +++ b/tests/test_get_bids.py @@ -2,7 +2,7 @@ import pytest from api.controllers.bid_controller import bid from pymongo.errors import ConnectionFailure -from unittest.mock import patch, MagicMock +from unittest.mock import patch @pytest.fixture @@ -13,36 +13,28 @@ def client(): yield client # Case 1: Successful get -def test_get_bids(client): - # Mock the behavior of dbConnection and find methods - with patch('api.controllers.bid_controller.dbConnection') as mock_dbConnection: - # Create a MagicMock object to simulate the behavior of the find method - mock_find = MagicMock(return_value=[]) - # Set the return value of db['bids'].find to the MagicMock object - mock_dbConnection.return_value.__getitem__.return_value.find = mock_find +@patch('api.controllers.bid_controller.dbConnection') +def test_get_bids(mock_dbConnection, client): + mock_db = mock_dbConnection.return_value + mock_db['bids'].find.return_value = [] - response = client.get('/api/bids') - assert response.status_code == 200 - assert response.get_json() == {'total_count': 0, 'items': []} + response = client.get('/api/bids') + assert response.status_code == 200 + assert response.get_json() == {'total_count': 0, 'items': []} # Case 2: Connection error -def test_get_bids_connection_error(client): - # Mock the behavior of dbConnection to raise ConnectionFailure - with patch('api.controllers.bid_controller.dbConnection', side_effect=ConnectionFailure): - response = client.get('/api/bids') - assert response.status_code == 500 - assert response.get_json() == {"Error": "Could not connect to database"} - - +@patch('api.controllers.bid_controller.dbConnection', side_effect=ConnectionFailure) +def test_get_bids_connection_error(mock_dbConnection, client): + response = client.get('/api/bids') + assert response.status_code == 500 + assert response.get_json() == {"Error": "Could not connect to database"} + # Case 3: Failed to call db['bids'].find -def test_get_bids_find_error(client): - # Mock the behavior of dbConnection and find methods - with patch('api.controllers.bid_controller.dbConnection') as mock_dbConnection: - # Create a MagicMock object to simulate the behavior of the find method - mock_find = MagicMock(side_effect=Exception) - # Set the return value of db['bids'].find to the MagicMock object - mock_dbConnection.return_value.__getitem__.return_value.find = mock_find +@patch('api.controllers.bid_controller.dbConnection') +def test_get_bids_find_error(mock_dbConnection, client): + mock_db = mock_dbConnection.return_value + mock_db['bids'].find.side_effect = Exception - response = client.get('/api/bids') - assert response.status_code == 500 - assert response.get_json() == {"Error": "Could not retrieve bids"} \ No newline at end of file + response = client.get('/api/bids') + assert response.status_code == 500 + assert response.get_json() == {"Error": "Could not retrieve bids"} \ No newline at end of file diff --git a/tests/test_post_bid.py b/tests/test_post_bid.py index 35b937c..3293b3a 100644 --- a/tests/test_post_bid.py +++ b/tests/test_post_bid.py @@ -1,6 +1,6 @@ import pytest from flask import Flask -from unittest.mock import patch, MagicMock +from unittest.mock import patch from pymongo.errors import ConnectionFailure from api.controllers.bid_controller import bid @@ -13,8 +13,10 @@ def client(): with app.test_client() as client: yield client + # Case 1: Successful post -def test_post_is_successful(client): +@patch('api.controllers.bid_controller.dbConnection') +def test_post_is_successful(mock_dbConnection, client): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -42,16 +44,17 @@ def test_post_is_successful(client): } # Mock the behavior of dbConnection - with patch('api.controllers.bid_controller.dbConnection') as mock_dbConnection: - # Create a MagicMock object to simulate the behavior of the insert_one method - mock_insert_one = MagicMock() - # Set the return value of db['bids'].insert_one to the MagicMock object - mock_dbConnection.return_value.__getitem__.return_value.insert_one = mock_insert_one + mock_db = mock_dbConnection.return_value + mock_db['bids'].insert_one.return_value = data + + response = client.post("api/bids", json=data) + assert response.status_code == 201 + assert "_id" in response.get_json() and response.get_json()["_id"] is not None + assert "tender" in response.get_json() and response.get_json()["tender"] == "Business Intelligence and Data Warehousing" + assert "client" in response.get_json() and response.get_json()["client"] == "Office for National Statistics" + assert "last_updated" in response.get_json() and response.get_json()["last_updated"] is not None + assert "bid_date" in response.get_json() and response.get_json()["bid_date"] == "2023-06-21" - response = client.post("api/bids", json=data) - assert response.status_code == 201 - # Check that the insert_one method was called with the correct argument - assert response.get_json() == mock_insert_one.call_args[0][0] # Case 2: Missing mandatory fields def test_field_missing(client): @@ -59,23 +62,28 @@ def test_field_missing(client): "client": "Sample Client", "bid_date": "20-06-2023" } + response = client.post("api/bids", json=data) - # Check that the response status code is 400 assert response.status_code == 400 - # Check that the response body contains the correct error message with the missing field (tender) assert response.get_json() == { - 'Error': "{'tender': {'message': 'Missing mandatory field'}}"} + 'Error': "{'tender': {'message': 'Missing mandatory field'}}" + } + # Case 3: Connection error -def test_get_bids_connection_error(client): - # Mock the behavior of dbConnection to raise ConnectionFailure - with patch('api.controllers.bid_controller.dbConnection', side_effect=ConnectionFailure): - response = client.get('/api/bids') - assert response.status_code == 500 - assert response.json == {"Error": "Could not connect to database"} +@patch('api.controllers.bid_controller.dbConnection', side_effect=ConnectionFailure) +def test_get_bids_connection_error(mock_dbConnection, client): + # Mock the behavior of dbConnection + mock_db = mock_dbConnection.return_value + mock_db['bids'].insert_one.side_effect = Exception + response = client.get('/api/bids') + assert response.status_code == 500 + assert response.get_json() == {"Error": "Could not connect to database"} + # Case 4: Neither success nor failed fields phase can be more than 2 -def test_phase_greater_than_2(client): +@patch('api.controllers.bid_controller.dbConnection') +def test_phase_greater_than_2(mock_dbConnection, client): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -103,20 +111,19 @@ def test_phase_greater_than_2(client): } # Mock the behavior of dbConnection - with patch('api.controllers.bid_controller.dbConnection') as mock_dbConnection: - # Create a MagicMock object to simulate the behavior of the insert_one method - mock_insert_one = MagicMock() - # Set the return value of db['bids'].insert_one to the MagicMock object - mock_dbConnection.return_value.__getitem__.return_value.insert_one = mock_insert_one - - response = client.post("api/bids", json=data) - assert response.status_code == 400 - # Check that the insert_one method was called with the correct argument - assert response.get_json() == { - 'Error': "{'failed': {'phase': ['Must be one of: 1, 2.']}}"} + mock_db = mock_dbConnection.return_value + mock_db['bids'].insert_one.side_effect = Exception + + response = client.post("api/bids", json=data) + assert response.status_code == 400 + assert response.get_json() == { + 'Error': "{'failed': {'phase': ['Must be one of: 1, 2.']}}" + } + # Case 5: Neither success nor failed fields can have the same phase -def test_same_phase(client): +@patch('api.controllers.bid_controller.dbConnection') +def test_same_phase(mock_dbConnection, client): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -142,21 +149,21 @@ def test_same_phase(client): "out_of": 36 } } + # Mock the behavior of dbConnection - with patch('api.controllers.bid_controller.dbConnection') as mock_dbConnection: - # Create a MagicMock object to simulate the behavior of the insert_one method - mock_insert_one = MagicMock() - # Set the return value of db['bids'].insert_one to the MagicMock object - mock_dbConnection.return_value.__getitem__.return_value.insert_one = mock_insert_one - - response = client.post("api/bids", json=data) - assert response.status_code == 400 - # Check that the insert_one method was called with the correct argument - assert response.get_json() == { - 'Error': '{\'success\': ["Phase value already exists in \'failed\' section and cannot be repeated."]}'} - -# Case 6: Success can not have the same phase in the list -def test_success_same_phase(client): + mock_db = mock_dbConnection.return_value + mock_db['bids'].insert_one.side_effect = Exception + + response = client.post("api/bids", json=data) + assert response.status_code == 400 + assert response.get_json() == { + 'Error': "{'success': [\"Phase value already exists in 'failed' section and cannot be repeated.\"]}" + } + + +# Case 6: Success cannot have the same phase in the list +@patch('api.controllers.bid_controller.dbConnection') +def test_success_same_phase(mock_dbConnection, client): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -184,14 +191,11 @@ def test_success_same_phase(client): } # Mock the behavior of dbConnection - with patch('api.controllers.bid_controller.dbConnection') as mock_dbConnection: - # Create a MagicMock object to simulate the behavior of the insert_one method - mock_insert_one = MagicMock() - # Set the return value of db['bids'].insert_one to the MagicMock object - mock_dbConnection.return_value.__getitem__.return_value.insert_one = mock_insert_one - - response = client.post("api/bids", json=data) - assert response.status_code == 400 - # Check that the insert_one method was called with the correct argument - assert response.get_json() == { - 'Error': '{\'success\': ["Phase value already exists in \'success\' list and cannot be repeated."]}'} + mock_db = mock_dbConnection.return_value + mock_db['bids'].insert_one.side_effect = Exception + + response = client.post("api/bids", json=data) + assert response.status_code == 400 + assert response.get_json() == { + 'Error': "{'success': [\"Phase value already exists in 'success' list and cannot be repeated.\"]}" + } From b5add4545dc12fba01166694a6519b0e3ce082c7 Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 13 Jul 2023 12:40:30 +0100 Subject: [PATCH 55/97] fix: updated soft delete method from PUT to DELETE --- api/controllers/bid_controller.py | 2 +- static/swagger_config.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 86bb965..909d813 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -46,7 +46,7 @@ def get_bid_by_id(bid_id): except ConnectionFailure: return showConnectionError() -@bid.route("/bids/", methods=["PUT"]) +@bid.route("/bids/", methods=["DELETE"]) def change_status_to_deleted(bid_id): # Validates query param try: diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 08d8442..33f05dd 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -126,7 +126,7 @@ paths: '500': $ref: '#/components/responses/InternalServerError' # -------------------------------------------- - put: + delete: tags: - bids summary: Soft delete a bid From 1a7ba79cdbd28b26db354d5d5f192a57a5e9e7e8 Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 13 Jul 2023 15:36:07 +0100 Subject: [PATCH 56/97] feat: added swag spec and endpoint for update bid route; changed bid request schema and bid model to allow reusability --- api/controllers/bid_controller.py | 26 ++++++++++++++++++----- api/models/bid_model.py | 6 +++--- api/schemas/bid_request_schema.py | 1 + request_examples/update_bid.http | 29 ++++++++++++++++++++++++++ static/swagger_config.yml | 34 ++++++++++++++++++++++++++++--- tests/test_delete_bid.py | 6 +++--- 6 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 request_examples/update_bid.http diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 909d813..2f4c30d 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -26,7 +26,7 @@ def get_bids(): @bid.route("/bids/", methods=["GET"]) def get_bid_by_id(bid_id): - # Validates query param + # Validates path param try: valid_bid_id = valid_bid_id_schema().load({"bid_id": bid_id}) bid_id = valid_bid_id["bid_id"] @@ -38,7 +38,6 @@ def get_bid_by_id(bid_id): db = dbConnection() data = db['bids'].find_one({"_id": bid_id}) # Return 404 response if not found / returns None - if data is None: return showNotFoundError() return data, 200 @@ -46,9 +45,28 @@ def get_bid_by_id(bid_id): except ConnectionFailure: return showConnectionError() +@bid.route("/bids/", methods=["PUT"]) +def update_bid_by_id(bid_id): + try: + # Add id to request body from path param + # This allows request to be validated against same schema + updated_bid = request.get_json() + updated_bid["_id"] = bid_id + # Deserialize and validate request against schema + # Process input and create data model + bid_document = BidRequestSchema().load(updated_bid) + # Serialize to a JSON object + replacement = BidSchema().dump(bid_document) + # Find bid by id and replace with user request body + db = dbConnection() + db['bids'].find_one_and_replace({"_id": bid_id}, replacement) + return replacement, 200 + except ValidationError as e: + return jsonify({"Error": str(e)}), 400 + @bid.route("/bids/", methods=["DELETE"]) def change_status_to_deleted(bid_id): - # Validates query param + # Validates path param try: valid_bid_id = valid_bid_id_schema().load({"bid_id": bid_id}) bid_id = valid_bid_id["bid_id"] @@ -67,8 +85,6 @@ def change_status_to_deleted(bid_id): return showConnectionError() except ValidationError as e: return jsonify({"Error": str(e)}), 400 - - @bid.route("/bids", methods=["POST"]) def post_bid(): diff --git a/api/models/bid_model.py b/api/models/bid_model.py index 7d6c339..875b6ab 100644 --- a/api/models/bid_model.py +++ b/api/models/bid_model.py @@ -5,14 +5,14 @@ # Description: Schema for the bid object class BidModel(): - def __init__(self, tender, client, bid_date, alias=None, bid_folder_url=None, feedback=None, failed=None, was_successful=False, success=[]): - self._id = uuid4() + def __init__(self, tender, client, bid_date, alias=None, bid_folder_url=None, feedback=None, failed=None, was_successful=False, success=[], _id=uuid4(), status=Status.IN_PROGRESS): + self._id = _id self.tender = tender self.client = client self.alias = alias self.bid_date = bid_date self.bid_folder_url = bid_folder_url - self.status = Status.IN_PROGRESS # enum: "deleted", "in_progress" or "completed" + self.status = status # enum: "deleted", "in_progress" or "completed" self.links = LinksModel(self._id) self.was_successful = was_successful self.success = success diff --git a/api/schemas/bid_request_schema.py b/api/schemas/bid_request_schema.py index 46c359a..492154e 100644 --- a/api/schemas/bid_request_schema.py +++ b/api/schemas/bid_request_schema.py @@ -7,6 +7,7 @@ # Marshmallow schema for request body class BidRequestSchema(Schema): + _id = fields.UUID() tender = fields.Str(required=True, error_messages={"required": {"message": "Missing mandatory field"}}) client = fields.Str(required=True, error_messages={"required": {"message": "Missing mandatory field"}}) alias = fields.Str() diff --git a/request_examples/update_bid.http b/request_examples/update_bid.http new file mode 100644 index 0000000..a0bb2a1 --- /dev/null +++ b/request_examples/update_bid.http @@ -0,0 +1,29 @@ +PUT http://localhost:8080/api/bids/674e1944-7c6b-4e13-9e88-4abc3c1ff8a3 HTTP/1.1 +Content-Type: application/json + +{ + "success": [ + { + "phase": 1, + "has_score": true, + "out_of": 36, + "score": 30 + } + ], + "was_successful": false, + "failed": { + "phase": 2, + "has_score": true, + "out_of": 36, + "score": 20 + }, + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "alias": "ONS", + "tender": "Updated", + "bid_date": "21-06-2023", + "client": "Office for National Statistics", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder" +} diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 33f05dd..35c1389 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -125,6 +125,34 @@ paths: } '500': $ref: '#/components/responses/InternalServerError' + # -------------------------------------------- + put: + tags: + - bids + summary: Update an existing bid + description: Update an existing bid by id + operationId: update_bid + parameters: + - name: bid_id + in: path + description: ID of bid to update + required: true + schema: + type: string + format: uuid + requestBody: + $ref: '#/components/requestBodies/Bid' + required: true + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Bid' + '400': + description: Bad request (missing mandatory field/fields) + $ref: '#/components/responses/BadRequest' # -------------------------------------------- delete: tags: @@ -331,8 +359,8 @@ components: schema: $ref: '#/components/schemas/BidRequestBody' examples: - 200 Created: - summary: 200 Created + 200 OK: + summary: 200 OK value: tender: 'Business Intelligence and Data Warehousing' bid_folder_url: 'https://organisation.sharepoint.com/Docs/dummyfolder' @@ -377,7 +405,7 @@ components: schema: type: object example: { - "Error": "{'': ['']}" + "Error": "{'{field}': ['{message}']}" } InternalServerError: description: Internal Server Error diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py index a0e6da6..0d2c064 100644 --- a/tests/test_delete_bid.py +++ b/tests/test_delete_bid.py @@ -22,7 +22,7 @@ def test_delete_bid_success(client): mock_dbConnection.return_value = mock_db mock_db['bids'].update_one = MagicMock() mock_db['bids'].update_one.return_value = {'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'status': 'deleted'} - response = client.put('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + response = client.delete('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') assert response.status_code == 204 assert response.get_json() == None @@ -33,7 +33,7 @@ def test_delete_bid_find_error(client): mock_db = MagicMock() mock_dbConnection.return_value = mock_db mock_db['bids'].update_one = MagicMock(side_effect=ConnectionFailure) - response = client.put('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + response = client.delete('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} @@ -49,7 +49,7 @@ def test_delete_bid_by_id_validation_error(client): # Patch the necessary functions and objects with the MagicMock object with patch('api.controllers.bid_controller.valid_bid_id_schema', mock_valid_bid_id_schema): # Call the endpoint with the desired URL - response = client.get('/api/bids/invalid_bid_id') + response = client.delete('/api/bids/invalid_bid_id') # Assertions mock_valid_bid_id_schema().load.assert_called_once_with({'bid_id': 'invalid_bid_id'}) From c841aeb43cc48ef37a10ed96f546971b14729bce Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 13 Jul 2023 16:00:02 +0100 Subject: [PATCH 57/97] refactor: updated examples in swagger spec --- request_examples/update_bid.http | 2 +- static/swagger_config.yml | 39 +++++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/request_examples/update_bid.http b/request_examples/update_bid.http index a0bb2a1..158b9b2 100644 --- a/request_examples/update_bid.http +++ b/request_examples/update_bid.http @@ -21,7 +21,7 @@ Content-Type: application/json "description": "Feedback from client in detail", "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" }, - "alias": "ONS", + "alias": "TEST", "tender": "Updated", "bid_date": "21-06-2023", "client": "Office for National Statistics", diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 35c1389..fe8902e 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -77,7 +77,7 @@ paths: description: Create a new bid operationId: post_bid requestBody: - $ref: '#/components/requestBodies/Bid' + $ref: '#/components/requestBodies/PostBid' required: true responses: '201': @@ -141,7 +141,7 @@ paths: type: string format: uuid requestBody: - $ref: '#/components/requestBodies/Bid' + $ref: '#/components/requestBodies/UpdateBid' required: true responses: '200': @@ -352,7 +352,7 @@ components: # -------------------------------------------- # Request bodies requestBodies: - Bid: + PostBid: description: Bid object to be added to collection content: application/json: @@ -395,6 +395,39 @@ components: client: 'Office for National Statistics' alias: 'ONS' bid_date: '21-06-2023' + UpdateBid: + description: Bid object to replace bid by Id + content: + application/json: + schema: + $ref: '#/components/schemas/BidRequestBody' + examples: + 200 OK: + summary: 200 OK + value: + tender: 'THIS HAS BEEN UPDATED' + bid_folder_url: 'https://organisation.sharepoint.com/Docs/dummyfolder' + feedback: + url: 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback' + description: 'Feedback from client in detail' + client: 'Office for National Statistics' + alias: 'ONS' + bid_date: '21-06-2023' + success: [ + { + "phase": 1, + "has_score": true, + "score": 28, + "out_of": 36 + } + ] + failed: { + "phase": 2, + "has_score": true, + "score": 20, + "out_of": 36 + } + was_successful: false # -------------------------------------------- # Error responses responses: From b3b574b34971fbc494b5813d58ed5899245b2921 Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 17 Jul 2023 17:17:48 +0100 Subject: [PATCH 58/97] test: update bid by id tests; refactored to add helper functions for input validation --- TODO.md | 2 - api/controllers/bid_controller.py | 23 +++------ db.txt | 0 helpers/helpers.py | 17 ++++++- request_examples/delete.http | 2 +- request_examples/get_all.http | 2 +- request_examples/invalid_string.http | 25 ++-------- request_examples/update_bid.http | 28 ++--------- tests/conftest.py | 10 ++++ tests/test_delete_bid.py | 14 +----- tests/test_get_bid_by_id.py | 13 +---- tests/test_get_bids.py | 13 +---- tests/test_post_bid.py | 13 ----- tests/test_update_bid_by_id.py | 72 ++++++++++++++++++++++++++++ 14 files changed, 120 insertions(+), 114 deletions(-) delete mode 100644 TODO.md delete mode 100644 db.txt create mode 100644 tests/conftest.py create mode 100644 tests/test_update_bid_by_id.py diff --git a/TODO.md b/TODO.md deleted file mode 100644 index a0f7a1b..0000000 --- a/TODO.md +++ /dev/null @@ -1,2 +0,0 @@ -- SWAGGER: how to do multiple examples of 400 response -- QUESTION: error responses as text/plain or application/json? \ No newline at end of file diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index c69f160..decd0a5 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -6,9 +6,7 @@ from api.models.status_enum import Status from dbconfig.mongo_setup import dbConnection from pymongo.errors import ConnectionFailure -from helpers.helpers import showConnectionError, showNotFoundError - - +from helpers.helpers import showConnectionError, showNotFoundError, validate_and_create_data, validate_bid_id_path bid = Blueprint('bid', __name__) @@ -28,8 +26,7 @@ def get_bids(): def get_bid_by_id(bid_id): # Validates path param try: - valid_bid_id = valid_bid_id_schema().load({"bid_id": bid_id}) - bid_id = valid_bid_id["bid_id"] + bid_id = validate_bid_id_path(bid_id) except ValidationError as e: return jsonify({"Error": str(e)}), 400 @@ -52,15 +49,12 @@ def update_bid_by_id(bid_id): # This allows request to be validated against same schema updated_bid = request.get_json() updated_bid["_id"] = bid_id - # Deserialize and validate request against schema # Process input and create data model - bid_document = BidRequestSchema().load(updated_bid) - # Serialize to a JSON object - replacement = BidSchema().dump(bid_document) + data = validate_and_create_data(updated_bid) # Find bid by id and replace with user request body db = dbConnection() - db['bids'].find_one_and_replace({"_id": bid_id}, replacement) - return replacement, 200 + db['bids'].find_one_and_replace({"_id": bid_id}, data) + return data, 200 except ValidationError as e: return jsonify({"Error": str(e)}), 400 @@ -68,8 +62,7 @@ def update_bid_by_id(bid_id): def change_status_to_deleted(bid_id): # Validates path param try: - valid_bid_id = valid_bid_id_schema().load({"bid_id": bid_id}) - bid_id = valid_bid_id["bid_id"] + bid_id = validate_bid_id_path(bid_id) except ValidationError as e: return jsonify({"Error": str(e)}), 400 @@ -92,9 +85,7 @@ def post_bid(): try: db = dbConnection() # Process input and create data model - bid_document = BidRequestSchema().load(request.get_json()) - # Serialize to a JSON object - data = BidSchema().dump(bid_document) + data = validate_and_create_data(request.get_json()) # Insert document into database collection db['bids'].insert_one(data) return data, 201 diff --git a/db.txt b/db.txt deleted file mode 100644 index e69de29..0000000 diff --git a/helpers/helpers.py b/helpers/helpers.py index e9a89c6..14ba168 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -1,6 +1,9 @@ from flask import jsonify import uuid from datetime import datetime +from api.schemas.bid_schema import BidSchema +from api.schemas.bid_request_schema import BidRequestSchema +from api.schemas.valid_bid_id_schema import valid_bid_id_schema def showConnectionError(): return jsonify({"Error": "Could not connect to database"}), 500 @@ -20,4 +23,16 @@ def is_valid_isoformat(string): datetime.fromisoformat(string) return True except: - return False \ No newline at end of file + return False + +def validate_and_create_data(request): + # Process input and create data model + bid_document = BidRequestSchema().load(request) + # Serialize to a JSON object + data = BidSchema().dump(bid_document) + return data + +def validate_bid_id_path(bid_id): + valid_bid_id = valid_bid_id_schema().load({"bid_id": bid_id}) + validated_id = valid_bid_id["bid_id"] + return validated_id \ No newline at end of file diff --git a/request_examples/delete.http b/request_examples/delete.http index 06aee60..8814193 100644 --- a/request_examples/delete.http +++ b/request_examples/delete.http @@ -1 +1 @@ -GET http://localhost:8080/api/bids/3706739b-a88c-4c6c-8b4f-869038d09e4a HTTP/1.1 +DELETE http://localhost:8080/api/bids/27fc4afb-fcdc-4bf7-9f8f-74f524831da4 HTTP/1.1 diff --git a/request_examples/get_all.http b/request_examples/get_all.http index 712676d..9f87ae8 100644 --- a/request_examples/get_all.http +++ b/request_examples/get_all.http @@ -1 +1 @@ -GET http://localhost:8080/api/bids HTTP/1.1 +GET http://localhost:8080/api/bids/27fc4afb-fcdc-4bf7-9f8f-74f524831da4 HTTP/1.1 diff --git a/request_examples/invalid_string.http b/request_examples/invalid_string.http index 698f822..12d71af 100644 --- a/request_examples/invalid_string.http +++ b/request_examples/invalid_string.http @@ -3,26 +3,9 @@ Content-Type: application/json { "tender": 42, - "client": "Office for National Statistics", - "bid_date": "21-06-2023", "alias": "ONS", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "feedback": { - "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" - }, - "success": [ - { - "phase": 1, - "has_score": true, - "score": 28, - "out_of": 36 - } - ], - "failed": { - "phase": 2, - "has_score": true, - "score": 20, - "out_of": 36 - } + "bid_date": "2023-12-25", + "bid_folder_url": "Not a valid URL", + "client": 7, + "was_successful": "String" } \ No newline at end of file diff --git a/request_examples/update_bid.http b/request_examples/update_bid.http index 158b9b2..a51ee0b 100644 --- a/request_examples/update_bid.http +++ b/request_examples/update_bid.http @@ -1,29 +1,11 @@ -PUT http://localhost:8080/api/bids/674e1944-7c6b-4e13-9e88-4abc3c1ff8a3 HTTP/1.1 +PUT http://localhost:8080/api/bids/a042cccd-ac58-47d8-820c-9704f7011969 HTTP/1.1 Content-Type: application/json { - "success": [ - { - "phase": 1, - "has_score": true, - "out_of": 36, - "score": 30 - } - ], - "was_successful": false, - "failed": { - "phase": 2, - "has_score": true, - "out_of": 36, - "score": 20 - }, - "feedback": { - "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" - }, - "alias": "TEST", - "tender": "Updated", + "tender": "UPDATED TENDER", + "alias": "ONS", "bid_date": "21-06-2023", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "client": "Office for National Statistics", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder" + "was_successful": false } diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..3c706e5 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,10 @@ +import pytest +from flask import Flask +from api.controllers.bid_controller import bid + +@pytest.fixture +def client(): + app = Flask(__name__) + app.register_blueprint(bid, url_prefix='/api') + with app.test_client() as client: + yield client \ No newline at end of file diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py index e3b52bd..dd0cea9 100644 --- a/tests/test_delete_bid.py +++ b/tests/test_delete_bid.py @@ -1,17 +1,7 @@ -from flask import Flask -import pytest -from api.controllers.bid_controller import bid from pymongo.errors import ConnectionFailure from unittest.mock import patch from marshmallow import ValidationError -@pytest.fixture -def client(): - app = Flask(__name__) - app.register_blueprint(bid, url_prefix='/api') - with app.test_client() as client: - yield client - # Case 1: Successful delete a bid by changing status to deleted @patch('api.controllers.bid_controller.dbConnection') def test_delete_bid_success(mock_dbConnection, client): @@ -20,7 +10,7 @@ def test_delete_bid_success(mock_dbConnection, client): '_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'status': 'deleted' } - response = client.put('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + response = client.delete('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') assert response.status_code == 204 assert response.get_json() is None @@ -29,7 +19,7 @@ def test_delete_bid_success(mock_dbConnection, client): def test_delete_bid_find_error(mock_dbConnection, client): mock_db = mock_dbConnection.return_value mock_db['bids'].update_one.side_effect = ConnectionFailure - response = client.put('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + response = client.delete('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index 1cb4f1b..b12bb33 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -1,18 +1,7 @@ -from flask import Flask -import pytest -from api.controllers.bid_controller import bid -from pymongo.errors import ConnectionFailure from unittest.mock import patch +from pymongo.errors import ConnectionFailure from marshmallow import ValidationError - -@pytest.fixture -def client(): - app = Flask(__name__) - app.register_blueprint(bid, url_prefix='/api') - with app.test_client() as client: - yield client - # Case 1: Successful get_bid_by_id @patch('api.controllers.bid_controller.dbConnection') def test_get_bid_by_id_success(mock_dbConnection, client): diff --git a/tests/test_get_bids.py b/tests/test_get_bids.py index 7f7d2de..923c344 100644 --- a/tests/test_get_bids.py +++ b/tests/test_get_bids.py @@ -1,16 +1,5 @@ -from flask import Flask -import pytest -from api.controllers.bid_controller import bid -from pymongo.errors import ConnectionFailure from unittest.mock import patch - - -@pytest.fixture -def client(): - app = Flask(__name__) - app.register_blueprint(bid, url_prefix='/api') - with app.test_client() as client: - yield client +from pymongo.errors import ConnectionFailure # Case 1: Successful get @patch('api.controllers.bid_controller.dbConnection') diff --git a/tests/test_post_bid.py b/tests/test_post_bid.py index 3293b3a..be5aa6d 100644 --- a/tests/test_post_bid.py +++ b/tests/test_post_bid.py @@ -1,19 +1,6 @@ -import pytest -from flask import Flask from unittest.mock import patch from pymongo.errors import ConnectionFailure -from api.controllers.bid_controller import bid - - -@pytest.fixture -def client(): - app = Flask(__name__) - app.register_blueprint(bid, url_prefix='/api') - with app.test_client() as client: - yield client - - # Case 1: Successful post @patch('api.controllers.bid_controller.dbConnection') def test_post_is_successful(mock_dbConnection, client): diff --git a/tests/test_update_bid_by_id.py b/tests/test_update_bid_by_id.py new file mode 100644 index 0000000..53e51d9 --- /dev/null +++ b/tests/test_update_bid_by_id.py @@ -0,0 +1,72 @@ +import ast +from unittest.mock import patch + +# Case 1: Successful update +@patch('api.controllers.bid_controller.dbConnection') +def test_update_bid_by_id_success(mock_dbConnection, client): + mock_db = mock_dbConnection.return_value + mock_db['bids'].find_one_and_replace.return_value = { + "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", + "tender": "Business Intelligence and Data Warehousing", + "alias": "ONS", + "bid_date": "2023-06-23", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "Office for National Statistics", + "was_successful": False + } + + bid_id = '9f688442-b535-4683-ae1a-a64c1a3b8616' + updated_bid = { + "tender": "UPDATED TENDER", + "alias": "ONS", + "bid_date": "21-06-2023", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "Office for National Statistics", + "was_successful": False + } + response = client.put(f"api/bids/{bid_id}", json=updated_bid) + + assert response.status_code == 200 + assert response.get_json()["tender"] == "UPDATED TENDER" + assert "last_updated" in response.get_json() and response.get_json()["last_updated"] is not None + assert response.get_json()["links"] is not None + assert "self" in response.get_json()["links"] + assert response.get_json()["links"]["self"] == f"/bids/{bid_id}" + assert 'questions' in response.get_json()["links"] + assert response.get_json()["links"]["questions"] == f"/bids/{bid_id}/questions" + +# Case 2: Invalid user input +@patch('api.controllers.bid_controller.dbConnection') +def test_input_validation(mock_dbConnection, client): + mock_db = mock_dbConnection.return_value + mock_db['bids'].find_one_and_replace.return_value = { + "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", + "tender": "Business Intelligence and Data Warehousing", + "alias": "ONS", + "bid_date": "2023-06-23", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "Office for National Statistics", + "was_successful": False + } + + bid_id = '9f688442-b535-4683-ae1a-a64c1a3b8616' + updated_bid = { + "tender": 42, + "alias": "ONS", + "bid_date": "2023-12-25", + "bid_folder_url": "Not a valid URL", + "client": 7, + "was_successful": "String" + } + response = client.put(f"api/bids/{bid_id}", json=updated_bid) + error_message = ast.literal_eval(response.get_json()["Error"]) + expected_errors = { + 'bid_folder_url': ['Not a valid URL.'], + 'client': ['Not a valid string.'], + 'tender': ['Not a valid string.'], + 'was_successful': ['Not a valid boolean.'], + 'bid_date': ['Not a valid date.'] + } + + assert response.status_code == 400 + assert expected_errors.items() <= error_message.items() \ No newline at end of file From 6f2f6a2b22c5f04f598908077af80fcae1bc80ee Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 17 Jul 2023 17:19:12 +0100 Subject: [PATCH 59/97] refactor: removed unused imports --- api/controllers/bid_controller.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index decd0a5..901a0fa 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -1,8 +1,5 @@ from flask import Blueprint, jsonify, request from marshmallow import ValidationError -from api.schemas.bid_schema import BidSchema -from api.schemas.bid_request_schema import BidRequestSchema -from api.schemas.valid_bid_id_schema import valid_bid_id_schema from api.models.status_enum import Status from dbconfig.mongo_setup import dbConnection from pymongo.errors import ConnectionFailure From f9ae9e99b1b558af915e6146a6a2ff53b5e065df Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 18 Jul 2023 09:38:45 +0100 Subject: [PATCH 60/97] refactor: cleaned up controller --- api/controllers/bid_controller.py | 15 +++------------ api/schemas/valid_bid_id_schema.py | 2 +- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 901a0fa..052b514 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -21,14 +21,8 @@ def get_bids(): @bid.route("/bids/", methods=["GET"]) def get_bid_by_id(bid_id): - # Validates path param try: bid_id = validate_bid_id_path(bid_id) - except ValidationError as e: - return jsonify({"Error": str(e)}), 400 - - # Get bid by id from database collection - try: db = dbConnection() data = db['bids'].find_one({"_id": bid_id , "status": {"$ne": Status.DELETED.value}}) # Return 404 response if not found / returns None @@ -38,6 +32,9 @@ def get_bid_by_id(bid_id): # Return 500 response in case of connection failure except ConnectionFailure: return showConnectionError() + # Return 400 if bid_id is invalid + except ValidationError as e: + return jsonify({"Error": str(e)}), 400 @bid.route("/bids/", methods=["PUT"]) def update_bid_by_id(bid_id): @@ -57,13 +54,8 @@ def update_bid_by_id(bid_id): @bid.route("/bids/", methods=["DELETE"]) def change_status_to_deleted(bid_id): - # Validates path param try: bid_id = validate_bid_id_path(bid_id) - except ValidationError as e: - return jsonify({"Error": str(e)}), 400 - - try: db = dbConnection() data = db['bids'].find_one({"_id": bid_id , "status": {"$ne": Status.DELETED.value}}) if data is None: @@ -78,7 +70,6 @@ def change_status_to_deleted(bid_id): @bid.route("/bids", methods=["POST"]) def post_bid(): - # Create bid document and inserts it into collection try: db = dbConnection() # Process input and create data model diff --git a/api/schemas/valid_bid_id_schema.py b/api/schemas/valid_bid_id_schema.py index c6b8fee..77f178a 100644 --- a/api/schemas/valid_bid_id_schema.py +++ b/api/schemas/valid_bid_id_schema.py @@ -1,4 +1,4 @@ from marshmallow import Schema, fields, validate class valid_bid_id_schema(Schema): - bid_id = fields.Str(required=True, validate=validate.Length(min=1)) \ No newline at end of file + bid_id = fields.Str(required=True, validate=validate.Length(min=36)) \ No newline at end of file From 1238a99a6febfb887485d6a604ad86b36cfa1aed Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 18 Jul 2023 10:26:42 +0100 Subject: [PATCH 61/97] refactor: changed find and replace to update one; wip: adding last_updated timestamp in correct format --- api/controllers/bid_controller.py | 11 +++++++---- request_examples/update_bid.http | 9 ++------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 052b514..553d202 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -1,8 +1,10 @@ from flask import Blueprint, jsonify, request +from datetime import datetime from marshmallow import ValidationError from api.models.status_enum import Status from dbconfig.mongo_setup import dbConnection from pymongo.errors import ConnectionFailure +from api.schemas.bid_schema import BidSchema from helpers.helpers import showConnectionError, showNotFoundError, validate_and_create_data, validate_bid_id_path bid = Blueprint('bid', __name__) @@ -41,13 +43,14 @@ def update_bid_by_id(bid_id): try: # Add id to request body from path param # This allows request to be validated against same schema - updated_bid = request.get_json() - updated_bid["_id"] = bid_id + user_request = request.get_json() + user_request["last_updated"] = datetime.now() # Process input and create data model - data = validate_and_create_data(updated_bid) + # data = validate_and_create_data(updated_bid) + data = BidSchema().load(user_request, partial=True) # Find bid by id and replace with user request body db = dbConnection() - db['bids'].find_one_and_replace({"_id": bid_id}, data) + db['bids'].update_one({"_id": bid_id}, {"$set": data}) return data, 200 except ValidationError as e: return jsonify({"Error": str(e)}), 400 diff --git a/request_examples/update_bid.http b/request_examples/update_bid.http index a51ee0b..062eb4c 100644 --- a/request_examples/update_bid.http +++ b/request_examples/update_bid.http @@ -1,11 +1,6 @@ -PUT http://localhost:8080/api/bids/a042cccd-ac58-47d8-820c-9704f7011969 HTTP/1.1 +PUT http://localhost:8080/api/bids/1fb19151-2360-4ad0-86ed-f58ad2cd3b85 HTTP/1.1 Content-Type: application/json { - "tender": "UPDATED TENDER", - "alias": "ONS", - "bid_date": "21-06-2023", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "client": "Office for National Statistics", - "was_successful": false + "tender": "UPDATED yeah" } From 1c986c21f2e203b3809ccbb5aab30c3a53e9200b Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 18 Jul 2023 12:26:25 +0100 Subject: [PATCH 62/97] test: updating tests after refactor --- api/controllers/bid_controller.py | 63 ++++++++++++++++--------------- api/models/bid_model.py | 4 +- api/schemas/bid_request_schema.py | 3 -- api/schemas/bid_schema.py | 10 ++++- request_examples/update_bid.http | 2 +- tests/test_delete_bid.py | 21 +++++------ 6 files changed, 54 insertions(+), 49 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 553d202..ae1a623 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -21,6 +21,22 @@ def get_bids(): except Exception: return jsonify({"Error": "Could not retrieve bids"}), 500 +@bid.route("/bids", methods=["POST"]) +def post_bid(): + try: + db = dbConnection() + # Process input and create data model + data = validate_and_create_data(request.get_json()) + # Insert document into database collection + db['bids'].insert_one(data) + return data, 201 + # Return 400 response if input validation fails + except ValidationError as e: + return jsonify({"Error": str(e)}), 400 + # Return 500 response in case of connection failure + except ConnectionFailure: + return showConnectionError() + @bid.route("/bids/", methods=["GET"]) def get_bid_by_id(bid_id): try: @@ -31,58 +47,45 @@ def get_bid_by_id(bid_id): if data is None: return showNotFoundError() return data, 200 - # Return 500 response in case of connection failure - except ConnectionFailure: - return showConnectionError() # Return 400 if bid_id is invalid except ValidationError as e: return jsonify({"Error": str(e)}), 400 + # Return 500 response in case of connection failure + except ConnectionFailure: + return showConnectionError() @bid.route("/bids/", methods=["PUT"]) def update_bid_by_id(bid_id): try: - # Add id to request body from path param - # This allows request to be validated against same schema - user_request = request.get_json() - user_request["last_updated"] = datetime.now() - # Process input and create data model - # data = validate_and_create_data(updated_bid) - data = BidSchema().load(user_request, partial=True) - # Find bid by id and replace with user request body + bid_id = validate_bid_id_path(bid_id) + # Validate user input + user_request = BidSchema().load(request.get_json(), partial=True) + # Updates document where id is equal to bid_id db = dbConnection() - db['bids'].update_one({"_id": bid_id}, {"$set": data}) + data = db['bids'].find_one_and_update({"_id": bid_id}, {"$set": user_request}, return_document=True) + # Return 404 response if not found / returns None + if data is None: + return showNotFoundError() return data, 200 + # Return 400 response if input validation fails except ValidationError as e: return jsonify({"Error": str(e)}), 400 + # Return 500 response in case of connection failure + except ConnectionFailure: + return showConnectionError() @bid.route("/bids/", methods=["DELETE"]) def change_status_to_deleted(bid_id): try: bid_id = validate_bid_id_path(bid_id) db = dbConnection() - data = db['bids'].find_one({"_id": bid_id , "status": {"$ne": Status.DELETED.value}}) + data = db['bids'].find_one_and_update({"_id": bid_id, "status": {"$ne": Status.DELETED.value}}, {"$set": {"status": Status.DELETED.value, "last_updated": datetime.now().isoformat()}}) if data is None: return showNotFoundError() - else: - db['bids'].update_one({"_id": bid_id}, {"$set": {"status": Status.DELETED.value}}) return data, 204 - except ConnectionFailure: - return showConnectionError() - except ValidationError as e: - return jsonify({"Error": str(e)}), 400 - -@bid.route("/bids", methods=["POST"]) -def post_bid(): - try: - db = dbConnection() - # Process input and create data model - data = validate_and_create_data(request.get_json()) - # Insert document into database collection - db['bids'].insert_one(data) - return data, 201 # Return 400 response if input validation fails except ValidationError as e: return jsonify({"Error": str(e)}), 400 # Return 500 response in case of connection failure except ConnectionFailure: - return showConnectionError() \ No newline at end of file + return showConnectionError() diff --git a/api/models/bid_model.py b/api/models/bid_model.py index 75407d1..bab98b1 100644 --- a/api/models/bid_model.py +++ b/api/models/bid_model.py @@ -5,8 +5,8 @@ # Description: Schema for the bid object class BidModel(): - def __init__(self, tender, client, bid_date, alias=None, bid_folder_url=None, feedback=None, failed=None, was_successful=False, success=[], _id=uuid4(), status=Status.IN_PROGRESS): - self._id = _id + def __init__(self, tender, client, bid_date, alias=None, bid_folder_url=None, feedback=None, failed=None, was_successful=False, success=[],status=Status.IN_PROGRESS): + self._id = uuid4() self.tender = tender self.client = client self.alias = alias diff --git a/api/schemas/bid_request_schema.py b/api/schemas/bid_request_schema.py index 492154e..36bae3b 100644 --- a/api/schemas/bid_request_schema.py +++ b/api/schemas/bid_request_schema.py @@ -7,7 +7,6 @@ # Marshmallow schema for request body class BidRequestSchema(Schema): - _id = fields.UUID() tender = fields.Str(required=True, error_messages={"required": {"message": "Missing mandatory field"}}) client = fields.Str(required=True, error_messages={"required": {"message": "Missing mandatory field"}}) alias = fields.Str() @@ -48,8 +47,6 @@ def validate_success_and_failed(self, value): raise ValidationError("Phase value already exists in 'failed' section and cannot be repeated.") success_phase_values.add(phase_value) - - # Creates a Bid instance after processing @post_load def makeBid(self, data, **kwargs): diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py index ae393fb..139a3b4 100644 --- a/api/schemas/bid_schema.py +++ b/api/schemas/bid_schema.py @@ -1,4 +1,5 @@ -from marshmallow import Schema, fields +from marshmallow import Schema, fields, post_load +from datetime import datetime from .links_schema import LinksSchema from .phase_schema import PhaseSchema from .feedback_schema import FeedbackSchema @@ -18,4 +19,9 @@ class BidSchema(Schema): success = fields.List(fields.Nested(PhaseSchema)) failed = fields.Nested(PhaseSchema) feedback = fields.Nested(FeedbackSchema) - last_updated = fields.DateTime(required=True) \ No newline at end of file + last_updated = fields.DateTime(required=True) + + @post_load + def set_last_updated(self, data, **kwargs): + data["last_updated"] = datetime.now().isoformat() + return data \ No newline at end of file diff --git a/request_examples/update_bid.http b/request_examples/update_bid.http index 062eb4c..0ca9eb3 100644 --- a/request_examples/update_bid.http +++ b/request_examples/update_bid.http @@ -1,4 +1,4 @@ -PUT http://localhost:8080/api/bids/1fb19151-2360-4ad0-86ed-f58ad2cd3b85 HTTP/1.1 +PUT http://localhost:8080/api/bids/1fb19151-2360-4ad0-86ed-f58ad2cd3b86 HTTP/1.1 Content-Type: application/json { diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py index dd0cea9..8f807fe 100644 --- a/tests/test_delete_bid.py +++ b/tests/test_delete_bid.py @@ -6,28 +6,27 @@ @patch('api.controllers.bid_controller.dbConnection') def test_delete_bid_success(mock_dbConnection, client): mock_db = mock_dbConnection.return_value - mock_db['bids'].update_one.return_value = { - '_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', - 'status': 'deleted' + mock_db['bids'].find_one_and_update.return_value = { + "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", + "status": "deleted" } response = client.delete('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') assert response.status_code == 204 - assert response.get_json() is None + assert response.body is None # Case 2: Failed to call database @patch('api.controllers.bid_controller.dbConnection') def test_delete_bid_find_error(mock_dbConnection, client): mock_db = mock_dbConnection.return_value - mock_db['bids'].update_one.side_effect = ConnectionFailure + mock_db['bids'].find_one_and_update.side_effect = ConnectionFailure response = client.delete('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} # Case 3: Validation error -@patch('api.controllers.bid_controller.valid_bid_id_schema.load') -def test_get_bid_by_id_validation_error(mock_valid_bid_id_schema_load, client): - mock_valid_bid_id_schema_load.side_effect = ValidationError('Invalid Bid ID') - response = client.get('/api/bids/invalid_bid_id') - mock_valid_bid_id_schema_load.assert_called_once_with({'bid_id': 'invalid_bid_id'}) +@patch('api.controllers.bid_controller.dbConnection') +def test_get_bid_by_id_validation_error(mock_dbConnection, client): + mock_dbConnection.side_effect = ValidationError('Invalid Bid ID') + response = client.delete('/api/bids/invalid_bid_id') assert response.status_code == 400 - assert response.get_json() == {"Error": "Invalid Bid ID"} \ No newline at end of file + assert response.get_json() == {'Error': "{'bid_id': ['Shorter than minimum length 36.']}"} \ No newline at end of file From dcd3f6aed9a9eaeebcb3a06f51b1c127dbdcde8f Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 18 Jul 2023 15:37:32 +0100 Subject: [PATCH 63/97] test: updated tests after refactoring --- tests/test_delete_bid.py | 4 +-- tests/test_get_bid_by_id.py | 21 +++++---------- tests/test_update_bid_by_id.py | 49 +++++----------------------------- 3 files changed, 14 insertions(+), 60 deletions(-) diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py index 8f807fe..c570254 100644 --- a/tests/test_delete_bid.py +++ b/tests/test_delete_bid.py @@ -12,7 +12,7 @@ def test_delete_bid_success(mock_dbConnection, client): } response = client.delete('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') assert response.status_code == 204 - assert response.body is None + assert response.content_length is None # Case 2: Failed to call database @patch('api.controllers.bid_controller.dbConnection') @@ -26,7 +26,7 @@ def test_delete_bid_find_error(mock_dbConnection, client): # Case 3: Validation error @patch('api.controllers.bid_controller.dbConnection') def test_get_bid_by_id_validation_error(mock_dbConnection, client): - mock_dbConnection.side_effect = ValidationError('Invalid Bid ID') + mock_dbConnection.side_effect = ValidationError response = client.delete('/api/bids/invalid_bid_id') assert response.status_code == 400 assert response.get_json() == {'Error': "{'bid_id': ['Shorter than minimum length 36.']}"} \ No newline at end of file diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index b12bb33..4a3b84a 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -11,12 +11,8 @@ def test_get_bid_by_id_success(mock_dbConnection, client): 'tender': 'Business Intelligence and Data Warehousing' } - with patch('api.controllers.bid_controller.valid_bid_id_schema.load') as mock_valid_bid_id_schema_load: - mock_valid_bid_id_schema_load.return_value = {'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'} - - response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') - mock_valid_bid_id_schema_load.assert_called_once_with({'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) mock_dbConnection.assert_called_once() mock_db['bids'].find_one.assert_called_once_with({'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'status': {'$ne': 'deleted'}}) assert response.status_code == 200 @@ -38,22 +34,17 @@ def test_get_bid_by_id_not_found(mock_dbConnection, client): mock_db = mock_dbConnection.return_value mock_db['bids'].find_one.return_value = None - with patch('api.controllers.bid_controller.valid_bid_id_schema.load') as mock_valid_bid_id_schema_load: - mock_valid_bid_id_schema_load.return_value = {'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'} - - response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') - mock_valid_bid_id_schema_load.assert_called_once_with({'bid_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9'}) mock_dbConnection.assert_called_once() mock_db['bids'].find_one.assert_called_once_with({'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'status': {'$ne': 'deleted'}}) assert response.status_code == 404 assert response.get_json() == {"Error": "Not found"} # Case 4: Validation error -@patch('api.controllers.bid_controller.valid_bid_id_schema.load') -def test_get_bid_by_id_validation_error(mock_valid_bid_id_schema_load, client): - mock_valid_bid_id_schema_load.side_effect = ValidationError('Invalid Bid ID') +@patch('api.controllers.bid_controller.dbConnection') +def test_get_bid_by_id_validation_error(mock_dbConnection, client): + mock_dbConnection.side_effect = ValidationError response = client.get('/api/bids/invalid_bid_id') - mock_valid_bid_id_schema_load.assert_called_once_with({'bid_id': 'invalid_bid_id'}) assert response.status_code == 400 - assert response.get_json() == {"Error": "Invalid Bid ID"} + assert response.get_json() == {'Error': "{'bid_id': ['Shorter than minimum length 36.']}"} diff --git a/tests/test_update_bid_by_id.py b/tests/test_update_bid_by_id.py index 53e51d9..4fbb1cc 100644 --- a/tests/test_update_bid_by_id.py +++ b/tests/test_update_bid_by_id.py @@ -1,11 +1,10 @@ -import ast from unittest.mock import patch # Case 1: Successful update @patch('api.controllers.bid_controller.dbConnection') def test_update_bid_by_id_success(mock_dbConnection, client): mock_db = mock_dbConnection.return_value - mock_db['bids'].find_one_and_replace.return_value = { + mock_db['bids'].find_one_and_update.return_value = { "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", "tender": "Business Intelligence and Data Warehousing", "alias": "ONS", @@ -17,56 +16,20 @@ def test_update_bid_by_id_success(mock_dbConnection, client): bid_id = '9f688442-b535-4683-ae1a-a64c1a3b8616' updated_bid = { - "tender": "UPDATED TENDER", - "alias": "ONS", - "bid_date": "21-06-2023", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "client": "Office for National Statistics", - "was_successful": False + "tender": "UPDATED TENDER" } response = client.put(f"api/bids/{bid_id}", json=updated_bid) - + mock_dbConnection.assert_called_once() + mock_db['bids'].find_one_and_update.assert_called_once() assert response.status_code == 200 - assert response.get_json()["tender"] == "UPDATED TENDER" - assert "last_updated" in response.get_json() and response.get_json()["last_updated"] is not None - assert response.get_json()["links"] is not None - assert "self" in response.get_json()["links"] - assert response.get_json()["links"]["self"] == f"/bids/{bid_id}" - assert 'questions' in response.get_json()["links"] - assert response.get_json()["links"]["questions"] == f"/bids/{bid_id}/questions" # Case 2: Invalid user input @patch('api.controllers.bid_controller.dbConnection') def test_input_validation(mock_dbConnection, client): - mock_db = mock_dbConnection.return_value - mock_db['bids'].find_one_and_replace.return_value = { - "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", - "tender": "Business Intelligence and Data Warehousing", - "alias": "ONS", - "bid_date": "2023-06-23", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "client": "Office for National Statistics", - "was_successful": False - } - bid_id = '9f688442-b535-4683-ae1a-a64c1a3b8616' updated_bid = { - "tender": 42, - "alias": "ONS", - "bid_date": "2023-12-25", - "bid_folder_url": "Not a valid URL", - "client": 7, - "was_successful": "String" + "tender": 42 } response = client.put(f"api/bids/{bid_id}", json=updated_bid) - error_message = ast.literal_eval(response.get_json()["Error"]) - expected_errors = { - 'bid_folder_url': ['Not a valid URL.'], - 'client': ['Not a valid string.'], - 'tender': ['Not a valid string.'], - 'was_successful': ['Not a valid boolean.'], - 'bid_date': ['Not a valid date.'] - } - assert response.status_code == 400 - assert expected_errors.items() <= error_message.items() \ No newline at end of file + assert response.get_json()["Error"] == "{'tender': ['Not a valid string.']}" \ No newline at end of file From a9f169c165a9565097b736870d17f90df88892a0 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Tue, 18 Jul 2023 17:33:58 +0100 Subject: [PATCH 64/97] feat: Makefile --- Makefile | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..619be32 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +.ONESHELL: + +.DEFAULT_GOAL := run + +PYTHON = ./.venv/bin/python3 +PIP = ./.venv/bin/pip + +venv/bin/activate: requirements.txt + python3 -m venv .venv + $(PIP) install -r requirements.txt + +venv: venv/bin/activate + . ./.venv/bin/activate + +run: venv + $(PYTHON) app.py + +test: venv + $(PYTHON) -m pytest + +clean: + rm -rf __pycache__ + rm -rf .venv + rm -rf .pytest_cache From f573dcb6118c7737c21f5987556d21bda3c45f50 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Tue, 18 Jul 2023 17:47:37 +0100 Subject: [PATCH 65/97] refactor: README and req.txt updated with Makefile --- README.md | 19 +++++++------------ requirements.txt | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 13af8ab..f5bc077 100644 --- a/README.md +++ b/README.md @@ -26,25 +26,20 @@ This API provides an endpoint to post a new bid document. ```bash python3 --version ``` -4. Create the virtual environment by running the following command: - - ``` - python3 -m venv .venv - ``` -5. Run the virtual environment by running the following command: +4. Install Makefile if not already installed. You can check if it is installed by running the following command: ```bash - source .venv/bin/activate - ``` -6. Install the required dependencies by running the following command: + make --version + ``` +5. Version 3.81 or higher is required. If you do not have Make installed, you can install it with Homebrew: ```bash - pip install -r requirements.txt + brew install make ``` -7. Run the following command to start the API: +6. Run the following command to start the API: ```bash - python app/app.py + gmake run ``` 8. The API will be available at http://localhost:8080/api/bids diff --git a/requirements.txt b/requirements.txt index cb694ff..aa4e9fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ -blinker==1.6.2 -click==8.1.3 -Flask==2.3.2 -itsdangerous==2.1.2 -Jinja2==3.1.2 -MarkupSafe==2.1.3 -Werkzeug==2.3.6 -pip==23.1.2 -pytest==7.3.2 -flask_swagger_ui==4.11.1 +blinker +click +Flask +itsdangerous +Jinja2 +MarkupSafe +Werkzeug +pip == 23.2.0 +pytest +flask_swagger_ui marshmallow pymongo certifi \ No newline at end of file From 99d5a63de3d84027967141d3dc1a53222be94386 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Tue, 18 Jul 2023 18:04:50 +0100 Subject: [PATCH 66/97] feat: vulnerability check using safety --- Makefile | 12 ++++++++++++ README.md | 7 ++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 619be32..80907af 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,19 @@ run: venv test: venv $(PYTHON) -m pytest +# security vulnerability checker +check: + $(PIP) install safety + $(PIP) freeze | $(PYTHON) -m safety check --stdin + clean: rm -rf __pycache__ rm -rf .venv rm -rf .pytest_cache + +help: + @echo "make run - run the application" + @echo "make test - run the tests" + @echo "make clean - remove all generated files" + @echo "make check - check for security vulnerabilities" + @echo "make help - display this help" \ No newline at end of file diff --git a/README.md b/README.md index f5bc077..130c755 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,12 @@ This API provides an endpoint to post a new bid document. ```bash brew install make ``` -6. Run the following command to start the API: +6. Run the following command to have all the commands to use the API with Makefile: + + ```bash + gmake help + ``` +7. Run the following command to start the API: ```bash gmake run From 8859be216ad165ccd835a3ad17509396ecfc8f11 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 19 Jul 2023 09:28:51 +0100 Subject: [PATCH 67/97] refactor: added custom error message to path param validation --- api/schemas/valid_bid_id_schema.py | 2 +- tests/test_delete_bid.py | 2 +- tests/test_get_bid_by_id.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/schemas/valid_bid_id_schema.py b/api/schemas/valid_bid_id_schema.py index 77f178a..c91c83d 100644 --- a/api/schemas/valid_bid_id_schema.py +++ b/api/schemas/valid_bid_id_schema.py @@ -1,4 +1,4 @@ from marshmallow import Schema, fields, validate class valid_bid_id_schema(Schema): - bid_id = fields.Str(required=True, validate=validate.Length(min=36)) \ No newline at end of file + bid_id = fields.Str(required=True, validate=validate.Length(min=36, error="Invalid bid Id")) \ No newline at end of file diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py index c570254..f31e993 100644 --- a/tests/test_delete_bid.py +++ b/tests/test_delete_bid.py @@ -29,4 +29,4 @@ def test_get_bid_by_id_validation_error(mock_dbConnection, client): mock_dbConnection.side_effect = ValidationError response = client.delete('/api/bids/invalid_bid_id') assert response.status_code == 400 - assert response.get_json() == {'Error': "{'bid_id': ['Shorter than minimum length 36.']}"} \ No newline at end of file + assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} \ No newline at end of file diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index 4a3b84a..cfafa6c 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -47,4 +47,4 @@ def test_get_bid_by_id_validation_error(mock_dbConnection, client): mock_dbConnection.side_effect = ValidationError response = client.get('/api/bids/invalid_bid_id') assert response.status_code == 400 - assert response.get_json() == {'Error': "{'bid_id': ['Shorter than minimum length 36.']}"} + assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} From 8329de67b29acdb8905b4e19e526fb3365bab216 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Wed, 19 Jul 2023 11:32:26 +0100 Subject: [PATCH 68/97] feat: commit script Makefile --- Makefile | 25 ++++++++++++++++++++----- commit_message.txt | 1 + 2 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 commit_message.txt diff --git a/Makefile b/Makefile index 80907af..82e8e3d 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,14 @@ .ONESHELL: .DEFAULT_GOAL := run +TOPICS := fix - feat - docs - style - refactor - test - chore - build +COMMIT_MSG := "chore: update" PYTHON = ./.venv/bin/python3 PIP = ./.venv/bin/pip +.PHONY: run test clean check help + venv/bin/activate: requirements.txt python3 -m venv .venv $(PIP) install -r requirements.txt @@ -18,19 +22,30 @@ run: venv test: venv $(PYTHON) -m pytest -# security vulnerability checker +commit: + @echo "Available topics:" + @echo "$(TOPICS)" + @read -p "Enter the topic for the commit: " topic; \ + read -p "Enter the commit message: " message; \ + echo "$${topic}: $${message}" > commit_message.txt; \ + git add .; \ + git commit -F commit_message.txt; \ + rm commit_message.txt + check: $(PIP) install safety $(PIP) freeze | $(PYTHON) -m safety check --stdin clean: - rm -rf __pycache__ - rm -rf .venv - rm -rf .pytest_cache + @echo "Cleaning up..." + @find . -name "__pycache__" -type d -exec rm -rf {} + + @find . -name ".pytest_cache" -exec rm -rf {} + + @find . -name ".venv" -exec rm -rf {} + help: @echo "make run - run the application" @echo "make test - run the tests" @echo "make clean - remove all generated files" @echo "make check - check for security vulnerabilities" - @echo "make help - display this help" \ No newline at end of file + @echo "make commit - commit changes to git" + @echo "make help - display this help" diff --git a/commit_message.txt b/commit_message.txt new file mode 100644 index 0000000..c5622b9 --- /dev/null +++ b/commit_message.txt @@ -0,0 +1 @@ +feat: commit script Makefile From b6f3e0e0b47721bd3d34313157cc9602526287ec Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Wed, 19 Jul 2023 11:42:30 +0100 Subject: [PATCH 69/97] feat: commit message to gitignore --- .gitignore | 2 +- Makefile | 1 + commit_message.txt | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 68bc17f..add4bcf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ __pycache__/ *.py[cod] *$py.class - +commit_message.txt # C extensions *.so diff --git a/Makefile b/Makefile index 82e8e3d..3765fde 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,7 @@ commit: echo "$${topic}: $${message}" > commit_message.txt; \ git add .; \ git commit -F commit_message.txt; \ + git push; \ rm commit_message.txt check: diff --git a/commit_message.txt b/commit_message.txt index c5622b9..c0696db 100644 --- a/commit_message.txt +++ b/commit_message.txt @@ -1 +1 @@ -feat: commit script Makefile +feat: commit message to gitignore From 738434aaf537755092e524666548f89bb056e5ed Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Wed, 19 Jul 2023 12:13:37 +0100 Subject: [PATCH 70/97] refactor: makefile check --- .gitignore | 4 +++- Makefile | 6 +++--- commit_message.txt | 2 +- requirements.txt | 1 - 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index add4bcf..96e8b05 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ __pycache__/ *.py[cod] *$py.class -commit_message.txt # C extensions *.so @@ -127,6 +126,9 @@ venv/ ENV/ env.bak/ venv.bak/ +commit_message.txt +./commit_message.txt + # Spyder project settings .spyderproject diff --git a/Makefile b/Makefile index 3765fde..bb3e9d5 100644 --- a/Makefile +++ b/Makefile @@ -2,12 +2,12 @@ .DEFAULT_GOAL := run TOPICS := fix - feat - docs - style - refactor - test - chore - build -COMMIT_MSG := "chore: update" + PYTHON = ./.venv/bin/python3 PIP = ./.venv/bin/pip -.PHONY: run test clean check help +.PHONY: run test clean check help commit venv/bin/activate: requirements.txt python3 -m venv .venv @@ -33,7 +33,7 @@ commit: git push; \ rm commit_message.txt -check: +check: venv $(PIP) install safety $(PIP) freeze | $(PYTHON) -m safety check --stdin diff --git a/commit_message.txt b/commit_message.txt index c0696db..983bbf3 100644 --- a/commit_message.txt +++ b/commit_message.txt @@ -1 +1 @@ -feat: commit message to gitignore +refactor: makefile check diff --git a/requirements.txt b/requirements.txt index aa4e9fc..4d5e92d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,3 @@ pytest flask_swagger_ui marshmallow pymongo -certifi \ No newline at end of file From d642f7af9377040869689ffb2756697a213af503 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Wed, 19 Jul 2023 12:20:36 +0100 Subject: [PATCH 71/97] test: testing commit msg --- Makefile | 12 ++++++------ commit_message.txt | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index bb3e9d5..68b12e5 100644 --- a/Makefile +++ b/Makefile @@ -44,9 +44,9 @@ clean: @find . -name ".venv" -exec rm -rf {} + help: - @echo "make run - run the application" - @echo "make test - run the tests" - @echo "make clean - remove all generated files" - @echo "make check - check for security vulnerabilities" - @echo "make commit - commit changes to git" - @echo "make help - display this help" + @echo "gmake run - run the application" + @echo "gmake test - run the tests" + @echo "gmake clean - remove all generated files" + @echo "gmake check - check for security vulnerabilities" + @echo "gmake commit - commit changes to git" + @echo "gmake help - display this help" diff --git a/commit_message.txt b/commit_message.txt index 983bbf3..0cd70b1 100644 --- a/commit_message.txt +++ b/commit_message.txt @@ -1 +1 @@ -refactor: makefile check +test: testing commit msg From eea3c3b91ced41faf3f50af18e63a46f24165247 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Wed, 19 Jul 2023 12:23:58 +0100 Subject: [PATCH 72/97] test: test commit msg --- commit_message.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 commit_message.txt diff --git a/commit_message.txt b/commit_message.txt deleted file mode 100644 index 0cd70b1..0000000 --- a/commit_message.txt +++ /dev/null @@ -1 +0,0 @@ -test: testing commit msg From 9a4b05cf8eaea1b521a6b3e7428c619a605f00a3 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Wed, 19 Jul 2023 12:24:39 +0100 Subject: [PATCH 73/97] test: --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 68b12e5..38ec23c 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ TOPICS := fix - feat - docs - style - refactor - test - chore - build PYTHON = ./.venv/bin/python3 PIP = ./.venv/bin/pip + .PHONY: run test clean check help commit venv/bin/activate: requirements.txt From cc28971649754796bf545444918371dc2629b58f Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 19 Jul 2023 12:44:53 +0100 Subject: [PATCH 74/97] refactor: updated swagger to include additional error responses --- api/controllers/bid_controller.py | 34 +++++++++++------------- api/models/bid_model.py | 2 +- helpers/helpers.py | 18 +++++++++---- request_examples/get_all.http | 2 +- request_examples/update_bid.http | 2 +- static/swagger_config.yml | 44 ++++++++++++++----------------- tests/test_get_bid_by_id.py | 2 +- tests/test_get_bids.py | 14 ++-------- 8 files changed, 54 insertions(+), 64 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index ae1a623..5db0ce7 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -4,8 +4,7 @@ from api.models.status_enum import Status from dbconfig.mongo_setup import dbConnection from pymongo.errors import ConnectionFailure -from api.schemas.bid_schema import BidSchema -from helpers.helpers import showConnectionError, showNotFoundError, validate_and_create_data, validate_bid_id_path +from helpers.helpers import showInternalServerError, showNotFoundError, showValidationError, validate_and_create_bid_document, validate_bid_id_path, validate_bid_update bid = Blueprint('bid', __name__) @@ -16,26 +15,24 @@ def get_bids(): db = dbConnection() data = list(db['bids'].find({"status": {"$ne": Status.DELETED.value}})) return {'total_count': len(data), 'items': data}, 200 - except ConnectionFailure: - return showConnectionError() except Exception: - return jsonify({"Error": "Could not retrieve bids"}), 500 + return showInternalServerError(), 500 @bid.route("/bids", methods=["POST"]) def post_bid(): try: db = dbConnection() # Process input and create data model - data = validate_and_create_data(request.get_json()) + data = validate_and_create_bid_document(request.get_json()) # Insert document into database collection db['bids'].insert_one(data) return data, 201 # Return 400 response if input validation fails except ValidationError as e: - return jsonify({"Error": str(e)}), 400 + return showValidationError(e), 400 # Return 500 response in case of connection failure except ConnectionFailure: - return showConnectionError() + return showInternalServerError(), 500 @bid.route("/bids/", methods=["GET"]) def get_bid_by_id(bid_id): @@ -45,34 +42,33 @@ def get_bid_by_id(bid_id): data = db['bids'].find_one({"_id": bid_id , "status": {"$ne": Status.DELETED.value}}) # Return 404 response if not found / returns None if data is None: - return showNotFoundError() + return showNotFoundError(), 404 return data, 200 # Return 400 if bid_id is invalid except ValidationError as e: - return jsonify({"Error": str(e)}), 400 + return showValidationError(e), 400 # Return 500 response in case of connection failure except ConnectionFailure: - return showConnectionError() + return showInternalServerError(), 500 @bid.route("/bids/", methods=["PUT"]) def update_bid_by_id(bid_id): try: bid_id = validate_bid_id_path(bid_id) - # Validate user input - user_request = BidSchema().load(request.get_json(), partial=True) + user_request = validate_bid_update(request.get_json()) # Updates document where id is equal to bid_id db = dbConnection() data = db['bids'].find_one_and_update({"_id": bid_id}, {"$set": user_request}, return_document=True) # Return 404 response if not found / returns None if data is None: - return showNotFoundError() + return showNotFoundError(), 404 return data, 200 # Return 400 response if input validation fails except ValidationError as e: - return jsonify({"Error": str(e)}), 400 + return showValidationError(e), 400 # Return 500 response in case of connection failure except ConnectionFailure: - return showConnectionError() + return showInternalServerError(), 500 @bid.route("/bids/", methods=["DELETE"]) def change_status_to_deleted(bid_id): @@ -81,11 +77,11 @@ def change_status_to_deleted(bid_id): db = dbConnection() data = db['bids'].find_one_and_update({"_id": bid_id, "status": {"$ne": Status.DELETED.value}}, {"$set": {"status": Status.DELETED.value, "last_updated": datetime.now().isoformat()}}) if data is None: - return showNotFoundError() + return showNotFoundError(), 404 return data, 204 # Return 400 response if input validation fails except ValidationError as e: - return jsonify({"Error": str(e)}), 400 + return showValidationError(e), 400 # Return 500 response in case of connection failure except ConnectionFailure: - return showConnectionError() + return showInternalServerError(), 500 diff --git a/api/models/bid_model.py b/api/models/bid_model.py index bab98b1..772b73c 100644 --- a/api/models/bid_model.py +++ b/api/models/bid_model.py @@ -5,7 +5,7 @@ # Description: Schema for the bid object class BidModel(): - def __init__(self, tender, client, bid_date, alias=None, bid_folder_url=None, feedback=None, failed=None, was_successful=False, success=[],status=Status.IN_PROGRESS): + def __init__(self, tender, client, bid_date, alias=None, bid_folder_url=None, feedback=None, failed=None, was_successful=False, success=[], status=Status.IN_PROGRESS): self._id = uuid4() self.tender = tender self.client = client diff --git a/helpers/helpers.py b/helpers/helpers.py index 14ba168..88a409c 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -1,15 +1,19 @@ from flask import jsonify import uuid from datetime import datetime +from marshmallow import ValidationError from api.schemas.bid_schema import BidSchema from api.schemas.bid_request_schema import BidRequestSchema from api.schemas.valid_bid_id_schema import valid_bid_id_schema -def showConnectionError(): - return jsonify({"Error": "Could not connect to database"}), 500 +def showInternalServerError(): + return jsonify({"Error": "Could not connect to database"}) def showNotFoundError(): - return jsonify({"Error": "Not found"}), 404 + return jsonify({"Error": "Resource not found"}) + +def showValidationError(e): + return jsonify({"Error": str(e)}) def is_valid_uuid(string): try: @@ -25,7 +29,7 @@ def is_valid_isoformat(string): except: return False -def validate_and_create_data(request): +def validate_and_create_bid_document(request): # Process input and create data model bid_document = BidRequestSchema().load(request) # Serialize to a JSON object @@ -35,4 +39,8 @@ def validate_and_create_data(request): def validate_bid_id_path(bid_id): valid_bid_id = valid_bid_id_schema().load({"bid_id": bid_id}) validated_id = valid_bid_id["bid_id"] - return validated_id \ No newline at end of file + return validated_id + +def validate_bid_update(user_request): + data = BidSchema().load(user_request, partial=True) + return data \ No newline at end of file diff --git a/request_examples/get_all.http b/request_examples/get_all.http index 9f87ae8..712676d 100644 --- a/request_examples/get_all.http +++ b/request_examples/get_all.http @@ -1 +1 @@ -GET http://localhost:8080/api/bids/27fc4afb-fcdc-4bf7-9f8f-74f524831da4 HTTP/1.1 +GET http://localhost:8080/api/bids HTTP/1.1 diff --git a/request_examples/update_bid.http b/request_examples/update_bid.http index 0ca9eb3..241514e 100644 --- a/request_examples/update_bid.http +++ b/request_examples/update_bid.http @@ -1,4 +1,4 @@ -PUT http://localhost:8080/api/bids/1fb19151-2360-4ad0-86ed-f58ad2cd3b86 HTTP/1.1 +PUT http://localhost:8080/api/bids/9f688442-b535-4683-ae1a-a64c1a3b8616 HTTP/1.1 Content-Type: application/json { diff --git a/static/swagger_config.yml b/static/swagger_config.yml index fe8902e..3ee9bcb 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -63,12 +63,7 @@ paths: items: $ref: '#/components/schemas/Bid' '500': - description: Internal server error - content: - application/json: - schema: - type: object - example: {"Error": "Could not connect to database"} + $ref: '#/components/responses/InternalServerError' # -------------------------------------------- post: tags: @@ -114,15 +109,10 @@ paths: application/json: schema: $ref: '#/components/schemas/Bid' + '400': + $ref: '#/components/responses/BadRequest' '404': - description: Bid not found - content: - application/json: - schema: - type: object - example: { - "Error": "Bid not found" - } + $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' # -------------------------------------------- @@ -151,8 +141,11 @@ paths: schema: $ref: '#/components/schemas/Bid' '400': - description: Bad request (missing mandatory field/fields) $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' # -------------------------------------------- delete: tags: @@ -174,16 +167,10 @@ paths: description: Bid deleted content: noContent: {} - + '400': + $ref: '#/components/responses/BadRequest' '404': - description: Bid not found - content: - application/json: - schema: - type: object - example: { - "Error": "Not found" - } + $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' # -------------------------------------------- @@ -440,6 +427,15 @@ components: example: { "Error": "{'{field}': ['{message}']}" } + NotFound: + description: Not Found Error + content: + application/json: + schema: + type: object + example: { + "Error": "Resource not found" + } InternalServerError: description: Internal Server Error content: diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index cfafa6c..f979d02 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -39,7 +39,7 @@ def test_get_bid_by_id_not_found(mock_dbConnection, client): mock_dbConnection.assert_called_once() mock_db['bids'].find_one.assert_called_once_with({'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'status': {'$ne': 'deleted'}}) assert response.status_code == 404 - assert response.get_json() == {"Error": "Not found"} + assert response.get_json() == {"Error": "Resource not found"} # Case 4: Validation error @patch('api.controllers.bid_controller.dbConnection') diff --git a/tests/test_get_bids.py b/tests/test_get_bids.py index 923c344..10a99f1 100644 --- a/tests/test_get_bids.py +++ b/tests/test_get_bids.py @@ -12,18 +12,8 @@ def test_get_bids(mock_dbConnection, client): assert response.get_json() == {'total_count': 0, 'items': []} # Case 2: Connection error -@patch('api.controllers.bid_controller.dbConnection', side_effect=ConnectionFailure) +@patch('api.controllers.bid_controller.dbConnection', side_effect=Exception) def test_get_bids_connection_error(mock_dbConnection, client): response = client.get('/api/bids') assert response.status_code == 500 - assert response.get_json() == {"Error": "Could not connect to database"} - -# Case 3: Failed to call db['bids'].find -@patch('api.controllers.bid_controller.dbConnection') -def test_get_bids_find_error(mock_dbConnection, client): - mock_db = mock_dbConnection.return_value - mock_db['bids'].find.side_effect = Exception - - response = client.get('/api/bids') - assert response.status_code == 500 - assert response.get_json() == {"Error": "Could not retrieve bids"} \ No newline at end of file + assert response.get_json() == {"Error": "Could not connect to database"} \ No newline at end of file From 631ca74abad3a482152f523f3ff79794ae6983c8 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 19 Jul 2023 15:59:49 +0100 Subject: [PATCH 75/97] refactor: updated makefile and refactored commit target --- CONTRIBUTING.md | 21 +++++++++++++++++++++ Makefile | 19 ++++++++++++++----- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f02b29..d436b19 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,6 +8,27 @@ * You may rebase your branch after feedback if it's to include relevant updates from the develop branch. It is preferable to rebase here then a merge commit as a clean and straight history on develop with discrete merge commits for features is preferred * To find out more about contributing click [here](https://contributing.md/) +## Branching +* Branches should be named in the following format: + ```textbox + / + ``` + where `type` is one of the following: + ```textbox + * fix: for a bug fix + * feat: for a new feature + * docs: for documentation changes + * style: for changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) + * refactor: for refactoring production code + * test: for adding tests + * chore: for updating build tasks, package manager configs, etc + * build: for changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) + ``` + and `description` is a short description of the feature or fix being made + * e.g. `feat/new-feature` or `fix/bug-fix` +* Branches should be created from the `develop` branch +* Branches should be merged back into the `develop` branch + ## Commit messages Please use the following format for commit messages: diff --git a/Makefile b/Makefile index 38ec23c..c6f8fc0 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ PYTHON = ./.venv/bin/python3 PIP = ./.venv/bin/pip -.PHONY: run test clean check help commit +.PHONY: run test clean check help commit swagger venv/bin/activate: requirements.txt python3 -m venv .venv @@ -23,16 +23,21 @@ run: venv test: venv $(PYTHON) -m pytest +branch: + @echo "Available branch types:" + @echo "$(TOPICS)" + @read -p "Enter the branch type: " type; \ + read -p "Enter the branch description: " description; \ + git checkout -b $${type}/$${description} + commit: @echo "Available topics:" @echo "$(TOPICS)" @read -p "Enter the topic for the commit: " topic; \ read -p "Enter the commit message: " message; \ - echo "$${topic}: $${message}" > commit_message.txt; \ git add .; \ - git commit -F commit_message.txt; \ - git push; \ - rm commit_message.txt + git commit -m "$${topic}: $${message}"; \ + git push check: venv $(PIP) install safety @@ -51,3 +56,7 @@ help: @echo "gmake check - check for security vulnerabilities" @echo "gmake commit - commit changes to git" @echo "gmake help - display this help" + +swagger: venv + open http://localhost:8080/api/docs/#/ + $(PYTHON) app.py \ No newline at end of file From 177f57421404c0a617845fe4247c828584f6d4aa Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 19 Jul 2023 16:05:55 +0100 Subject: [PATCH 76/97] fix: set upstream for new branch --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c6f8fc0..76b36c4 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,8 @@ branch: @echo "$(TOPICS)" @read -p "Enter the branch type: " type; \ read -p "Enter the branch description: " description; \ - git checkout -b $${type}/$${description} + git checkout -b $${type}/$${description}; \ + git push --set-upstream origin $${type}/$${description} commit: @echo "Available topics:" From f86fdad6c74299899ede4b5d88b55c0e311018bd Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 19 Jul 2023 17:04:56 +0100 Subject: [PATCH 77/97] refactor: removed unnecessary mocking in post_bid tests --- tests/test_post_bid.py | 53 ++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/tests/test_post_bid.py b/tests/test_post_bid.py index be5aa6d..f66e6a5 100644 --- a/tests/test_post_bid.py +++ b/tests/test_post_bid.py @@ -58,12 +58,37 @@ def test_field_missing(client): # Case 3: Connection error -@patch('api.controllers.bid_controller.dbConnection', side_effect=ConnectionFailure) -def test_get_bids_connection_error(mock_dbConnection, client): - # Mock the behavior of dbConnection +@patch('api.controllers.bid_controller.dbConnection') +def test_post_bid_connection_error(mock_dbConnection, client): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "success": [ + { + "phase": 1, + "has_score": True, + "out_of": 36, + "score": 30 + } + ], + "failed": { + "phase": 2, + "has_score": True, + "score": 20, + "out_of": 36 + } + } + # Mock the behavior of dbConnection mock_db = mock_dbConnection.return_value - mock_db['bids'].insert_one.side_effect = Exception - response = client.get('/api/bids') + mock_db['bids'].insert_one.side_effect = ConnectionFailure + response = client.post('/api/bids', json=data) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} @@ -97,15 +122,11 @@ def test_phase_greater_than_2(mock_dbConnection, client): } } - # Mock the behavior of dbConnection - mock_db = mock_dbConnection.return_value - mock_db['bids'].insert_one.side_effect = Exception - response = client.post("api/bids", json=data) assert response.status_code == 400 assert response.get_json() == { 'Error': "{'failed': {'phase': ['Must be one of: 1, 2.']}}" - } + } # Case 5: Neither success nor failed fields can have the same phase @@ -137,15 +158,11 @@ def test_same_phase(mock_dbConnection, client): } } - # Mock the behavior of dbConnection - mock_db = mock_dbConnection.return_value - mock_db['bids'].insert_one.side_effect = Exception - response = client.post("api/bids", json=data) assert response.status_code == 400 assert response.get_json() == { 'Error': "{'success': [\"Phase value already exists in 'failed' section and cannot be repeated.\"]}" - } + } # Case 6: Success cannot have the same phase in the list @@ -177,12 +194,8 @@ def test_success_same_phase(mock_dbConnection, client): ], } - # Mock the behavior of dbConnection - mock_db = mock_dbConnection.return_value - mock_db['bids'].insert_one.side_effect = Exception - response = client.post("api/bids", json=data) assert response.status_code == 400 assert response.get_json() == { 'Error': "{'success': [\"Phase value already exists in 'success' list and cannot be repeated.\"]}" - } + } From 161e8b7d1e9132c53e53b1c03b3852134abee1f6 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 19 Jul 2023 17:22:42 +0100 Subject: [PATCH 78/97] refactor: changed variable name in helpers for clarity --- helpers/helpers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/helpers/helpers.py b/helpers/helpers.py index 88a409c..84af000 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -1,7 +1,6 @@ from flask import jsonify import uuid from datetime import datetime -from marshmallow import ValidationError from api.schemas.bid_schema import BidSchema from api.schemas.bid_request_schema import BidRequestSchema from api.schemas.valid_bid_id_schema import valid_bid_id_schema @@ -38,8 +37,8 @@ def validate_and_create_bid_document(request): def validate_bid_id_path(bid_id): valid_bid_id = valid_bid_id_schema().load({"bid_id": bid_id}) - validated_id = valid_bid_id["bid_id"] - return validated_id + data = valid_bid_id["bid_id"] + return data def validate_bid_update(user_request): data = BidSchema().load(user_request, partial=True) From 91821791e136d031e03e55c6fe042fd366a20124 Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 20 Jul 2023 09:36:13 +0100 Subject: [PATCH 79/97] refactor: removed unused imports --- api/controllers/bid_controller.py | 11 +++++------ api/schemas/bid_request_schema.py | 2 -- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 5db0ce7..93426a5 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -1,9 +1,8 @@ -from flask import Blueprint, jsonify, request +from flask import Blueprint, request from datetime import datetime from marshmallow import ValidationError from api.models.status_enum import Status from dbconfig.mongo_setup import dbConnection -from pymongo.errors import ConnectionFailure from helpers.helpers import showInternalServerError, showNotFoundError, showValidationError, validate_and_create_bid_document, validate_bid_id_path, validate_bid_update bid = Blueprint('bid', __name__) @@ -31,7 +30,7 @@ def post_bid(): except ValidationError as e: return showValidationError(e), 400 # Return 500 response in case of connection failure - except ConnectionFailure: + except Exception: return showInternalServerError(), 500 @bid.route("/bids/", methods=["GET"]) @@ -48,7 +47,7 @@ def get_bid_by_id(bid_id): except ValidationError as e: return showValidationError(e), 400 # Return 500 response in case of connection failure - except ConnectionFailure: + except Exception: return showInternalServerError(), 500 @bid.route("/bids/", methods=["PUT"]) @@ -67,7 +66,7 @@ def update_bid_by_id(bid_id): except ValidationError as e: return showValidationError(e), 400 # Return 500 response in case of connection failure - except ConnectionFailure: + except Exception: return showInternalServerError(), 500 @bid.route("/bids/", methods=["DELETE"]) @@ -83,5 +82,5 @@ def change_status_to_deleted(bid_id): except ValidationError as e: return showValidationError(e), 400 # Return 500 response in case of connection failure - except ConnectionFailure: + except Exception: return showInternalServerError(), 500 diff --git a/api/schemas/bid_request_schema.py b/api/schemas/bid_request_schema.py index 36bae3b..f0421c8 100644 --- a/api/schemas/bid_request_schema.py +++ b/api/schemas/bid_request_schema.py @@ -1,9 +1,7 @@ from marshmallow import Schema, fields, post_load, validates, ValidationError from api.models.bid_model import BidModel -from .links_schema import LinksSchema from .phase_schema import PhaseSchema from .feedback_schema import FeedbackSchema -from ..models.status_enum import Status # Marshmallow schema for request body class BidRequestSchema(Schema): From 64521738c64de0643d9eadf0592aed5b61ebc99b Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 20 Jul 2023 10:36:47 +0100 Subject: [PATCH 80/97] feat: created prepend host function and test --- helpers/helpers.py | 8 +++++++- tests/test_prepend_host.py | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 tests/test_prepend_host.py diff --git a/helpers/helpers.py b/helpers/helpers.py index 84af000..a4c13da 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -42,4 +42,10 @@ def validate_bid_id_path(bid_id): def validate_bid_update(user_request): data = BidSchema().load(user_request, partial=True) - return data \ No newline at end of file + return data + +def prepend_host(resource, hostname): + host = f"http//{hostname}" + for key in resource["links"]: + resource["links"][key] = f'{host}{resource["links"][key]}' + return resource \ No newline at end of file diff --git a/tests/test_prepend_host.py b/tests/test_prepend_host.py new file mode 100644 index 0000000..2986e33 --- /dev/null +++ b/tests/test_prepend_host.py @@ -0,0 +1,26 @@ +from helpers.helpers import prepend_host + +def test_prepend_host(): + resource = { + "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", + "alias": "ONS", + "bid_date": "2023-06-23", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "Office for National Statistics", + "last_updated": "2023-07-19T11:15:25.743340", + "links": { + "questions": "/bids/9f688442-b535-4683-ae1a-a64c1a3b8616/questions", + "self": "/bids/9f688442-b535-4683-ae1a-a64c1a3b8616" + }, + "status": "in_progress", + "tender": "Business Intelligence and Data Warehousing", + "was_successful": False + } + hostname = "localhost:8080" + + result = prepend_host(resource, hostname) + + assert result["links"] == { + "questions": "http//localhost:8080/bids/9f688442-b535-4683-ae1a-a64c1a3b8616/questions", + "self": "http//localhost:8080/bids/9f688442-b535-4683-ae1a-a64c1a3b8616" + } \ No newline at end of file From ef35c17de1b1947c401099a458c923a4c940e77f Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Thu, 20 Jul 2023 12:12:44 +0100 Subject: [PATCH 81/97] feat: lint and format features added to makefile --- Makefile | 13 ++- api/controllers/bid_controller.py | 44 +++++++--- api/models/bid_model.py | 21 ++++- api/models/links_model.py | 2 +- api/models/status_enum.py | 3 +- api/schemas/bid_request_schema.py | 35 ++++++-- api/schemas/bid_schema.py | 3 +- api/schemas/feedback_schema.py | 3 +- api/schemas/links_schema.py | 3 +- api/schemas/phase_schema.py | 2 + api/schemas/valid_bid_id_schema.py | 5 +- app.py | 23 ++--- dbconfig/mongo_setup.py | 2 +- helpers/helpers.py | 14 ++- tests/conftest.py | 5 +- tests/test_BidRequestSchema.py | 45 ++++------ tests/test_delete_bid.py | 23 ++--- tests/test_get_bid_by_id.py | 40 +++++---- tests/test_get_bids.py | 16 ++-- tests/test_post_bid.py | 133 +++++++++++------------------ tests/test_update_bid_by_id.py | 26 +++--- 21 files changed, 256 insertions(+), 205 deletions(-) diff --git a/Makefile b/Makefile index 76b36c4..1a9ac1d 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ PYTHON = ./.venv/bin/python3 PIP = ./.venv/bin/pip -.PHONY: run test clean check help commit swagger +.PHONY: run test clean check help commit swagger format branch lint venv/bin/activate: requirements.txt python3 -m venv .venv @@ -31,7 +31,7 @@ branch: git checkout -b $${type}/$${description}; \ git push --set-upstream origin $${type}/$${description} -commit: +commit: format @echo "Available topics:" @echo "$(TOPICS)" @read -p "Enter the topic for the commit: " topic; \ @@ -50,6 +50,15 @@ clean: @find . -name ".pytest_cache" -exec rm -rf {} + @find . -name ".venv" -exec rm -rf {} + +lint: venv + $(PIP) install flake8 pylint + $(PYTHON) -m flake8 + $(PYTHON) -m pylint **/*.py + +format: + $(PIP) install black + $(PYTHON) -m black . + help: @echo "gmake run - run the application" @echo "gmake test - run the tests" diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 5db0ce7..7016ee2 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -4,20 +4,29 @@ from api.models.status_enum import Status from dbconfig.mongo_setup import dbConnection from pymongo.errors import ConnectionFailure -from helpers.helpers import showInternalServerError, showNotFoundError, showValidationError, validate_and_create_bid_document, validate_bid_id_path, validate_bid_update +from helpers.helpers import ( + showInternalServerError, + showNotFoundError, + showValidationError, + validate_and_create_bid_document, + validate_bid_id_path, + validate_bid_update, +) + +bid = Blueprint("bid", __name__) -bid = Blueprint('bid', __name__) @bid.route("/bids", methods=["GET"]) def get_bids(): # Get all bids from database collection try: db = dbConnection() - data = list(db['bids'].find({"status": {"$ne": Status.DELETED.value}})) - return {'total_count': len(data), 'items': data}, 200 + data = list(db["bids"].find({"status": {"$ne": Status.DELETED.value}})) + return {"total_count": len(data), "items": data}, 200 except Exception: return showInternalServerError(), 500 - + + @bid.route("/bids", methods=["POST"]) def post_bid(): try: @@ -25,7 +34,7 @@ def post_bid(): # Process input and create data model data = validate_and_create_bid_document(request.get_json()) # Insert document into database collection - db['bids'].insert_one(data) + db["bids"].insert_one(data) return data, 201 # Return 400 response if input validation fails except ValidationError as e: @@ -33,13 +42,16 @@ def post_bid(): # Return 500 response in case of connection failure except ConnectionFailure: return showInternalServerError(), 500 - + + @bid.route("/bids/", methods=["GET"]) def get_bid_by_id(bid_id): try: bid_id = validate_bid_id_path(bid_id) db = dbConnection() - data = db['bids'].find_one({"_id": bid_id , "status": {"$ne": Status.DELETED.value}}) + data = db["bids"].find_one( + {"_id": bid_id, "status": {"$ne": Status.DELETED.value}} + ) # Return 404 response if not found / returns None if data is None: return showNotFoundError(), 404 @@ -51,6 +63,7 @@ def get_bid_by_id(bid_id): except ConnectionFailure: return showInternalServerError(), 500 + @bid.route("/bids/", methods=["PUT"]) def update_bid_by_id(bid_id): try: @@ -58,7 +71,9 @@ def update_bid_by_id(bid_id): user_request = validate_bid_update(request.get_json()) # Updates document where id is equal to bid_id db = dbConnection() - data = db['bids'].find_one_and_update({"_id": bid_id}, {"$set": user_request}, return_document=True) + data = db["bids"].find_one_and_update( + {"_id": bid_id}, {"$set": user_request}, return_document=True + ) # Return 404 response if not found / returns None if data is None: return showNotFoundError(), 404 @@ -70,12 +85,21 @@ def update_bid_by_id(bid_id): except ConnectionFailure: return showInternalServerError(), 500 + @bid.route("/bids/", methods=["DELETE"]) def change_status_to_deleted(bid_id): try: bid_id = validate_bid_id_path(bid_id) db = dbConnection() - data = db['bids'].find_one_and_update({"_id": bid_id, "status": {"$ne": Status.DELETED.value}}, {"$set": {"status": Status.DELETED.value, "last_updated": datetime.now().isoformat()}}) + data = db["bids"].find_one_and_update( + {"_id": bid_id, "status": {"$ne": Status.DELETED.value}}, + { + "$set": { + "status": Status.DELETED.value, + "last_updated": datetime.now().isoformat(), + } + }, + ) if data is None: return showNotFoundError(), 404 return data, 204 diff --git a/api/models/bid_model.py b/api/models/bid_model.py index 772b73c..3e13bbf 100644 --- a/api/models/bid_model.py +++ b/api/models/bid_model.py @@ -3,19 +3,32 @@ from .links_model import LinksModel from api.models.status_enum import Status + # Description: Schema for the bid object -class BidModel(): - def __init__(self, tender, client, bid_date, alias=None, bid_folder_url=None, feedback=None, failed=None, was_successful=False, success=[], status=Status.IN_PROGRESS): +class BidModel: + def __init__( + self, + tender, + client, + bid_date, + alias=None, + bid_folder_url=None, + feedback=None, + failed=None, + was_successful=False, + success=[], + status=Status.IN_PROGRESS, + ): self._id = uuid4() self.tender = tender self.client = client self.alias = alias self.bid_date = bid_date self.bid_folder_url = bid_folder_url - self.status = status # enum: "deleted", "in_progress" or "completed" + self.status = status # enum: "deleted", "in_progress" or "completed" self.links = LinksModel(self._id) self.was_successful = was_successful - self.success = success + self.success = success self.failed = failed self.feedback = feedback self.last_updated = datetime.now() diff --git a/api/models/links_model.py b/api/models/links_model.py index 83f3827..4a3535b 100644 --- a/api/models/links_model.py +++ b/api/models/links_model.py @@ -1,5 +1,5 @@ # Schema for links object -class LinksModel(): +class LinksModel: def __init__(self, id): self.self = f"/bids/{id}" self.questions = f"/bids/{id}/questions" diff --git a/api/models/status_enum.py b/api/models/status_enum.py index 592e02a..6fa7dd5 100644 --- a/api/models/status_enum.py +++ b/api/models/status_enum.py @@ -1,8 +1,9 @@ from enum import Enum, unique + # Enum for status @unique class Status(Enum): DELETED = "deleted" IN_PROGRESS = "in_progress" - COMPLETED = "completed" \ No newline at end of file + COMPLETED = "completed" diff --git a/api/schemas/bid_request_schema.py b/api/schemas/bid_request_schema.py index 36bae3b..292fa2c 100644 --- a/api/schemas/bid_request_schema.py +++ b/api/schemas/bid_request_schema.py @@ -5,18 +5,29 @@ from .feedback_schema import FeedbackSchema from ..models.status_enum import Status + # Marshmallow schema for request body class BidRequestSchema(Schema): - tender = fields.Str(required=True, error_messages={"required": {"message": "Missing mandatory field"}}) - client = fields.Str(required=True, error_messages={"required": {"message": "Missing mandatory field"}}) + tender = fields.Str( + required=True, + error_messages={"required": {"message": "Missing mandatory field"}}, + ) + client = fields.Str( + required=True, + error_messages={"required": {"message": "Missing mandatory field"}}, + ) alias = fields.Str() - bid_date = fields.Date(format='%d-%m-%Y', required=True, error_messages={"required": {"message": "Missing mandatory field"}}) + bid_date = fields.Date( + format="%d-%m-%Y", + required=True, + error_messages={"required": {"message": "Missing mandatory field"}}, + ) bid_folder_url = fields.URL() was_successful = fields.Boolean() success = fields.List(fields.Nested(PhaseSchema)) failed = fields.Nested(PhaseSchema) feedback = fields.Nested(FeedbackSchema) - + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.context["failed_phase_values"] = set() @@ -27,14 +38,18 @@ def validate_success(self, value): for phase in value: phase_value = phase.get("phase") if phase_value in phase_values: - raise ValidationError("Phase value already exists in 'success' list and cannot be repeated.") + raise ValidationError( + "Phase value already exists in 'success' list and cannot be repeated." + ) phase_values.add(phase_value) @validates("failed") def validate_failed(self, value): phase_value = value.get("phase") if phase_value in self.context.get("success_phase_values", set()): - raise ValidationError("Phase value already exists in 'success' list and cannot be repeated.") + raise ValidationError( + "Phase value already exists in 'success' list and cannot be repeated." + ) self.context["failed_phase_values"].add(phase_value) @validates("success") @@ -44,10 +59,12 @@ def validate_success_and_failed(self, value): for phase in value: phase_value = phase.get("phase") if phase_value in failed_phase_values: - raise ValidationError("Phase value already exists in 'failed' section and cannot be repeated.") + raise ValidationError( + "Phase value already exists in 'failed' section and cannot be repeated." + ) success_phase_values.add(phase_value) - + # Creates a Bid instance after processing @post_load def makeBid(self, data, **kwargs): - return BidModel(**data) \ No newline at end of file + return BidModel(**data) diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py index 139a3b4..4bfc935 100644 --- a/api/schemas/bid_schema.py +++ b/api/schemas/bid_schema.py @@ -5,6 +5,7 @@ from .feedback_schema import FeedbackSchema from ..models.status_enum import Status + # Marshmallow schema class BidSchema(Schema): _id = fields.UUID(required=True) @@ -24,4 +25,4 @@ class BidSchema(Schema): @post_load def set_last_updated(self, data, **kwargs): data["last_updated"] = datetime.now().isoformat() - return data \ No newline at end of file + return data diff --git a/api/schemas/feedback_schema.py b/api/schemas/feedback_schema.py index ec24452..a6aa08a 100644 --- a/api/schemas/feedback_schema.py +++ b/api/schemas/feedback_schema.py @@ -1,5 +1,6 @@ from marshmallow import Schema, fields + class FeedbackSchema(Schema): description = fields.Str(required=True) - url = fields.URL(required=True) \ No newline at end of file + url = fields.URL(required=True) diff --git a/api/schemas/links_schema.py b/api/schemas/links_schema.py index 4a711e2..57bfacd 100644 --- a/api/schemas/links_schema.py +++ b/api/schemas/links_schema.py @@ -1,5 +1,6 @@ from marshmallow import Schema, fields + class LinksSchema(Schema): self = fields.Str(required=True) - questions = fields.Str(required=True) \ No newline at end of file + questions = fields.Str(required=True) diff --git a/api/schemas/phase_schema.py b/api/schemas/phase_schema.py index c723821..259a437 100644 --- a/api/schemas/phase_schema.py +++ b/api/schemas/phase_schema.py @@ -1,11 +1,13 @@ from marshmallow import Schema, fields, validates_schema, ValidationError from enum import Enum, unique + @unique class Phase(Enum): PHASE_1 = 1 PHASE_2 = 2 + class PhaseSchema(Schema): phase = fields.Enum(Phase, required=True, by_value=True) has_score = fields.Boolean(required=True) diff --git a/api/schemas/valid_bid_id_schema.py b/api/schemas/valid_bid_id_schema.py index c91c83d..acf5862 100644 --- a/api/schemas/valid_bid_id_schema.py +++ b/api/schemas/valid_bid_id_schema.py @@ -1,4 +1,7 @@ from marshmallow import Schema, fields, validate + class valid_bid_id_schema(Schema): - bid_id = fields.Str(required=True, validate=validate.Length(min=36, error="Invalid bid Id")) \ No newline at end of file + bid_id = fields.Str( + required=True, validate=validate.Length(min=36, error="Invalid bid Id") + ) diff --git a/app.py b/app.py index 3ce7ba0..9d1930b 100644 --- a/app.py +++ b/app.py @@ -1,24 +1,27 @@ +""" +This is a simple Python application. + +""" + from flask import Flask from flask_swagger_ui import get_swaggerui_blueprint - from api.controllers.bid_controller import bid app = Flask(__name__) -SWAGGER_URL = '/api/docs' # URL for exposing Swagger UI -API_URL = '/static/swagger_config.yml' # Our API url +SWAGGER_URL = "/api/docs" # URL for exposing Swagger UI +API_URL = "/static/swagger_config.yml" # Our API url # Call factory function to create our blueprint swaggerui_blueprint = get_swaggerui_blueprint( - SWAGGER_URL, # Swagger UI static files will be mapped to '{SWAGGER_URL}/dist/' + SWAGGER_URL, API_URL, - config={ # Swagger UI config overrides - 'app_name': "Bids API Swagger" - }) + config={"app_name": "Bids API Swagger"}, +) app.register_blueprint(swaggerui_blueprint) -app.register_blueprint(bid, url_prefix='/api') +app.register_blueprint(bid, url_prefix="/api") -if __name__ == '__main__': - app.run(debug=True, port=8080) \ No newline at end of file +if __name__ == "__main__": + app.run(debug=True, port=8080) diff --git a/dbconfig/mongo_setup.py b/dbconfig/mongo_setup.py index 97a116d..d49f1b5 100644 --- a/dbconfig/mongo_setup.py +++ b/dbconfig/mongo_setup.py @@ -2,9 +2,9 @@ MONGO_URI = "mongodb://localhost:27017/bidsAPI" + # Create a new client and connect to the server def dbConnection(): client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=10000) db = client["bidsAPI"] return db - \ No newline at end of file diff --git a/helpers/helpers.py b/helpers/helpers.py index 88a409c..ee36dee 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -6,29 +6,35 @@ from api.schemas.bid_request_schema import BidRequestSchema from api.schemas.valid_bid_id_schema import valid_bid_id_schema + def showInternalServerError(): return jsonify({"Error": "Could not connect to database"}) + def showNotFoundError(): return jsonify({"Error": "Resource not found"}) + def showValidationError(e): return jsonify({"Error": str(e)}) + def is_valid_uuid(string): try: uuid.UUID(str(string)) return True except ValueError: return False - + + def is_valid_isoformat(string): try: datetime.fromisoformat(string) return True except: return False - + + def validate_and_create_bid_document(request): # Process input and create data model bid_document = BidRequestSchema().load(request) @@ -36,11 +42,13 @@ def validate_and_create_bid_document(request): data = BidSchema().dump(bid_document) return data + def validate_bid_id_path(bid_id): valid_bid_id = valid_bid_id_schema().load({"bid_id": bid_id}) validated_id = valid_bid_id["bid_id"] return validated_id + def validate_bid_update(user_request): data = BidSchema().load(user_request, partial=True) - return data \ No newline at end of file + return data diff --git a/tests/conftest.py b/tests/conftest.py index 3c706e5..8aa7aa7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,9 +2,10 @@ from flask import Flask from api.controllers.bid_controller import bid + @pytest.fixture def client(): app = Flask(__name__) - app.register_blueprint(bid, url_prefix='/api') + app.register_blueprint(bid, url_prefix="/api") with app.test_client() as client: - yield client \ No newline at end of file + yield client diff --git a/tests/test_BidRequestSchema.py b/tests/test_BidRequestSchema.py index ac01665..b8fa4b9 100644 --- a/tests/test_BidRequestSchema.py +++ b/tests/test_BidRequestSchema.py @@ -4,6 +4,7 @@ from api.schemas.bid_request_schema import BidRequestSchema from helpers.helpers import is_valid_uuid, is_valid_isoformat + def test_bid_model(): data = { "tender": "Business Intelligence and Data Warehousing", @@ -13,22 +14,10 @@ def test_bid_model(): "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": { "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", }, - "success": [ - { - "phase": 1, - "has_score": True, - "out_of": 36, - "score": 30 - } - ], - "failed": { - "phase": 2, - "has_score": True, - "score": 20, - "out_of": 36 - } + "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], + "failed": {"phase": 2, "has_score": True, "score": 20, "out_of": 36}, } bid_document = BidRequestSchema().load(data) to_post = BidSchema().dump(bid_document) @@ -39,12 +28,12 @@ def test_bid_model(): assert is_valid_uuid(id) is True # Test UUID validator assert is_valid_uuid("99999") is False - + # Test that links object is generated and URLs are correct assert to_post["links"] is not None assert "self" in to_post["links"] assert to_post["links"]["self"] == f"/bids/{id}" - assert 'questions' in to_post["links"] + assert "questions" in to_post["links"] assert to_post["links"]["questions"] == f"/bids/{id}/questions" # Test that status is set to in_progress @@ -56,44 +45,49 @@ def test_bid_model(): # Test ISOformat validator assert is_valid_isoformat("07-06-2023") is False + def test_validate_tender(): data = { "tender": 42, "client": "Office for National Statistics", - "bid_date": "21-06-2023" + "bid_date": "21-06-2023", } with pytest.raises(ValidationError): BidRequestSchema().load(data) - + + def test_validate_client(): data = { "tender": "Business Intelligence and Data Warehousing", "client": 42, - "bid_date": "21-06-2023" + "bid_date": "21-06-2023", } with pytest.raises(ValidationError): BidRequestSchema().load(data) + def test_validate_bid_date(): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", - "bid_date": "2023-12-25" + "bid_date": "2023-12-25", } with pytest.raises(ValidationError): BidRequestSchema().load(data) + def test_validate_bid_folder_url(): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", "bid_date": "21-06-2023", - "bid_folder_url": "Not a valid URL" + "bid_folder_url": "Not a valid URL", } with pytest.raises(ValidationError): BidRequestSchema().load(data) + def test_validate_feedback(): data = { "tender": "Business Intelligence and Data Warehousing", @@ -101,11 +95,8 @@ def test_validate_feedback(): "bid_date": "21-06-2023", "alias": "ONS", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "feedback": { - "description": 42, - "url": "Invalid URL" - } + "feedback": {"description": 42, "url": "Invalid URL"}, } with pytest.raises(ValidationError): - BidRequestSchema().load(data) \ No newline at end of file + BidRequestSchema().load(data) diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py index f31e993..81be726 100644 --- a/tests/test_delete_bid.py +++ b/tests/test_delete_bid.py @@ -2,31 +2,34 @@ from unittest.mock import patch from marshmallow import ValidationError + # Case 1: Successful delete a bid by changing status to deleted -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_delete_bid_success(mock_dbConnection, client): mock_db = mock_dbConnection.return_value - mock_db['bids'].find_one_and_update.return_value = { + mock_db["bids"].find_one_and_update.return_value = { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", - "status": "deleted" + "status": "deleted", } - response = client.delete('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + response = client.delete("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") assert response.status_code == 204 assert response.content_length is None + # Case 2: Failed to call database -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_delete_bid_find_error(mock_dbConnection, client): mock_db = mock_dbConnection.return_value - mock_db['bids'].find_one_and_update.side_effect = ConnectionFailure - response = client.delete('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + mock_db["bids"].find_one_and_update.side_effect = ConnectionFailure + response = client.delete("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} + # Case 3: Validation error -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_get_bid_by_id_validation_error(mock_dbConnection, client): mock_dbConnection.side_effect = ValidationError - response = client.delete('/api/bids/invalid_bid_id') + response = client.delete("/api/bids/invalid_bid_id") assert response.status_code == 400 - assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} \ No newline at end of file + assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index f979d02..4f84799 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -2,49 +2,57 @@ from pymongo.errors import ConnectionFailure from marshmallow import ValidationError + # Case 1: Successful get_bid_by_id -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_get_bid_by_id_success(mock_dbConnection, client): mock_db = mock_dbConnection.return_value - mock_db['bids'].find_one.return_value = { - '_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', - 'tender': 'Business Intelligence and Data Warehousing' + mock_db["bids"].find_one.return_value = { + "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", + "tender": "Business Intelligence and Data Warehousing", } - response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + response = client.get("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") mock_dbConnection.assert_called_once() - mock_db['bids'].find_one.assert_called_once_with({'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'status': {'$ne': 'deleted'}}) + mock_db["bids"].find_one.assert_called_once_with( + {"_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "status": {"$ne": "deleted"}} + ) assert response.status_code == 200 assert response.get_json() == { - '_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', - 'tender': 'Business Intelligence and Data Warehousing' + "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", + "tender": "Business Intelligence and Data Warehousing", } + # Case 2: Connection error -@patch('api.controllers.bid_controller.dbConnection', side_effect=ConnectionFailure) +@patch("api.controllers.bid_controller.dbConnection", side_effect=ConnectionFailure) def test_get_bids_connection_error(mock_dbConnection, client): - response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + response = client.get("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} + # Case 3: Bid not found -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_get_bid_by_id_not_found(mock_dbConnection, client): mock_db = mock_dbConnection.return_value - mock_db['bids'].find_one.return_value = None + mock_db["bids"].find_one.return_value = None - response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + response = client.get("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") mock_dbConnection.assert_called_once() - mock_db['bids'].find_one.assert_called_once_with({'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'status': {'$ne': 'deleted'}}) + mock_db["bids"].find_one.assert_called_once_with( + {"_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "status": {"$ne": "deleted"}} + ) assert response.status_code == 404 assert response.get_json() == {"Error": "Resource not found"} + # Case 4: Validation error -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_get_bid_by_id_validation_error(mock_dbConnection, client): mock_dbConnection.side_effect = ValidationError - response = client.get('/api/bids/invalid_bid_id') + response = client.get("/api/bids/invalid_bid_id") assert response.status_code == 400 assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} diff --git a/tests/test_get_bids.py b/tests/test_get_bids.py index 10a99f1..0658fd8 100644 --- a/tests/test_get_bids.py +++ b/tests/test_get_bids.py @@ -1,19 +1,21 @@ from unittest.mock import patch from pymongo.errors import ConnectionFailure + # Case 1: Successful get -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_get_bids(mock_dbConnection, client): mock_db = mock_dbConnection.return_value - mock_db['bids'].find.return_value = [] + mock_db["bids"].find.return_value = [] - response = client.get('/api/bids') + response = client.get("/api/bids") assert response.status_code == 200 - assert response.get_json() == {'total_count': 0, 'items': []} + assert response.get_json() == {"total_count": 0, "items": []} + # Case 2: Connection error -@patch('api.controllers.bid_controller.dbConnection', side_effect=Exception) +@patch("api.controllers.bid_controller.dbConnection", side_effect=Exception) def test_get_bids_connection_error(mock_dbConnection, client): - response = client.get('/api/bids') + response = client.get("/api/bids") assert response.status_code == 500 - assert response.get_json() == {"Error": "Could not connect to database"} \ No newline at end of file + assert response.get_json() == {"Error": "Could not connect to database"} diff --git a/tests/test_post_bid.py b/tests/test_post_bid.py index be5aa6d..f07c3e2 100644 --- a/tests/test_post_bid.py +++ b/tests/test_post_bid.py @@ -1,8 +1,9 @@ from unittest.mock import patch from pymongo.errors import ConnectionFailure + # Case 1: Successful post -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_post_is_successful(mock_dbConnection, client): data = { "tender": "Business Intelligence and Data Warehousing", @@ -12,64 +13,62 @@ def test_post_is_successful(mock_dbConnection, client): "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": { "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", }, - "success": [ - { - "phase": 1, - "has_score": True, - "out_of": 36, - "score": 30 - } - ], - "failed": { - "phase": 2, - "has_score": True, - "score": 20, - "out_of": 36 - } + "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], + "failed": {"phase": 2, "has_score": True, "score": 20, "out_of": 36}, } # Mock the behavior of dbConnection mock_db = mock_dbConnection.return_value - mock_db['bids'].insert_one.return_value = data + mock_db["bids"].insert_one.return_value = data response = client.post("api/bids", json=data) assert response.status_code == 201 - assert "_id" in response.get_json() and response.get_json()["_id"] is not None - assert "tender" in response.get_json() and response.get_json()["tender"] == "Business Intelligence and Data Warehousing" - assert "client" in response.get_json() and response.get_json()["client"] == "Office for National Statistics" - assert "last_updated" in response.get_json() and response.get_json()["last_updated"] is not None - assert "bid_date" in response.get_json() and response.get_json()["bid_date"] == "2023-06-21" + assert "_id" in response.get_json() and response.get_json()["_id"] is not None + assert ( + "tender" in response.get_json() + and response.get_json()["tender"] + == "Business Intelligence and Data Warehousing" + ) + assert ( + "client" in response.get_json() + and response.get_json()["client"] == "Office for National Statistics" + ) + assert ( + "last_updated" in response.get_json() + and response.get_json()["last_updated"] is not None + ) + assert ( + "bid_date" in response.get_json() + and response.get_json()["bid_date"] == "2023-06-21" + ) # Case 2: Missing mandatory fields def test_field_missing(client): - data = { - "client": "Sample Client", - "bid_date": "20-06-2023" - } - + data = {"client": "Sample Client", "bid_date": "20-06-2023"} + response = client.post("api/bids", json=data) assert response.status_code == 400 assert response.get_json() == { - 'Error': "{'tender': {'message': 'Missing mandatory field'}}" + "Error": "{'tender': {'message': 'Missing mandatory field'}}" } # Case 3: Connection error -@patch('api.controllers.bid_controller.dbConnection', side_effect=ConnectionFailure) +@patch("api.controllers.bid_controller.dbConnection", side_effect=ConnectionFailure) def test_get_bids_connection_error(mock_dbConnection, client): - # Mock the behavior of dbConnection + # Mock the behavior of dbConnection mock_db = mock_dbConnection.return_value - mock_db['bids'].insert_one.side_effect = Exception - response = client.get('/api/bids') + mock_db["bids"].insert_one.side_effect = Exception + response = client.get("/api/bids") assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} # Case 4: Neither success nor failed fields phase can be more than 2 -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_phase_greater_than_2(mock_dbConnection, client): data = { "tender": "Business Intelligence and Data Warehousing", @@ -79,37 +78,25 @@ def test_phase_greater_than_2(mock_dbConnection, client): "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": { "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", }, - "success": [ - { - "phase": 1, - "has_score": True, - "out_of": 36, - "score": 30 - } - ], - "failed": { - "phase": 3, - "has_score": True, - "score": 20, - "out_of": 36 - } + "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], + "failed": {"phase": 3, "has_score": True, "score": 20, "out_of": 36}, } # Mock the behavior of dbConnection mock_db = mock_dbConnection.return_value - mock_db['bids'].insert_one.side_effect = Exception + mock_db["bids"].insert_one.side_effect = Exception response = client.post("api/bids", json=data) assert response.status_code == 400 assert response.get_json() == { - 'Error': "{'failed': {'phase': ['Must be one of: 1, 2.']}}" + "Error": "{'failed': {'phase': ['Must be one of: 1, 2.']}}" } # Case 5: Neither success nor failed fields can have the same phase -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_same_phase(mock_dbConnection, client): data = { "tender": "Business Intelligence and Data Warehousing", @@ -119,37 +106,25 @@ def test_same_phase(mock_dbConnection, client): "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": { "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", }, - "success": [ - { - "phase": 1, - "has_score": True, - "out_of": 36, - "score": 30 - } - ], - "failed": { - "phase": 1, - "has_score": True, - "score": 20, - "out_of": 36 - } + "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], + "failed": {"phase": 1, "has_score": True, "score": 20, "out_of": 36}, } # Mock the behavior of dbConnection mock_db = mock_dbConnection.return_value - mock_db['bids'].insert_one.side_effect = Exception + mock_db["bids"].insert_one.side_effect = Exception response = client.post("api/bids", json=data) assert response.status_code == 400 assert response.get_json() == { - 'Error': "{'success': [\"Phase value already exists in 'failed' section and cannot be repeated.\"]}" + "Error": "{'success': [\"Phase value already exists in 'failed' section and cannot be repeated.\"]}" } # Case 6: Success cannot have the same phase in the list -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_success_same_phase(mock_dbConnection, client): data = { "tender": "Business Intelligence and Data Warehousing", @@ -159,30 +134,20 @@ def test_success_same_phase(mock_dbConnection, client): "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": { "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", }, "success": [ - { - "phase": 1, - "has_score": True, - "out_of": 36, - "score": 30 - }, - { - "phase": 1, - "has_score": True, - "out_of": 50, - "score": 60 - } + {"phase": 1, "has_score": True, "out_of": 36, "score": 30}, + {"phase": 1, "has_score": True, "out_of": 50, "score": 60}, ], } # Mock the behavior of dbConnection mock_db = mock_dbConnection.return_value - mock_db['bids'].insert_one.side_effect = Exception + mock_db["bids"].insert_one.side_effect = Exception response = client.post("api/bids", json=data) assert response.status_code == 400 assert response.get_json() == { - 'Error': "{'success': [\"Phase value already exists in 'success' list and cannot be repeated.\"]}" + "Error": "{'success': [\"Phase value already exists in 'success' list and cannot be repeated.\"]}" } diff --git a/tests/test_update_bid_by_id.py b/tests/test_update_bid_by_id.py index 4fbb1cc..69fde6b 100644 --- a/tests/test_update_bid_by_id.py +++ b/tests/test_update_bid_by_id.py @@ -1,35 +1,33 @@ from unittest.mock import patch + # Case 1: Successful update -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_update_bid_by_id_success(mock_dbConnection, client): mock_db = mock_dbConnection.return_value - mock_db['bids'].find_one_and_update.return_value = { + mock_db["bids"].find_one_and_update.return_value = { "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", "tender": "Business Intelligence and Data Warehousing", "alias": "ONS", "bid_date": "2023-06-23", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "client": "Office for National Statistics", - "was_successful": False + "was_successful": False, } - bid_id = '9f688442-b535-4683-ae1a-a64c1a3b8616' - updated_bid = { - "tender": "UPDATED TENDER" - } + bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" + updated_bid = {"tender": "UPDATED TENDER"} response = client.put(f"api/bids/{bid_id}", json=updated_bid) mock_dbConnection.assert_called_once() - mock_db['bids'].find_one_and_update.assert_called_once() + mock_db["bids"].find_one_and_update.assert_called_once() assert response.status_code == 200 + # Case 2: Invalid user input -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_input_validation(mock_dbConnection, client): - bid_id = '9f688442-b535-4683-ae1a-a64c1a3b8616' - updated_bid = { - "tender": 42 - } + bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" + updated_bid = {"tender": 42} response = client.put(f"api/bids/{bid_id}", json=updated_bid) assert response.status_code == 400 - assert response.get_json()["Error"] == "{'tender': ['Not a valid string.']}" \ No newline at end of file + assert response.get_json()["Error"] == "{'tender': ['Not a valid string.']}" From fd04b55873c3e2b9bebf84f1227f4ed3cc3569e4 Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 20 Jul 2023 12:44:06 +0100 Subject: [PATCH 82/97] feat: added prepend host to controller methods --- Makefile | 2 +- api/controllers/bid_controller.py | 9 ++++++- helpers/helpers.py | 4 +-- request_examples/get_all.http | 2 +- tests/test_get_bid_by_id.py | 26 ++++++++++++++++--- ..._host.py => test_prepend_host_to_links.py} | 8 +++--- 6 files changed, 39 insertions(+), 12 deletions(-) rename tests/{test_prepend_host.py => test_prepend_host_to_links.py} (71%) diff --git a/Makefile b/Makefile index 38ec23c..1b505fb 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ run: venv $(PYTHON) app.py test: venv - $(PYTHON) -m pytest + $(PYTHON) -m pytest -vv commit: @echo "Available topics:" diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 93426a5..2dbe077 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -3,7 +3,7 @@ from marshmallow import ValidationError from api.models.status_enum import Status from dbconfig.mongo_setup import dbConnection -from helpers.helpers import showInternalServerError, showNotFoundError, showValidationError, validate_and_create_bid_document, validate_bid_id_path, validate_bid_update +from helpers.helpers import showInternalServerError, showNotFoundError, showValidationError, validate_and_create_bid_document, validate_bid_id_path, validate_bid_update, prepend_host_to_links bid = Blueprint('bid', __name__) @@ -13,6 +13,9 @@ def get_bids(): try: db = dbConnection() data = list(db['bids'].find({"status": {"$ne": Status.DELETED.value}})) + hostname = request.headers.get("host") + for resource in data: + prepend_host_to_links(resource, hostname) return {'total_count': len(data), 'items': data}, 200 except Exception: return showInternalServerError(), 500 @@ -42,6 +45,10 @@ def get_bid_by_id(bid_id): # Return 404 response if not found / returns None if data is None: return showNotFoundError(), 404 + # Get hostname from request headers + hostname = request.headers.get("host") + # print(data, hostname) + data = prepend_host_to_links(data, hostname) return data, 200 # Return 400 if bid_id is invalid except ValidationError as e: diff --git a/helpers/helpers.py b/helpers/helpers.py index a4c13da..c977296 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -44,8 +44,8 @@ def validate_bid_update(user_request): data = BidSchema().load(user_request, partial=True) return data -def prepend_host(resource, hostname): - host = f"http//{hostname}" +def prepend_host_to_links(resource, hostname): + host = f"http://{hostname}" for key in resource["links"]: resource["links"][key] = f'{host}{resource["links"][key]}' return resource \ No newline at end of file diff --git a/request_examples/get_all.http b/request_examples/get_all.http index 712676d..68f17b1 100644 --- a/request_examples/get_all.http +++ b/request_examples/get_all.http @@ -1 +1 @@ -GET http://localhost:8080/api/bids HTTP/1.1 +GET http://localhost:8080/api/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301 HTTP/1.1 diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index f979d02..2335137 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -8,17 +8,37 @@ def test_get_bid_by_id_success(mock_dbConnection, client): mock_db = mock_dbConnection.return_value mock_db['bids'].find_one.return_value = { '_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', - 'tender': 'Business Intelligence and Data Warehousing' + "alias": "ONS", + "bid_date": "2023-06-23", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "Office for National Statistics", + "links": { + "questions": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301/questions", + "self": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301" + }, + "status": "in_progress", + "tender": "Business Intelligence and Data Warehousing", + "was_successful": False } - response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9', headers={"host": "localhost:8080"}) mock_dbConnection.assert_called_once() mock_db['bids'].find_one.assert_called_once_with({'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'status': {'$ne': 'deleted'}}) assert response.status_code == 200 assert response.get_json() == { '_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', - 'tender': 'Business Intelligence and Data Warehousing' + "alias": "ONS", + "bid_date": "2023-06-23", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "Office for National Statistics", + "links": { + "questions": "http://localhost:8080/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301/questions", + "self": "http://localhost:8080/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301" + }, + "status": "in_progress", + "tender": "Business Intelligence and Data Warehousing", + "was_successful": False } # Case 2: Connection error diff --git a/tests/test_prepend_host.py b/tests/test_prepend_host_to_links.py similarity index 71% rename from tests/test_prepend_host.py rename to tests/test_prepend_host_to_links.py index 2986e33..a67c1f6 100644 --- a/tests/test_prepend_host.py +++ b/tests/test_prepend_host_to_links.py @@ -1,4 +1,4 @@ -from helpers.helpers import prepend_host +from helpers.helpers import prepend_host_to_links def test_prepend_host(): resource = { @@ -18,9 +18,9 @@ def test_prepend_host(): } hostname = "localhost:8080" - result = prepend_host(resource, hostname) + result = prepend_host_to_links(resource, hostname) assert result["links"] == { - "questions": "http//localhost:8080/bids/9f688442-b535-4683-ae1a-a64c1a3b8616/questions", - "self": "http//localhost:8080/bids/9f688442-b535-4683-ae1a-a64c1a3b8616" + "questions": "http://localhost:8080/bids/9f688442-b535-4683-ae1a-a64c1a3b8616/questions", + "self": "http://localhost:8080/bids/9f688442-b535-4683-ae1a-a64c1a3b8616" } \ No newline at end of file From 97b4b252ae609f10599053b2b28e12245a619fb5 Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 20 Jul 2023 12:53:51 +0100 Subject: [PATCH 83/97] refactor: formatted with black --- api/controllers/bid_controller.py | 45 +++++++-- api/models/bid_model.py | 21 +++- api/models/links_model.py | 2 +- api/models/status_enum.py | 3 +- api/schemas/bid_request_schema.py | 35 +++++-- api/schemas/bid_schema.py | 3 +- api/schemas/feedback_schema.py | 3 +- api/schemas/links_schema.py | 3 +- api/schemas/phase_schema.py | 2 + api/schemas/valid_bid_id_schema.py | 5 +- app.py | 15 ++- dbconfig/mongo_setup.py | 2 +- helpers/helpers.py | 15 ++- tests/conftest.py | 5 +- tests/test_BidRequestSchema.py | 45 ++++----- tests/test_delete_bid.py | 23 +++-- tests/test_get_bid_by_id.py | 51 ++++++---- tests/test_get_bids.py | 16 +-- tests/test_post_bid.py | 149 ++++++++++------------------ tests/test_prepend_host_to_links.py | 33 +++--- tests/test_update_bid_by_id.py | 26 +++-- 21 files changed, 267 insertions(+), 235 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 2dbe077..b2fe03b 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -3,23 +3,33 @@ from marshmallow import ValidationError from api.models.status_enum import Status from dbconfig.mongo_setup import dbConnection -from helpers.helpers import showInternalServerError, showNotFoundError, showValidationError, validate_and_create_bid_document, validate_bid_id_path, validate_bid_update, prepend_host_to_links +from helpers.helpers import ( + showInternalServerError, + showNotFoundError, + showValidationError, + validate_and_create_bid_document, + validate_bid_id_path, + validate_bid_update, + prepend_host_to_links, +) + +bid = Blueprint("bid", __name__) -bid = Blueprint('bid', __name__) @bid.route("/bids", methods=["GET"]) def get_bids(): # Get all bids from database collection try: db = dbConnection() - data = list(db['bids'].find({"status": {"$ne": Status.DELETED.value}})) + data = list(db["bids"].find({"status": {"$ne": Status.DELETED.value}})) hostname = request.headers.get("host") for resource in data: prepend_host_to_links(resource, hostname) - return {'total_count': len(data), 'items': data}, 200 + return {"total_count": len(data), "items": data}, 200 except Exception: return showInternalServerError(), 500 - + + @bid.route("/bids", methods=["POST"]) def post_bid(): try: @@ -27,7 +37,7 @@ def post_bid(): # Process input and create data model data = validate_and_create_bid_document(request.get_json()) # Insert document into database collection - db['bids'].insert_one(data) + db["bids"].insert_one(data) return data, 201 # Return 400 response if input validation fails except ValidationError as e: @@ -35,13 +45,16 @@ def post_bid(): # Return 500 response in case of connection failure except Exception: return showInternalServerError(), 500 - + + @bid.route("/bids/", methods=["GET"]) def get_bid_by_id(bid_id): try: bid_id = validate_bid_id_path(bid_id) db = dbConnection() - data = db['bids'].find_one({"_id": bid_id , "status": {"$ne": Status.DELETED.value}}) + data = db["bids"].find_one( + {"_id": bid_id, "status": {"$ne": Status.DELETED.value}} + ) # Return 404 response if not found / returns None if data is None: return showNotFoundError(), 404 @@ -57,6 +70,7 @@ def get_bid_by_id(bid_id): except Exception: return showInternalServerError(), 500 + @bid.route("/bids/", methods=["PUT"]) def update_bid_by_id(bid_id): try: @@ -64,7 +78,9 @@ def update_bid_by_id(bid_id): user_request = validate_bid_update(request.get_json()) # Updates document where id is equal to bid_id db = dbConnection() - data = db['bids'].find_one_and_update({"_id": bid_id}, {"$set": user_request}, return_document=True) + data = db["bids"].find_one_and_update( + {"_id": bid_id}, {"$set": user_request}, return_document=True + ) # Return 404 response if not found / returns None if data is None: return showNotFoundError(), 404 @@ -76,12 +92,21 @@ def update_bid_by_id(bid_id): except Exception: return showInternalServerError(), 500 + @bid.route("/bids/", methods=["DELETE"]) def change_status_to_deleted(bid_id): try: bid_id = validate_bid_id_path(bid_id) db = dbConnection() - data = db['bids'].find_one_and_update({"_id": bid_id, "status": {"$ne": Status.DELETED.value}}, {"$set": {"status": Status.DELETED.value, "last_updated": datetime.now().isoformat()}}) + data = db["bids"].find_one_and_update( + {"_id": bid_id, "status": {"$ne": Status.DELETED.value}}, + { + "$set": { + "status": Status.DELETED.value, + "last_updated": datetime.now().isoformat(), + } + }, + ) if data is None: return showNotFoundError(), 404 return data, 204 diff --git a/api/models/bid_model.py b/api/models/bid_model.py index 772b73c..3e13bbf 100644 --- a/api/models/bid_model.py +++ b/api/models/bid_model.py @@ -3,19 +3,32 @@ from .links_model import LinksModel from api.models.status_enum import Status + # Description: Schema for the bid object -class BidModel(): - def __init__(self, tender, client, bid_date, alias=None, bid_folder_url=None, feedback=None, failed=None, was_successful=False, success=[], status=Status.IN_PROGRESS): +class BidModel: + def __init__( + self, + tender, + client, + bid_date, + alias=None, + bid_folder_url=None, + feedback=None, + failed=None, + was_successful=False, + success=[], + status=Status.IN_PROGRESS, + ): self._id = uuid4() self.tender = tender self.client = client self.alias = alias self.bid_date = bid_date self.bid_folder_url = bid_folder_url - self.status = status # enum: "deleted", "in_progress" or "completed" + self.status = status # enum: "deleted", "in_progress" or "completed" self.links = LinksModel(self._id) self.was_successful = was_successful - self.success = success + self.success = success self.failed = failed self.feedback = feedback self.last_updated = datetime.now() diff --git a/api/models/links_model.py b/api/models/links_model.py index 83f3827..4a3535b 100644 --- a/api/models/links_model.py +++ b/api/models/links_model.py @@ -1,5 +1,5 @@ # Schema for links object -class LinksModel(): +class LinksModel: def __init__(self, id): self.self = f"/bids/{id}" self.questions = f"/bids/{id}/questions" diff --git a/api/models/status_enum.py b/api/models/status_enum.py index 592e02a..6fa7dd5 100644 --- a/api/models/status_enum.py +++ b/api/models/status_enum.py @@ -1,8 +1,9 @@ from enum import Enum, unique + # Enum for status @unique class Status(Enum): DELETED = "deleted" IN_PROGRESS = "in_progress" - COMPLETED = "completed" \ No newline at end of file + COMPLETED = "completed" diff --git a/api/schemas/bid_request_schema.py b/api/schemas/bid_request_schema.py index f0421c8..19b098c 100644 --- a/api/schemas/bid_request_schema.py +++ b/api/schemas/bid_request_schema.py @@ -3,18 +3,29 @@ from .phase_schema import PhaseSchema from .feedback_schema import FeedbackSchema + # Marshmallow schema for request body class BidRequestSchema(Schema): - tender = fields.Str(required=True, error_messages={"required": {"message": "Missing mandatory field"}}) - client = fields.Str(required=True, error_messages={"required": {"message": "Missing mandatory field"}}) + tender = fields.Str( + required=True, + error_messages={"required": {"message": "Missing mandatory field"}}, + ) + client = fields.Str( + required=True, + error_messages={"required": {"message": "Missing mandatory field"}}, + ) alias = fields.Str() - bid_date = fields.Date(format='%d-%m-%Y', required=True, error_messages={"required": {"message": "Missing mandatory field"}}) + bid_date = fields.Date( + format="%d-%m-%Y", + required=True, + error_messages={"required": {"message": "Missing mandatory field"}}, + ) bid_folder_url = fields.URL() was_successful = fields.Boolean() success = fields.List(fields.Nested(PhaseSchema)) failed = fields.Nested(PhaseSchema) feedback = fields.Nested(FeedbackSchema) - + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.context["failed_phase_values"] = set() @@ -25,14 +36,18 @@ def validate_success(self, value): for phase in value: phase_value = phase.get("phase") if phase_value in phase_values: - raise ValidationError("Phase value already exists in 'success' list and cannot be repeated.") + raise ValidationError( + "Phase value already exists in 'success' list and cannot be repeated." + ) phase_values.add(phase_value) @validates("failed") def validate_failed(self, value): phase_value = value.get("phase") if phase_value in self.context.get("success_phase_values", set()): - raise ValidationError("Phase value already exists in 'success' list and cannot be repeated.") + raise ValidationError( + "Phase value already exists in 'success' list and cannot be repeated." + ) self.context["failed_phase_values"].add(phase_value) @validates("success") @@ -42,10 +57,12 @@ def validate_success_and_failed(self, value): for phase in value: phase_value = phase.get("phase") if phase_value in failed_phase_values: - raise ValidationError("Phase value already exists in 'failed' section and cannot be repeated.") + raise ValidationError( + "Phase value already exists in 'failed' section and cannot be repeated." + ) success_phase_values.add(phase_value) - + # Creates a Bid instance after processing @post_load def makeBid(self, data, **kwargs): - return BidModel(**data) \ No newline at end of file + return BidModel(**data) diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py index 139a3b4..4bfc935 100644 --- a/api/schemas/bid_schema.py +++ b/api/schemas/bid_schema.py @@ -5,6 +5,7 @@ from .feedback_schema import FeedbackSchema from ..models.status_enum import Status + # Marshmallow schema class BidSchema(Schema): _id = fields.UUID(required=True) @@ -24,4 +25,4 @@ class BidSchema(Schema): @post_load def set_last_updated(self, data, **kwargs): data["last_updated"] = datetime.now().isoformat() - return data \ No newline at end of file + return data diff --git a/api/schemas/feedback_schema.py b/api/schemas/feedback_schema.py index ec24452..a6aa08a 100644 --- a/api/schemas/feedback_schema.py +++ b/api/schemas/feedback_schema.py @@ -1,5 +1,6 @@ from marshmallow import Schema, fields + class FeedbackSchema(Schema): description = fields.Str(required=True) - url = fields.URL(required=True) \ No newline at end of file + url = fields.URL(required=True) diff --git a/api/schemas/links_schema.py b/api/schemas/links_schema.py index 4a711e2..57bfacd 100644 --- a/api/schemas/links_schema.py +++ b/api/schemas/links_schema.py @@ -1,5 +1,6 @@ from marshmallow import Schema, fields + class LinksSchema(Schema): self = fields.Str(required=True) - questions = fields.Str(required=True) \ No newline at end of file + questions = fields.Str(required=True) diff --git a/api/schemas/phase_schema.py b/api/schemas/phase_schema.py index c723821..259a437 100644 --- a/api/schemas/phase_schema.py +++ b/api/schemas/phase_schema.py @@ -1,11 +1,13 @@ from marshmallow import Schema, fields, validates_schema, ValidationError from enum import Enum, unique + @unique class Phase(Enum): PHASE_1 = 1 PHASE_2 = 2 + class PhaseSchema(Schema): phase = fields.Enum(Phase, required=True, by_value=True) has_score = fields.Boolean(required=True) diff --git a/api/schemas/valid_bid_id_schema.py b/api/schemas/valid_bid_id_schema.py index c91c83d..acf5862 100644 --- a/api/schemas/valid_bid_id_schema.py +++ b/api/schemas/valid_bid_id_schema.py @@ -1,4 +1,7 @@ from marshmallow import Schema, fields, validate + class valid_bid_id_schema(Schema): - bid_id = fields.Str(required=True, validate=validate.Length(min=36, error="Invalid bid Id")) \ No newline at end of file + bid_id = fields.Str( + required=True, validate=validate.Length(min=36, error="Invalid bid Id") + ) diff --git a/app.py b/app.py index 3ce7ba0..0c12a5d 100644 --- a/app.py +++ b/app.py @@ -5,20 +5,19 @@ app = Flask(__name__) -SWAGGER_URL = '/api/docs' # URL for exposing Swagger UI -API_URL = '/static/swagger_config.yml' # Our API url +SWAGGER_URL = "/api/docs" # URL for exposing Swagger UI +API_URL = "/static/swagger_config.yml" # Our API url # Call factory function to create our blueprint swaggerui_blueprint = get_swaggerui_blueprint( SWAGGER_URL, # Swagger UI static files will be mapped to '{SWAGGER_URL}/dist/' API_URL, - config={ # Swagger UI config overrides - 'app_name': "Bids API Swagger" - }) + config={"app_name": "Bids API Swagger"}, # Swagger UI config overrides +) app.register_blueprint(swaggerui_blueprint) -app.register_blueprint(bid, url_prefix='/api') +app.register_blueprint(bid, url_prefix="/api") -if __name__ == '__main__': - app.run(debug=True, port=8080) \ No newline at end of file +if __name__ == "__main__": + app.run(debug=True, port=8080) diff --git a/dbconfig/mongo_setup.py b/dbconfig/mongo_setup.py index 97a116d..d49f1b5 100644 --- a/dbconfig/mongo_setup.py +++ b/dbconfig/mongo_setup.py @@ -2,9 +2,9 @@ MONGO_URI = "mongodb://localhost:27017/bidsAPI" + # Create a new client and connect to the server def dbConnection(): client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=10000) db = client["bidsAPI"] return db - \ No newline at end of file diff --git a/helpers/helpers.py b/helpers/helpers.py index c977296..f762ad2 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -5,29 +5,35 @@ from api.schemas.bid_request_schema import BidRequestSchema from api.schemas.valid_bid_id_schema import valid_bid_id_schema + def showInternalServerError(): return jsonify({"Error": "Could not connect to database"}) + def showNotFoundError(): return jsonify({"Error": "Resource not found"}) + def showValidationError(e): return jsonify({"Error": str(e)}) + def is_valid_uuid(string): try: uuid.UUID(str(string)) return True except ValueError: return False - + + def is_valid_isoformat(string): try: datetime.fromisoformat(string) return True except: return False - + + def validate_and_create_bid_document(request): # Process input and create data model bid_document = BidRequestSchema().load(request) @@ -35,17 +41,20 @@ def validate_and_create_bid_document(request): data = BidSchema().dump(bid_document) return data + def validate_bid_id_path(bid_id): valid_bid_id = valid_bid_id_schema().load({"bid_id": bid_id}) data = valid_bid_id["bid_id"] return data + def validate_bid_update(user_request): data = BidSchema().load(user_request, partial=True) return data + def prepend_host_to_links(resource, hostname): host = f"http://{hostname}" for key in resource["links"]: resource["links"][key] = f'{host}{resource["links"][key]}' - return resource \ No newline at end of file + return resource diff --git a/tests/conftest.py b/tests/conftest.py index 3c706e5..8aa7aa7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,9 +2,10 @@ from flask import Flask from api.controllers.bid_controller import bid + @pytest.fixture def client(): app = Flask(__name__) - app.register_blueprint(bid, url_prefix='/api') + app.register_blueprint(bid, url_prefix="/api") with app.test_client() as client: - yield client \ No newline at end of file + yield client diff --git a/tests/test_BidRequestSchema.py b/tests/test_BidRequestSchema.py index ac01665..b8fa4b9 100644 --- a/tests/test_BidRequestSchema.py +++ b/tests/test_BidRequestSchema.py @@ -4,6 +4,7 @@ from api.schemas.bid_request_schema import BidRequestSchema from helpers.helpers import is_valid_uuid, is_valid_isoformat + def test_bid_model(): data = { "tender": "Business Intelligence and Data Warehousing", @@ -13,22 +14,10 @@ def test_bid_model(): "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": { "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", }, - "success": [ - { - "phase": 1, - "has_score": True, - "out_of": 36, - "score": 30 - } - ], - "failed": { - "phase": 2, - "has_score": True, - "score": 20, - "out_of": 36 - } + "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], + "failed": {"phase": 2, "has_score": True, "score": 20, "out_of": 36}, } bid_document = BidRequestSchema().load(data) to_post = BidSchema().dump(bid_document) @@ -39,12 +28,12 @@ def test_bid_model(): assert is_valid_uuid(id) is True # Test UUID validator assert is_valid_uuid("99999") is False - + # Test that links object is generated and URLs are correct assert to_post["links"] is not None assert "self" in to_post["links"] assert to_post["links"]["self"] == f"/bids/{id}" - assert 'questions' in to_post["links"] + assert "questions" in to_post["links"] assert to_post["links"]["questions"] == f"/bids/{id}/questions" # Test that status is set to in_progress @@ -56,44 +45,49 @@ def test_bid_model(): # Test ISOformat validator assert is_valid_isoformat("07-06-2023") is False + def test_validate_tender(): data = { "tender": 42, "client": "Office for National Statistics", - "bid_date": "21-06-2023" + "bid_date": "21-06-2023", } with pytest.raises(ValidationError): BidRequestSchema().load(data) - + + def test_validate_client(): data = { "tender": "Business Intelligence and Data Warehousing", "client": 42, - "bid_date": "21-06-2023" + "bid_date": "21-06-2023", } with pytest.raises(ValidationError): BidRequestSchema().load(data) + def test_validate_bid_date(): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", - "bid_date": "2023-12-25" + "bid_date": "2023-12-25", } with pytest.raises(ValidationError): BidRequestSchema().load(data) + def test_validate_bid_folder_url(): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", "bid_date": "21-06-2023", - "bid_folder_url": "Not a valid URL" + "bid_folder_url": "Not a valid URL", } with pytest.raises(ValidationError): BidRequestSchema().load(data) + def test_validate_feedback(): data = { "tender": "Business Intelligence and Data Warehousing", @@ -101,11 +95,8 @@ def test_validate_feedback(): "bid_date": "21-06-2023", "alias": "ONS", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "feedback": { - "description": 42, - "url": "Invalid URL" - } + "feedback": {"description": 42, "url": "Invalid URL"}, } with pytest.raises(ValidationError): - BidRequestSchema().load(data) \ No newline at end of file + BidRequestSchema().load(data) diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py index f31e993..81be726 100644 --- a/tests/test_delete_bid.py +++ b/tests/test_delete_bid.py @@ -2,31 +2,34 @@ from unittest.mock import patch from marshmallow import ValidationError + # Case 1: Successful delete a bid by changing status to deleted -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_delete_bid_success(mock_dbConnection, client): mock_db = mock_dbConnection.return_value - mock_db['bids'].find_one_and_update.return_value = { + mock_db["bids"].find_one_and_update.return_value = { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", - "status": "deleted" + "status": "deleted", } - response = client.delete('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + response = client.delete("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") assert response.status_code == 204 assert response.content_length is None + # Case 2: Failed to call database -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_delete_bid_find_error(mock_dbConnection, client): mock_db = mock_dbConnection.return_value - mock_db['bids'].find_one_and_update.side_effect = ConnectionFailure - response = client.delete('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + mock_db["bids"].find_one_and_update.side_effect = ConnectionFailure + response = client.delete("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} + # Case 3: Validation error -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_get_bid_by_id_validation_error(mock_dbConnection, client): mock_dbConnection.side_effect = ValidationError - response = client.delete('/api/bids/invalid_bid_id') + response = client.delete("/api/bids/invalid_bid_id") assert response.status_code == 400 - assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} \ No newline at end of file + assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index 2335137..58a33d2 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -2,69 +2,80 @@ from pymongo.errors import ConnectionFailure from marshmallow import ValidationError + # Case 1: Successful get_bid_by_id -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_get_bid_by_id_success(mock_dbConnection, client): mock_db = mock_dbConnection.return_value - mock_db['bids'].find_one.return_value = { - '_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', + mock_db["bids"].find_one.return_value = { + "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "alias": "ONS", "bid_date": "2023-06-23", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "client": "Office for National Statistics", "links": { "questions": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301/questions", - "self": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301" - }, + "self": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301", + }, "status": "in_progress", "tender": "Business Intelligence and Data Warehousing", - "was_successful": False + "was_successful": False, } - response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9', headers={"host": "localhost:8080"}) + response = client.get( + "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", + headers={"host": "localhost:8080"}, + ) mock_dbConnection.assert_called_once() - mock_db['bids'].find_one.assert_called_once_with({'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'status': {'$ne': 'deleted'}}) + mock_db["bids"].find_one.assert_called_once_with( + {"_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "status": {"$ne": "deleted"}} + ) assert response.status_code == 200 assert response.get_json() == { - '_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', + "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "alias": "ONS", "bid_date": "2023-06-23", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "client": "Office for National Statistics", "links": { "questions": "http://localhost:8080/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301/questions", - "self": "http://localhost:8080/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301" - }, + "self": "http://localhost:8080/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301", + }, "status": "in_progress", "tender": "Business Intelligence and Data Warehousing", - "was_successful": False + "was_successful": False, } + # Case 2: Connection error -@patch('api.controllers.bid_controller.dbConnection', side_effect=ConnectionFailure) +@patch("api.controllers.bid_controller.dbConnection", side_effect=ConnectionFailure) def test_get_bids_connection_error(mock_dbConnection, client): - response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + response = client.get("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} + # Case 3: Bid not found -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_get_bid_by_id_not_found(mock_dbConnection, client): mock_db = mock_dbConnection.return_value - mock_db['bids'].find_one.return_value = None + mock_db["bids"].find_one.return_value = None - response = client.get('/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9') + response = client.get("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") mock_dbConnection.assert_called_once() - mock_db['bids'].find_one.assert_called_once_with({'_id': '1ff45b42-b72a-464c-bde9-9bead14a07b9', 'status': {'$ne': 'deleted'}}) + mock_db["bids"].find_one.assert_called_once_with( + {"_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "status": {"$ne": "deleted"}} + ) assert response.status_code == 404 assert response.get_json() == {"Error": "Resource not found"} + # Case 4: Validation error -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_get_bid_by_id_validation_error(mock_dbConnection, client): mock_dbConnection.side_effect = ValidationError - response = client.get('/api/bids/invalid_bid_id') + response = client.get("/api/bids/invalid_bid_id") assert response.status_code == 400 assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} diff --git a/tests/test_get_bids.py b/tests/test_get_bids.py index 10a99f1..0658fd8 100644 --- a/tests/test_get_bids.py +++ b/tests/test_get_bids.py @@ -1,19 +1,21 @@ from unittest.mock import patch from pymongo.errors import ConnectionFailure + # Case 1: Successful get -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_get_bids(mock_dbConnection, client): mock_db = mock_dbConnection.return_value - mock_db['bids'].find.return_value = [] + mock_db["bids"].find.return_value = [] - response = client.get('/api/bids') + response = client.get("/api/bids") assert response.status_code == 200 - assert response.get_json() == {'total_count': 0, 'items': []} + assert response.get_json() == {"total_count": 0, "items": []} + # Case 2: Connection error -@patch('api.controllers.bid_controller.dbConnection', side_effect=Exception) +@patch("api.controllers.bid_controller.dbConnection", side_effect=Exception) def test_get_bids_connection_error(mock_dbConnection, client): - response = client.get('/api/bids') + response = client.get("/api/bids") assert response.status_code == 500 - assert response.get_json() == {"Error": "Could not connect to database"} \ No newline at end of file + assert response.get_json() == {"Error": "Could not connect to database"} diff --git a/tests/test_post_bid.py b/tests/test_post_bid.py index f66e6a5..e6be6b3 100644 --- a/tests/test_post_bid.py +++ b/tests/test_post_bid.py @@ -1,8 +1,9 @@ from unittest.mock import patch from pymongo.errors import ConnectionFailure + # Case 1: Successful post -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_post_is_successful(mock_dbConnection, client): data = { "tender": "Business Intelligence and Data Warehousing", @@ -12,53 +13,51 @@ def test_post_is_successful(mock_dbConnection, client): "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": { "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", }, - "success": [ - { - "phase": 1, - "has_score": True, - "out_of": 36, - "score": 30 - } - ], - "failed": { - "phase": 2, - "has_score": True, - "score": 20, - "out_of": 36 - } + "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], + "failed": {"phase": 2, "has_score": True, "score": 20, "out_of": 36}, } # Mock the behavior of dbConnection mock_db = mock_dbConnection.return_value - mock_db['bids'].insert_one.return_value = data + mock_db["bids"].insert_one.return_value = data response = client.post("api/bids", json=data) assert response.status_code == 201 - assert "_id" in response.get_json() and response.get_json()["_id"] is not None - assert "tender" in response.get_json() and response.get_json()["tender"] == "Business Intelligence and Data Warehousing" - assert "client" in response.get_json() and response.get_json()["client"] == "Office for National Statistics" - assert "last_updated" in response.get_json() and response.get_json()["last_updated"] is not None - assert "bid_date" in response.get_json() and response.get_json()["bid_date"] == "2023-06-21" + assert "_id" in response.get_json() and response.get_json()["_id"] is not None + assert ( + "tender" in response.get_json() + and response.get_json()["tender"] + == "Business Intelligence and Data Warehousing" + ) + assert ( + "client" in response.get_json() + and response.get_json()["client"] == "Office for National Statistics" + ) + assert ( + "last_updated" in response.get_json() + and response.get_json()["last_updated"] is not None + ) + assert ( + "bid_date" in response.get_json() + and response.get_json()["bid_date"] == "2023-06-21" + ) # Case 2: Missing mandatory fields def test_field_missing(client): - data = { - "client": "Sample Client", - "bid_date": "20-06-2023" - } - + data = {"client": "Sample Client", "bid_date": "20-06-2023"} + response = client.post("api/bids", json=data) assert response.status_code == 400 assert response.get_json() == { - 'Error': "{'tender': {'message': 'Missing mandatory field'}}" + "Error": "{'tender': {'message': 'Missing mandatory field'}}" } # Case 3: Connection error -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_post_bid_connection_error(mock_dbConnection, client): data = { "tender": "Business Intelligence and Data Warehousing", @@ -68,33 +67,21 @@ def test_post_bid_connection_error(mock_dbConnection, client): "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": { "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", }, - "success": [ - { - "phase": 1, - "has_score": True, - "out_of": 36, - "score": 30 - } - ], - "failed": { - "phase": 2, - "has_score": True, - "score": 20, - "out_of": 36 - } + "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], + "failed": {"phase": 2, "has_score": True, "score": 20, "out_of": 36}, } # Mock the behavior of dbConnection mock_db = mock_dbConnection.return_value - mock_db['bids'].insert_one.side_effect = ConnectionFailure - response = client.post('/api/bids', json=data) + mock_db["bids"].insert_one.side_effect = ConnectionFailure + response = client.post("/api/bids", json=data) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} # Case 4: Neither success nor failed fields phase can be more than 2 -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_phase_greater_than_2(mock_dbConnection, client): data = { "tender": "Business Intelligence and Data Warehousing", @@ -104,33 +91,21 @@ def test_phase_greater_than_2(mock_dbConnection, client): "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": { "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", }, - "success": [ - { - "phase": 1, - "has_score": True, - "out_of": 36, - "score": 30 - } - ], - "failed": { - "phase": 3, - "has_score": True, - "score": 20, - "out_of": 36 - } + "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], + "failed": {"phase": 3, "has_score": True, "score": 20, "out_of": 36}, } response = client.post("api/bids", json=data) assert response.status_code == 400 assert response.get_json() == { - 'Error': "{'failed': {'phase': ['Must be one of: 1, 2.']}}" - } + "Error": "{'failed': {'phase': ['Must be one of: 1, 2.']}}" + } # Case 5: Neither success nor failed fields can have the same phase -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_same_phase(mock_dbConnection, client): data = { "tender": "Business Intelligence and Data Warehousing", @@ -140,33 +115,21 @@ def test_same_phase(mock_dbConnection, client): "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": { "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", }, - "success": [ - { - "phase": 1, - "has_score": True, - "out_of": 36, - "score": 30 - } - ], - "failed": { - "phase": 1, - "has_score": True, - "score": 20, - "out_of": 36 - } + "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], + "failed": {"phase": 1, "has_score": True, "score": 20, "out_of": 36}, } response = client.post("api/bids", json=data) assert response.status_code == 400 assert response.get_json() == { - 'Error': "{'success': [\"Phase value already exists in 'failed' section and cannot be repeated.\"]}" - } + "Error": "{'success': [\"Phase value already exists in 'failed' section and cannot be repeated.\"]}" + } # Case 6: Success cannot have the same phase in the list -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_success_same_phase(mock_dbConnection, client): data = { "tender": "Business Intelligence and Data Warehousing", @@ -176,26 +139,16 @@ def test_success_same_phase(mock_dbConnection, client): "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": { "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", }, "success": [ - { - "phase": 1, - "has_score": True, - "out_of": 36, - "score": 30 - }, - { - "phase": 1, - "has_score": True, - "out_of": 50, - "score": 60 - } + {"phase": 1, "has_score": True, "out_of": 36, "score": 30}, + {"phase": 1, "has_score": True, "out_of": 50, "score": 60}, ], } response = client.post("api/bids", json=data) assert response.status_code == 400 assert response.get_json() == { - 'Error': "{'success': [\"Phase value already exists in 'success' list and cannot be repeated.\"]}" - } + "Error": "{'success': [\"Phase value already exists in 'success' list and cannot be repeated.\"]}" + } diff --git a/tests/test_prepend_host_to_links.py b/tests/test_prepend_host_to_links.py index a67c1f6..e60f835 100644 --- a/tests/test_prepend_host_to_links.py +++ b/tests/test_prepend_host_to_links.py @@ -1,26 +1,27 @@ from helpers.helpers import prepend_host_to_links + def test_prepend_host(): resource = { - "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", - "alias": "ONS", - "bid_date": "2023-06-23", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "client": "Office for National Statistics", - "last_updated": "2023-07-19T11:15:25.743340", - "links": { - "questions": "/bids/9f688442-b535-4683-ae1a-a64c1a3b8616/questions", - "self": "/bids/9f688442-b535-4683-ae1a-a64c1a3b8616" - }, - "status": "in_progress", - "tender": "Business Intelligence and Data Warehousing", - "was_successful": False + "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", + "alias": "ONS", + "bid_date": "2023-06-23", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "Office for National Statistics", + "last_updated": "2023-07-19T11:15:25.743340", + "links": { + "questions": "/bids/9f688442-b535-4683-ae1a-a64c1a3b8616/questions", + "self": "/bids/9f688442-b535-4683-ae1a-a64c1a3b8616", + }, + "status": "in_progress", + "tender": "Business Intelligence and Data Warehousing", + "was_successful": False, } hostname = "localhost:8080" - + result = prepend_host_to_links(resource, hostname) assert result["links"] == { "questions": "http://localhost:8080/bids/9f688442-b535-4683-ae1a-a64c1a3b8616/questions", - "self": "http://localhost:8080/bids/9f688442-b535-4683-ae1a-a64c1a3b8616" - } \ No newline at end of file + "self": "http://localhost:8080/bids/9f688442-b535-4683-ae1a-a64c1a3b8616", + } diff --git a/tests/test_update_bid_by_id.py b/tests/test_update_bid_by_id.py index 4fbb1cc..69fde6b 100644 --- a/tests/test_update_bid_by_id.py +++ b/tests/test_update_bid_by_id.py @@ -1,35 +1,33 @@ from unittest.mock import patch + # Case 1: Successful update -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_update_bid_by_id_success(mock_dbConnection, client): mock_db = mock_dbConnection.return_value - mock_db['bids'].find_one_and_update.return_value = { + mock_db["bids"].find_one_and_update.return_value = { "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", "tender": "Business Intelligence and Data Warehousing", "alias": "ONS", "bid_date": "2023-06-23", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "client": "Office for National Statistics", - "was_successful": False + "was_successful": False, } - bid_id = '9f688442-b535-4683-ae1a-a64c1a3b8616' - updated_bid = { - "tender": "UPDATED TENDER" - } + bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" + updated_bid = {"tender": "UPDATED TENDER"} response = client.put(f"api/bids/{bid_id}", json=updated_bid) mock_dbConnection.assert_called_once() - mock_db['bids'].find_one_and_update.assert_called_once() + mock_db["bids"].find_one_and_update.assert_called_once() assert response.status_code == 200 + # Case 2: Invalid user input -@patch('api.controllers.bid_controller.dbConnection') +@patch("api.controllers.bid_controller.dbConnection") def test_input_validation(mock_dbConnection, client): - bid_id = '9f688442-b535-4683-ae1a-a64c1a3b8616' - updated_bid = { - "tender": 42 - } + bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" + updated_bid = {"tender": 42} response = client.put(f"api/bids/{bid_id}", json=updated_bid) assert response.status_code == 400 - assert response.get_json()["Error"] == "{'tender': ['Not a valid string.']}" \ No newline at end of file + assert response.get_json()["Error"] == "{'tender': ['Not a valid string.']}" From a77ac56be068afaa2f0934c7b1369f3fee370f63 Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 20 Jul 2023 14:33:01 +0100 Subject: [PATCH 84/97] feat: added test coverage report; updated gmake help --- Makefile | 7 ++++++- api/controllers/bid_controller.py | 1 - api/schemas/bid_request_schema.py | 1 - helpers/helpers.py | 1 - requirements.txt | 1 + tests/test_get_bid_by_id.py | 4 ++-- tests/test_post_bid.py | 3 +-- 7 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index a930eb8..12e954a 100644 --- a/Makefile +++ b/Makefile @@ -22,12 +22,14 @@ run: venv test: venv $(PYTHON) -m pytest -vv + @echo "TEST COVERAGE REPORT" + coverage report -m branch: @echo "Available branch types:" @echo "$(TOPICS)" @read -p "Enter the branch type: " type; \ - read -p "Enter the branch description: " description; \ + read -p "Enter the branch description (kebab-case): " description; \ git checkout -b $${type}/$${description}; \ git push --set-upstream origin $${type}/$${description} @@ -64,7 +66,10 @@ help: @echo "gmake test - run the tests" @echo "gmake clean - remove all generated files" @echo "gmake check - check for security vulnerabilities" + @echo "gmake branch - create and checkout to new branch" @echo "gmake commit - commit changes to git" + @echo "gmake lint - run linters" + @echo "gmake format - format all files in directory" @echo "gmake help - display this help" swagger: venv diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 210a796..b2fe03b 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -11,7 +11,6 @@ validate_bid_id_path, validate_bid_update, prepend_host_to_links, - ) bid = Blueprint("bid", __name__) diff --git a/api/schemas/bid_request_schema.py b/api/schemas/bid_request_schema.py index 77ba440..19b098c 100644 --- a/api/schemas/bid_request_schema.py +++ b/api/schemas/bid_request_schema.py @@ -4,7 +4,6 @@ from .feedback_schema import FeedbackSchema - # Marshmallow schema for request body class BidRequestSchema(Schema): tender = fields.Str( diff --git a/helpers/helpers.py b/helpers/helpers.py index 4a82fff..f762ad2 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -58,4 +58,3 @@ def prepend_host_to_links(resource, hostname): for key in resource["links"]: resource["links"][key] = f'{host}{resource["links"][key]}' return resource - diff --git a/requirements.txt b/requirements.txt index 4d5e92d..db7502d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ pytest flask_swagger_ui marshmallow pymongo +coverage \ No newline at end of file diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index 6643f65..58a33d2 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -24,7 +24,7 @@ def test_get_bid_by_id_success(mock_dbConnection, client): response = client.get( "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", - headers={"host": "localhost:8080"} + headers={"host": "localhost:8080"}, ) mock_dbConnection.assert_called_once() @@ -44,7 +44,7 @@ def test_get_bid_by_id_success(mock_dbConnection, client): }, "status": "in_progress", "tender": "Business Intelligence and Data Warehousing", - "was_successful": False + "was_successful": False, } diff --git a/tests/test_post_bid.py b/tests/test_post_bid.py index 131a2fd..1d3a7ee 100644 --- a/tests/test_post_bid.py +++ b/tests/test_post_bid.py @@ -76,7 +76,7 @@ def test_post_bid_connection_error(mock_dbConnection, client): mock_db = mock_dbConnection.return_value mock_db["bids"].insert_one.side_effect = ConnectionFailure response = client.post("/api/bids", json=data) - + assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} @@ -98,7 +98,6 @@ def test_phase_greater_than_2(mock_dbConnection, client): "failed": {"phase": 3, "has_score": True, "score": 20, "out_of": 36}, } - response = client.post("api/bids", json=data) assert response.status_code == 400 assert response.get_json() == { From 65316f011372457f9020d36aaf7b1f0304fedcaf Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Thu, 20 Jul 2023 17:43:02 +0100 Subject: [PATCH 85/97] feat: added script to populate and delete bids --- Makefile | 85 +++++++++++++++--------- api/controllers/bid_controller.py | 1 - api/schemas/bid_request_schema.py | 1 - dbconfig/mongo_setup.py | 6 +- helpers/helpers.py | 1 - requirements.txt | 1 + scripts/README.md | 0 scripts/create_sample_data.py | 54 ++++++++++++++++ scripts/delete_db.py | 39 +++++++++++ scripts/test_data/bids.json | 104 ++++++++++++++++++++++++++++++ tests/test_get_bid_by_id.py | 4 +- tests/test_post_bid.py | 3 +- 12 files changed, 261 insertions(+), 38 deletions(-) create mode 100644 scripts/README.md create mode 100644 scripts/create_sample_data.py create mode 100644 scripts/delete_db.py create mode 100644 scripts/test_data/bids.json diff --git a/Makefile b/Makefile index a930eb8..613a73c 100644 --- a/Makefile +++ b/Makefile @@ -10,18 +10,24 @@ PIP = ./.venv/bin/pip .PHONY: run test clean check help commit swagger format branch lint -venv/bin/activate: requirements.txt - python3 -m venv .venv - $(PIP) install -r requirements.txt - -venv: venv/bin/activate - . ./.venv/bin/activate - -run: venv - $(PYTHON) app.py +help: + @echo "gmake help - display this help" + @echo "gmake bids - create sample data" + @echo "gmake branch - create a new branch" + @echo "gmake check - check for security vulnerabilities" + @echo "gmake clean - remove all generated files" + @echo "gmake commit - commit changes to git" + @echo "gmake dbclean - clean up the application database" + @echo "gmake format - format the code" + @echo "gmake lint - run linters" + @echo "gmake run - run the application" + @echo "gmake swagger - open swagger documentation" + @echo "gmake setup - setup the application database" + @echo "gmake test - run the tests" -test: venv - $(PYTHON) -m pytest -vv +bids: + @echo "Creating sample data..." + @find . -name "create_sample_data.py" -exec python3 {} \; branch: @echo "Available branch types:" @@ -31,6 +37,16 @@ branch: git checkout -b $${type}/$${description}; \ git push --set-upstream origin $${type}/$${description} +check: venv + $(PIP) install safety + $(PIP) freeze | $(PYTHON) -m safety check --stdin + +clean: + @echo "Cleaning up..." + @find . -name "__pycache__" -type d -exec rm -rf {} + + @find . -name ".pytest_cache" -exec rm -rf {} + + @find . -name ".venv" -exec rm -rf {} + + commit: format @echo "Available topics:" @echo "$(TOPICS)" @@ -40,33 +56,42 @@ commit: format git commit -m "$${topic}: $${message}"; \ git push -check: venv - $(PIP) install safety - $(PIP) freeze | $(PYTHON) -m safety check --stdin +dbclean: + @echo "Cleaning up database..." + @find . -name "delete_db.py" -exec python3 {} \; -clean: - @echo "Cleaning up..." - @find . -name "__pycache__" -type d -exec rm -rf {} + - @find . -name ".pytest_cache" -exec rm -rf {} + - @find . -name ".venv" -exec rm -rf {} + +format: + $(PIP) install black + $(PYTHON) -m black . lint: venv $(PIP) install flake8 pylint $(PYTHON) -m flake8 $(PYTHON) -m pylint **/*.py -format: - $(PIP) install black - $(PYTHON) -m black . +run: venv + $(PYTHON) app.py -help: - @echo "gmake run - run the application" - @echo "gmake test - run the tests" - @echo "gmake clean - remove all generated files" - @echo "gmake check - check for security vulnerabilities" - @echo "gmake commit - commit changes to git" - @echo "gmake help - display this help" +setup: dbclean bids + @echo "Setting up the application database..." swagger: venv open http://localhost:8080/api/docs/#/ - $(PYTHON) app.py \ No newline at end of file + $(PYTHON) app.py + +test: venv + $(PYTHON) -m pytest -vv + +venv/bin/activate: requirements.txt + python3 -m venv .venv + $(PIP) install -r requirements.txt + +venv: venv/bin/activate + . ./.venv/bin/activate + + + + + + + diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 210a796..b2fe03b 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -11,7 +11,6 @@ validate_bid_id_path, validate_bid_update, prepend_host_to_links, - ) bid = Blueprint("bid", __name__) diff --git a/api/schemas/bid_request_schema.py b/api/schemas/bid_request_schema.py index 77ba440..19b098c 100644 --- a/api/schemas/bid_request_schema.py +++ b/api/schemas/bid_request_schema.py @@ -4,7 +4,6 @@ from .feedback_schema import FeedbackSchema - # Marshmallow schema for request body class BidRequestSchema(Schema): tender = fields.Str( diff --git a/dbconfig/mongo_setup.py b/dbconfig/mongo_setup.py index d49f1b5..d6bd278 100644 --- a/dbconfig/mongo_setup.py +++ b/dbconfig/mongo_setup.py @@ -1,6 +1,10 @@ from pymongo import MongoClient +import os +from dotenv import load_dotenv -MONGO_URI = "mongodb://localhost:27017/bidsAPI" +load_dotenv() + +MONGO_URI = os.getenv("MONGO_URL") or "mongodb://localhost:27017/bidsAPI" # Create a new client and connect to the server diff --git a/helpers/helpers.py b/helpers/helpers.py index 4a82fff..f762ad2 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -58,4 +58,3 @@ def prepend_host_to_links(resource, hostname): for key in resource["links"]: resource["links"][key] = f'{host}{resource["links"][key]}' return resource - diff --git a/requirements.txt b/requirements.txt index 4d5e92d..6ac82bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ pytest flask_swagger_ui marshmallow pymongo +python-dotenv \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..e69de29 diff --git a/scripts/create_sample_data.py b/scripts/create_sample_data.py new file mode 100644 index 0000000..20365ef --- /dev/null +++ b/scripts/create_sample_data.py @@ -0,0 +1,54 @@ +""" + +This script creates sample data for the MongoDB database. + +""" + +from pymongo import MongoClient +from dotenv import load_dotenv +import os +import json + +load_dotenv() + +MONGO_URI = os.getenv("MONGO_URL") or "mongodb://localhost:27017/bidsAPI" + + +def populate_bids(): + try: + client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=10000) + db = client["bidsAPI"] + collection = db["bids"] + + # Get the current script's directory + current_dir = os.path.dirname(os.path.abspath(__file__)) + + # Construct the file path to bids.json + file_path = os.path.join(current_dir, "test_data", "bids.json") + + # Read bids data from JSON file + with open(file_path) as bids_file: + bids_data = json.load(bids_file) + + # Insert bids into the database + for bid in bids_data["items"]: + # Check if the bid already exists in the database + existing_bid = collection.find_one({"_id": bid["_id"]}) + if existing_bid: + print(f"Skipping existing bid with _id: {bid['_id']}") + else: + collection.insert_one(bid) + print(f"Inserted bid with _id: {bid['_id']}") + + except Exception as e: + print(f"Error: {e}") + exit(1) + + finally: + # Close the MongoDB connection + client.close() + + +if __name__ == "__main__": + populate_bids() + exit(0) diff --git a/scripts/delete_db.py b/scripts/delete_db.py new file mode 100644 index 0000000..e9c319b --- /dev/null +++ b/scripts/delete_db.py @@ -0,0 +1,39 @@ +""" +This script deletes all bids from the MongoDB collection. + +""" + +from pymongo import MongoClient +import os +from dotenv import load_dotenv + +load_dotenv() + +MONGO_URI = os.getenv("MONGO_URL") or "mongodb://localhost:27017/bidsAPI" + + +def delete_bids(): + try: + client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=10000) + db = client["bidsAPI"] + collection = db["bids"] + + if collection.count_documents({}) == 0: + print("No bids to delete.") + + delete_result = collection.delete_many({}) + + # Print the number of deleted bids + print(f"Deleted {delete_result.deleted_count} bids from the collection.") + + except Exception as e: + print(f"Error: {e}") + exit(1) + + finally: + client.close() + + +if __name__ == "__main__": + delete_bids() + exit(0) diff --git a/scripts/test_data/bids.json b/scripts/test_data/bids.json new file mode 100644 index 0000000..caf934e --- /dev/null +++ b/scripts/test_data/bids.json @@ -0,0 +1,104 @@ +{ + "items": [ + { + "_id": "be15c306-c85b-4e67-a9f6-682553c065a1", + "alias": "ONS", + "bid_date": "2023-06-23", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "Office for National Statistics", + "failed": { + "has_score": true, + "out_of": 36, + "phase": 2, + "score": 20 + }, + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "last_updated": "2023-07-20T17:00:40.510224", + "links": { + "questions": "http://localhost:8080/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions", + "self": "http://localhost:8080/bids/be15c306-c85b-4e67-a9f6-682553c065a1" + }, + "status": "in_progress", + "success": [ + { + "has_score": true, + "out_of": 36, + "phase": 1, + "score": 30 + } + ], + "tender": "Business Intelligence and Data Warehousing", + "was_successful": false + }, + { + "_id": "b4846631-9135-4208-8e37-70eba8f77e15", + "alias": "ONS", + "bid_date": "2023-06-23", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "Office for National Statistics", + "failed": { + "has_score": true, + "out_of": 36, + "phase": 2, + "score": 20 + }, + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "last_updated": "2023-07-20T17:03:12.972780", + "links": { + "questions": "http://localhost:8080/bids/b4846631-9135-4208-8e37-70eba8f77e15/questions", + "self": "http://localhost:8080/bids/b4846631-9135-4208-8e37-70eba8f77e15" + }, + "status": "in_progress", + "success": [ + { + "has_score": true, + "out_of": 36, + "phase": 1, + "score": 30 + } + ], + "tender": "Business Intelligence and Data Warehousing", + "was_successful": false + }, + { + "_id": "2529a0be-6e1c-4202-92c7-65c3742dfd4e", + "alias": "ONS", + "bid_date": "2023-06-23", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "Office for National Statistics", + "failed": { + "has_score": true, + "out_of": 36, + "phase": 2, + "score": 20 + }, + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "last_updated": "2023-07-20T17:03:19.452381", + "links": { + "questions": "http://localhost:8080/bids/2529a0be-6e1c-4202-92c7-65c3742dfd4e/questions", + "self": "http://localhost:8080/bids/2529a0be-6e1c-4202-92c7-65c3742dfd4e" + }, + "status": "in_progress", + "success": [ + { + "has_score": true, + "out_of": 36, + "phase": 1, + "score": 30 + } + ], + "tender": "Business Intelligence and Data Warehousing", + "was_successful": false + } + ], + "total_count": 3 + } \ No newline at end of file diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index 6643f65..58a33d2 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -24,7 +24,7 @@ def test_get_bid_by_id_success(mock_dbConnection, client): response = client.get( "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", - headers={"host": "localhost:8080"} + headers={"host": "localhost:8080"}, ) mock_dbConnection.assert_called_once() @@ -44,7 +44,7 @@ def test_get_bid_by_id_success(mock_dbConnection, client): }, "status": "in_progress", "tender": "Business Intelligence and Data Warehousing", - "was_successful": False + "was_successful": False, } diff --git a/tests/test_post_bid.py b/tests/test_post_bid.py index 131a2fd..1d3a7ee 100644 --- a/tests/test_post_bid.py +++ b/tests/test_post_bid.py @@ -76,7 +76,7 @@ def test_post_bid_connection_error(mock_dbConnection, client): mock_db = mock_dbConnection.return_value mock_db["bids"].insert_one.side_effect = ConnectionFailure response = client.post("/api/bids", json=data) - + assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} @@ -98,7 +98,6 @@ def test_phase_greater_than_2(mock_dbConnection, client): "failed": {"phase": 3, "has_score": True, "score": 20, "out_of": 36}, } - response = client.post("api/bids", json=data) assert response.status_code == 400 assert response.get_json() == { From 9e2882290027b62bf673862cc8c6f82f31859a81 Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 20 Jul 2023 17:44:10 +0100 Subject: [PATCH 86/97] feat: added update bid status endpoint; updated swagger; handled errors for puts --- api/controllers/bid_controller.py | 33 ++++++++++++- helpers/helpers.py | 17 +++++++ request_examples/update_bid.http | 4 +- request_examples/update_bid_status.http | 6 +++ static/swagger_config.yml | 66 ++++++++++++++++++++++++- 5 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 request_examples/update_bid_status.http diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index b2fe03b..b0eea17 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -1,15 +1,18 @@ from flask import Blueprint, request from datetime import datetime from marshmallow import ValidationError +from werkzeug.exceptions import UnprocessableEntity from api.models.status_enum import Status from dbconfig.mongo_setup import dbConnection from helpers.helpers import ( showInternalServerError, showNotFoundError, + showUnprocessableEntityError, showValidationError, validate_and_create_bid_document, validate_bid_id_path, validate_bid_update, + validate_status_update, prepend_host_to_links, ) @@ -79,7 +82,9 @@ def update_bid_by_id(bid_id): # Updates document where id is equal to bid_id db = dbConnection() data = db["bids"].find_one_and_update( - {"_id": bid_id}, {"$set": user_request}, return_document=True + {"_id": bid_id, "status": Status.IN_PROGRESS.value}, + {"$set": user_request}, + return_document=True, ) # Return 404 response if not found / returns None if data is None: @@ -88,6 +93,8 @@ def update_bid_by_id(bid_id): # Return 400 response if input validation fails except ValidationError as e: return showValidationError(e), 400 + except UnprocessableEntity as e: + return showUnprocessableEntityError(e), 422 # Return 500 response in case of connection failure except Exception: return showInternalServerError(), 500 @@ -116,3 +123,27 @@ def change_status_to_deleted(bid_id): # Return 500 response in case of connection failure except Exception: return showInternalServerError(), 500 + + +@bid.route("/bids//status", methods=["PUT"]) +def update_bid_status(bid_id): + try: + bid_id = validate_bid_id_path(bid_id) + data = validate_status_update(request.get_json()) + # Updates document where id is equal to bid_id + db = dbConnection() + response = db["bids"].find_one_and_update( + {"_id": bid_id}, {"$set": data}, return_document=True + ) + # Return 404 response if not found / returns None + if response is None: + return showNotFoundError(), 404 + return response, 200 + # Return 400 response if input validation fails + except ValidationError as e: + return showValidationError(e), 400 + except UnprocessableEntity as e: + return showUnprocessableEntityError(e), 422 + # Return 500 response in case of connection failure + except Exception: + return showInternalServerError(), 500 diff --git a/helpers/helpers.py b/helpers/helpers.py index f762ad2..78df047 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -1,6 +1,8 @@ from flask import jsonify import uuid from datetime import datetime +from werkzeug.exceptions import UnprocessableEntity +from api.models.status_enum import Status from api.schemas.bid_schema import BidSchema from api.schemas.bid_request_schema import BidRequestSchema from api.schemas.valid_bid_id_schema import valid_bid_id_schema @@ -14,6 +16,10 @@ def showNotFoundError(): return jsonify({"Error": "Resource not found"}) +def showUnprocessableEntityError(e): + return jsonify({"Error": str(e.description)}) + + def showValidationError(e): return jsonify({"Error": str(e)}) @@ -49,7 +55,18 @@ def validate_bid_id_path(bid_id): def validate_bid_update(user_request): + if "status" in user_request: + raise UnprocessableEntity("Cannot update status") + data = BidSchema().load(user_request, partial=True) + return data + + +def validate_status_update(user_request): + if user_request == {}: + raise UnprocessableEntity("Request must not be empty") data = BidSchema().load(user_request, partial=True) + if data: + data["status"] = data["status"].value return data diff --git a/request_examples/update_bid.http b/request_examples/update_bid.http index 241514e..0729337 100644 --- a/request_examples/update_bid.http +++ b/request_examples/update_bid.http @@ -1,6 +1,6 @@ -PUT http://localhost:8080/api/bids/9f688442-b535-4683-ae1a-a64c1a3b8616 HTTP/1.1 +PUT http://localhost:8080/api/bids/7cea822f-fb27-4efd-87b3-3467eeb49d68 HTTP/1.1 Content-Type: application/json { - "tender": "UPDATED yeah" + "status": "completed" } diff --git a/request_examples/update_bid_status.http b/request_examples/update_bid_status.http new file mode 100644 index 0000000..b602932 --- /dev/null +++ b/request_examples/update_bid_status.http @@ -0,0 +1,6 @@ +PUT http://localhost:8080/api/bids/7cea822f-fb27-4efd-87b3-3467eeb49d68/status HTTP/1.1 +Content-Type: application/json + +{ + "status": "completed" +} \ No newline at end of file diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 3ee9bcb..35f5894 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -144,6 +144,8 @@ paths: $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' + '422': + $ref: '#/components/responses/UnprocessableEntity' '500': $ref: '#/components/responses/InternalServerError' # -------------------------------------------- @@ -173,6 +175,42 @@ paths: $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' +# -------------------------------------------- + /bids/{bid_id}/status: +# -------------------------------------------- + put: + tags: + - bids + summary: Update status of an existing bid + description: Update status of an existing bid + operationId: update_bid_status + parameters: + - name: bid_id + in: path + description: ID of bid to update + required: true + schema: + type: string + format: uuid + requestBody: + $ref: '#/components/requestBodies/UpdateBid' + required: true + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Bid' + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '422': + $ref: '#/components/responses/UnprocessableEntity' + '500': + $ref: '#/components/responses/InternalServerError' +# -------------------------------------------- # -------------------------------------------- # Components components: @@ -294,12 +332,12 @@ components: QuestionsLink: description: A link to a collection of questions for a bid type: string - example: 'https://localhost:8080/api//bids/96d69775-29af-46b1-aaf4-bfbdb1543412/questions' + example: 'https://{hostname}/api//bids/96d69775-29af-46b1-aaf4-bfbdb1543412/questions' # -------------------------------------------- SelfLink: description: A link to the current resource type: string - example: 'https://localhost:8080/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428' + example: 'https://{hostname}/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428' # -------------------------------------------- BidRequestBody: type: object @@ -415,6 +453,21 @@ components: "out_of": 36 } was_successful: false + UpdateStatus: + description: Request body to update bid status + content: + application/json: + schema: + properties: + status: + description: Status of bid + type: string + format: enum + examples: + 200 OK: + summary: 200 OK + value: + status: 'completed' # -------------------------------------------- # Error responses responses: @@ -445,4 +498,13 @@ components: example: { "Error": "Could not connect to database" } + UnprocessableEntity: + description: Unprocessable Entity + content: + application/json: + schema: + type: object + example: { + "Error": "Request must not be empty" + } # -------------------------------------------- \ No newline at end of file From 89101e57b7f8d473dda8e1fa3ee0f12e624fd948 Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 20 Jul 2023 17:53:10 +0100 Subject: [PATCH 87/97] test: changed file name to test_helpers --- helpers/helpers.py | 1 - tests/{test_prepend_host_to_links.py => test_helpers.py} | 0 2 files changed, 1 deletion(-) rename tests/{test_prepend_host_to_links.py => test_helpers.py} (100%) diff --git a/helpers/helpers.py b/helpers/helpers.py index 78df047..7d883eb 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -2,7 +2,6 @@ import uuid from datetime import datetime from werkzeug.exceptions import UnprocessableEntity -from api.models.status_enum import Status from api.schemas.bid_schema import BidSchema from api.schemas.bid_request_schema import BidRequestSchema from api.schemas.valid_bid_id_schema import valid_bid_id_schema diff --git a/tests/test_prepend_host_to_links.py b/tests/test_helpers.py similarity index 100% rename from tests/test_prepend_host_to_links.py rename to tests/test_helpers.py From 11ef07fe85b6d92b343a731539df38d4147a39b5 Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 20 Jul 2023 19:51:50 +0100 Subject: [PATCH 88/97] test: increased test coverage; added commands to makefile --- Makefile | 4 +- ...alid_bid_id_schema.py => bid_id_schema.py} | 2 +- helpers/helpers.py | 4 +- tests/test_PhaseSchema.py | 38 ++++++++++++ tests/test_get_bids.py | 42 +++++++++++++- tests/test_update_bid_by_id.py | 30 ++++++++-- tests/test_update_bid_status.py | 58 +++++++++++++++++++ 7 files changed, 167 insertions(+), 11 deletions(-) rename api/schemas/{valid_bid_id_schema.py => bid_id_schema.py} (82%) create mode 100644 tests/test_PhaseSchema.py create mode 100644 tests/test_update_bid_status.py diff --git a/Makefile b/Makefile index 12e954a..bfb6c45 100644 --- a/Makefile +++ b/Makefile @@ -21,9 +21,9 @@ run: venv $(PYTHON) app.py test: venv - $(PYTHON) -m pytest -vv + coverage run -m pytest -vv @echo "TEST COVERAGE REPORT" - coverage report -m + coverage report -m --omit="tests/*" branch: @echo "Available branch types:" diff --git a/api/schemas/valid_bid_id_schema.py b/api/schemas/bid_id_schema.py similarity index 82% rename from api/schemas/valid_bid_id_schema.py rename to api/schemas/bid_id_schema.py index acf5862..ab61b42 100644 --- a/api/schemas/valid_bid_id_schema.py +++ b/api/schemas/bid_id_schema.py @@ -1,7 +1,7 @@ from marshmallow import Schema, fields, validate -class valid_bid_id_schema(Schema): +class BidIdSchema(Schema): bid_id = fields.Str( required=True, validate=validate.Length(min=36, error="Invalid bid Id") ) diff --git a/helpers/helpers.py b/helpers/helpers.py index 7d883eb..0cf7c34 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -4,7 +4,7 @@ from werkzeug.exceptions import UnprocessableEntity from api.schemas.bid_schema import BidSchema from api.schemas.bid_request_schema import BidRequestSchema -from api.schemas.valid_bid_id_schema import valid_bid_id_schema +from api.schemas.bid_id_schema import BidIdSchema def showInternalServerError(): @@ -48,7 +48,7 @@ def validate_and_create_bid_document(request): def validate_bid_id_path(bid_id): - valid_bid_id = valid_bid_id_schema().load({"bid_id": bid_id}) + valid_bid_id = BidIdSchema().load({"bid_id": bid_id}) data = valid_bid_id["bid_id"] return data diff --git a/tests/test_PhaseSchema.py b/tests/test_PhaseSchema.py new file mode 100644 index 0000000..f15e9f9 --- /dev/null +++ b/tests/test_PhaseSchema.py @@ -0,0 +1,38 @@ +from unittest.mock import patch + + +# Case 1: score is mandatory when has_score is set to True +@patch("api.controllers.bid_controller.dbConnection") +def test_score_is_mandatory(mock_dbConnection, client): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "success": [{"phase": 1, "has_score": True, "out_of": 36}], + "failed": {"phase": 2, "has_score": True, "score": 20, "out_of": 36}, + } + + response = client.post("api/bids", json=data) + assert response.status_code == 400 + assert ( + response.get_json()["Error"] + == "{'success': {0: {'_schema': ['Score is mandatory when has_score is set to true.']}}}" + ) + + +# Case 2: out_of is mandatory when has_score is set to True +@patch("api.controllers.bid_controller.dbConnection") +def test_score_is_mandatory(mock_dbConnection, client): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "failed": {"phase": 2, "has_score": True, "score": 20}, + } + + response = client.post("api/bids", json=data) + assert response.status_code == 400 + assert ( + response.get_json()["Error"] + == "{'failed': {'_schema': ['Out_of is mandatory when has_score is set to true.']}}" + ) diff --git a/tests/test_get_bids.py b/tests/test_get_bids.py index 0658fd8..a6f539f 100644 --- a/tests/test_get_bids.py +++ b/tests/test_get_bids.py @@ -1,5 +1,4 @@ from unittest.mock import patch -from pymongo.errors import ConnectionFailure # Case 1: Successful get @@ -9,11 +8,50 @@ def test_get_bids(mock_dbConnection, client): mock_db["bids"].find.return_value = [] response = client.get("/api/bids") + mock_db["bids"].find.assert_called_once_with({"status": {"$ne": "deleted"}}) assert response.status_code == 200 assert response.get_json() == {"total_count": 0, "items": []} -# Case 2: Connection error +# Case 2: Links prepended with hostname +@patch("api.controllers.bid_controller.dbConnection") +def test_links_with_host(mock_dbConnection, client): + mock_db = mock_dbConnection.return_value + mock_db["bids"].find.return_value = [ + { + "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", + "bid_date": "2023-06-23", + "client": "Office for National Statistics", + "links": { + "questions": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301/questions", + "self": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301", + }, + "status": "in_progress", + "tender": "Business Intelligence and Data Warehousing", + } + ] + + response = client.get("/api/bids", headers={"host": "localhost:8080"}) + assert response.status_code == 200 + assert response.get_json() == { + "total_count": 1, + "items": [ + { + "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", + "bid_date": "2023-06-23", + "client": "Office for National Statistics", + "links": { + "questions": "http://localhost:8080/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301/questions", + "self": "http://localhost:8080/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301", + }, + "status": "in_progress", + "tender": "Business Intelligence and Data Warehousing", + } + ], + } + + +# Case 3: Connection error @patch("api.controllers.bid_controller.dbConnection", side_effect=Exception) def test_get_bids_connection_error(mock_dbConnection, client): response = client.get("/api/bids") diff --git a/tests/test_update_bid_by_id.py b/tests/test_update_bid_by_id.py index 69fde6b..1db43ef 100644 --- a/tests/test_update_bid_by_id.py +++ b/tests/test_update_bid_by_id.py @@ -16,8 +16,8 @@ def test_update_bid_by_id_success(mock_dbConnection, client): } bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" - updated_bid = {"tender": "UPDATED TENDER"} - response = client.put(f"api/bids/{bid_id}", json=updated_bid) + update = {"tender": "UPDATED TENDER"} + response = client.put(f"api/bids/{bid_id}", json=update) mock_dbConnection.assert_called_once() mock_db["bids"].find_one_and_update.assert_called_once() assert response.status_code == 200 @@ -27,7 +27,29 @@ def test_update_bid_by_id_success(mock_dbConnection, client): @patch("api.controllers.bid_controller.dbConnection") def test_input_validation(mock_dbConnection, client): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" - updated_bid = {"tender": 42} - response = client.put(f"api/bids/{bid_id}", json=updated_bid) + update = {"tender": 42} + response = client.put(f"api/bids/{bid_id}", json=update) assert response.status_code == 400 assert response.get_json()["Error"] == "{'tender': ['Not a valid string.']}" + + +# Case 3: Bid not found +@patch("api.controllers.bid_controller.dbConnection") +def test_bid_not_found(mock_dbConnection, client): + mock_db = mock_dbConnection.return_value + mock_db["bids"].find_one_and_update.return_value = None + bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" + update = {"tender": "Updated tender"} + response = client.put(f"api/bids/{bid_id}", json=update) + assert response.status_code == 404 + assert response.get_json()["Error"] == "Resource not found" + + +# Case 4: Cannot update status +@patch("api.controllers.bid_controller.dbConnection") +def test_cannot_update_status(mock_dbConnection, client): + bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" + update = {"status": "deleted"} + response = client.put(f"api/bids/{bid_id}", json=update) + assert response.status_code == 422 + assert response.get_json()["Error"] == "Cannot update status" diff --git a/tests/test_update_bid_status.py b/tests/test_update_bid_status.py new file mode 100644 index 0000000..a94f3b6 --- /dev/null +++ b/tests/test_update_bid_status.py @@ -0,0 +1,58 @@ +from unittest.mock import patch + + +# Case 1: Successful update +@patch("api.controllers.bid_controller.dbConnection") +def test_update_bid_status_success(mock_dbConnection, client): + mock_db = mock_dbConnection.return_value + mock_db["bids"].find_one_and_update.return_value = { + "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", + "tender": "Business Intelligence and Data Warehousing", + "alias": "ONS", + "bid_date": "2023-06-23", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "Office for National Statistics", + "was_successful": False, + } + + bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" + update = {"status": "completed"} + response = client.put(f"api/bids/{bid_id}/status", json=update) + mock_dbConnection.assert_called_once() + mock_db["bids"].find_one_and_update.assert_called_once() + assert response.status_code == 200 + + +# Case 2: Invalid status +@patch("api.controllers.bid_controller.dbConnection") +def test_invalid_status(mock_dbConnection, client): + bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" + update = {"status": "invalid"} + response = client.put(f"api/bids/{bid_id}/status", json=update) + assert response.status_code == 400 + assert ( + response.get_json()["Error"] + == "{'status': ['Must be one of: deleted, in_progress, completed.']}" + ) + + +# Case 3: Empty request body +@patch("api.controllers.bid_controller.dbConnection") +def test_empty_request(mock_dbConnection, client): + bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" + update = {} + response = client.put(f"api/bids/{bid_id}/status", json=update) + assert response.status_code == 422 + assert response.get_json()["Error"] == "Request must not be empty" + + +# Case 4: Bid not found +@patch("api.controllers.bid_controller.dbConnection") +def test_bid_not_found(mock_dbConnection, client): + mock_db = mock_dbConnection.return_value + mock_db["bids"].find_one_and_update.return_value = None + bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" + update = {"status": "completed"} + response = client.put(f"api/bids/{bid_id}/status", json=update) + assert response.status_code == 404 + assert response.get_json()["Error"] == "Resource not found" From 51fc35cc6c2c050b1d4b93e5aac6921b8dec6702 Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 20 Jul 2023 22:04:24 +0100 Subject: [PATCH 89/97] test: added more tests --- Makefile | 2 +- tests/test_PhaseSchema.py | 1 - tests/test_delete_bid.py | 23 +++++++++++++++++------ tests/test_get_bid_by_id.py | 6 ++---- tests/test_get_bids.py | 2 +- tests/test_post_bid.py | 6 +++--- tests/test_update_bid_by_id.py | 12 ++++++++++++ tests/test_update_bid_status.py | 12 ++++++++++++ 8 files changed, 48 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index bfb6c45..2dcf50f 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ run: venv test: venv coverage run -m pytest -vv @echo "TEST COVERAGE REPORT" - coverage report -m --omit="tests/*" + coverage report -m --omit="tests/*,dbconfig/*" branch: @echo "Available branch types:" diff --git a/tests/test_PhaseSchema.py b/tests/test_PhaseSchema.py index f15e9f9..7a654b8 100644 --- a/tests/test_PhaseSchema.py +++ b/tests/test_PhaseSchema.py @@ -9,7 +9,6 @@ def test_score_is_mandatory(mock_dbConnection, client): "client": "Office for National Statistics", "bid_date": "21-06-2023", "success": [{"phase": 1, "has_score": True, "out_of": 36}], - "failed": {"phase": 2, "has_score": True, "score": 20, "out_of": 36}, } response = client.post("api/bids", json=data) diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py index 81be726..c4f44de 100644 --- a/tests/test_delete_bid.py +++ b/tests/test_delete_bid.py @@ -1,6 +1,4 @@ -from pymongo.errors import ConnectionFailure from unittest.mock import patch -from marshmallow import ValidationError # Case 1: Successful delete a bid by changing status to deleted @@ -18,9 +16,9 @@ def test_delete_bid_success(mock_dbConnection, client): # Case 2: Failed to call database @patch("api.controllers.bid_controller.dbConnection") -def test_delete_bid_find_error(mock_dbConnection, client): +def test_delete_bid_connection_error(mock_dbConnection, client): mock_db = mock_dbConnection.return_value - mock_db["bids"].find_one_and_update.side_effect = ConnectionFailure + mock_db["bids"].find_one_and_update.side_effect = Exception response = client.delete("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} @@ -28,8 +26,21 @@ def test_delete_bid_find_error(mock_dbConnection, client): # Case 3: Validation error @patch("api.controllers.bid_controller.dbConnection") -def test_get_bid_by_id_validation_error(mock_dbConnection, client): - mock_dbConnection.side_effect = ValidationError +def test_delete_bid_validation_error(mock_dbConnection, client): response = client.delete("/api/bids/invalid_bid_id") assert response.status_code == 400 assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} + + +# Case 4: Bid not found +@patch("api.controllers.bid_controller.dbConnection") +def test_delete_bid_not_found(mock_dbConnection, client): + mock_db = mock_dbConnection.return_value + mock_db["bids"].find_one_and_update.return_value = None + + response = client.delete("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") + + mock_dbConnection.assert_called_once() + mock_db["bids"].find_one_and_update.assert_called_once() + assert response.status_code == 404 + assert response.get_json() == {"Error": "Resource not found"} diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index 58a33d2..b75a565 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -1,6 +1,5 @@ from unittest.mock import patch from pymongo.errors import ConnectionFailure -from marshmallow import ValidationError # Case 1: Successful get_bid_by_id @@ -49,8 +48,8 @@ def test_get_bid_by_id_success(mock_dbConnection, client): # Case 2: Connection error -@patch("api.controllers.bid_controller.dbConnection", side_effect=ConnectionFailure) -def test_get_bids_connection_error(mock_dbConnection, client): +@patch("api.controllers.bid_controller.dbConnection", side_effect=Exception) +def test_get_bid_by_id_connection_error(mock_dbConnection, client): response = client.get("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} @@ -75,7 +74,6 @@ def test_get_bid_by_id_not_found(mock_dbConnection, client): # Case 4: Validation error @patch("api.controllers.bid_controller.dbConnection") def test_get_bid_by_id_validation_error(mock_dbConnection, client): - mock_dbConnection.side_effect = ValidationError response = client.get("/api/bids/invalid_bid_id") assert response.status_code == 400 assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} diff --git a/tests/test_get_bids.py b/tests/test_get_bids.py index a6f539f..f90427e 100644 --- a/tests/test_get_bids.py +++ b/tests/test_get_bids.py @@ -3,7 +3,7 @@ # Case 1: Successful get @patch("api.controllers.bid_controller.dbConnection") -def test_get_bids(mock_dbConnection, client): +def test_get_bids_success(mock_dbConnection, client): mock_db = mock_dbConnection.return_value mock_db["bids"].find.return_value = [] diff --git a/tests/test_post_bid.py b/tests/test_post_bid.py index 1d3a7ee..5137d83 100644 --- a/tests/test_post_bid.py +++ b/tests/test_post_bid.py @@ -1,5 +1,4 @@ from unittest.mock import patch -from pymongo.errors import ConnectionFailure # Case 1: Successful post @@ -46,7 +45,8 @@ def test_post_is_successful(mock_dbConnection, client): # Case 2: Missing mandatory fields -def test_field_missing(client): +@patch("api.controllers.bid_controller.dbConnection") +def test_field_missing(mock_dbConnection, client): data = {"client": "Sample Client", "bid_date": "20-06-2023"} response = client.post("api/bids", json=data) @@ -74,7 +74,7 @@ def test_post_bid_connection_error(mock_dbConnection, client): } # Mock the behavior of dbConnection mock_db = mock_dbConnection.return_value - mock_db["bids"].insert_one.side_effect = ConnectionFailure + mock_db["bids"].insert_one.side_effect = Exception response = client.post("/api/bids", json=data) assert response.status_code == 500 diff --git a/tests/test_update_bid_by_id.py b/tests/test_update_bid_by_id.py index 1db43ef..0ca7621 100644 --- a/tests/test_update_bid_by_id.py +++ b/tests/test_update_bid_by_id.py @@ -53,3 +53,15 @@ def test_cannot_update_status(mock_dbConnection, client): response = client.put(f"api/bids/{bid_id}", json=update) assert response.status_code == 422 assert response.get_json()["Error"] == "Cannot update status" + + +# Case 5: Failed to call database +@patch("api.controllers.bid_controller.dbConnection") +def test_update_by_id_find_error(mock_dbConnection, client): + mock_db = mock_dbConnection.return_value + mock_db["bids"].find_one_and_update.side_effect = Exception + bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" + update = {"tender": "Updated tender"} + response = client.put(f"api/bids/{bid_id}", json=update) + assert response.status_code == 500 + assert response.get_json() == {"Error": "Could not connect to database"} diff --git a/tests/test_update_bid_status.py b/tests/test_update_bid_status.py index a94f3b6..722d886 100644 --- a/tests/test_update_bid_status.py +++ b/tests/test_update_bid_status.py @@ -56,3 +56,15 @@ def test_bid_not_found(mock_dbConnection, client): response = client.put(f"api/bids/{bid_id}/status", json=update) assert response.status_code == 404 assert response.get_json()["Error"] == "Resource not found" + + +# Case 5: Failed to call database +@patch("api.controllers.bid_controller.dbConnection") +def test_update_status_find_error(mock_dbConnection, client): + mock_db = mock_dbConnection.return_value + mock_db["bids"].find_one_and_update.side_effect = Exception + bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" + update = {"status": "completed"} + response = client.put(f"api/bids/{bid_id}/status", json=update) + assert response.status_code == 500 + assert response.get_json() == {"Error": "Could not connect to database"} From bca764a1dc24f91545a9358df8fed5141a14e440 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Fri, 21 Jul 2023 09:24:46 +0100 Subject: [PATCH 90/97] refactor: makefile setup --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 613a73c..1ec19b9 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ PYTHON = ./.venv/bin/python3 PIP = ./.venv/bin/pip -.PHONY: run test clean check help commit swagger format branch lint +.PHONY: run test clean check help commit swagger format branch lint setup bids dbclean help: @echo "gmake help - display this help" @@ -25,7 +25,7 @@ help: @echo "gmake setup - setup the application database" @echo "gmake test - run the tests" -bids: +bids: venv @echo "Creating sample data..." @find . -name "create_sample_data.py" -exec python3 {} \; @@ -56,7 +56,7 @@ commit: format git commit -m "$${topic}: $${message}"; \ git push -dbclean: +dbclean: venv @echo "Cleaning up database..." @find . -name "delete_db.py" -exec python3 {} \; @@ -72,7 +72,7 @@ lint: venv run: venv $(PYTHON) app.py -setup: dbclean bids +setup: venv dbclean bids @echo "Setting up the application database..." swagger: venv From 64f7135d3cd952b4e01b110f47e953ab92d1a4db Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 21 Jul 2023 11:56:12 +0100 Subject: [PATCH 91/97] docs: separated test_requirements and added make target; updated readme --- Makefile | 10 ++++++---- README.md | 22 +++++++++++++++++++--- requirements.txt | 2 -- test_requirements.txt | 2 ++ 4 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 test_requirements.txt diff --git a/Makefile b/Makefile index f97e6a6..59292f8 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ help: @echo "gmake help - display this help" @echo "gmake bids - create sample data" @echo "gmake branch - create a new branch" - @echo "gmake build - create and activate virtual environment" + @echo "gmake build - create and activate virtual environment" @echo "gmake check - check for security vulnerabilities" @echo "gmake clean - remove all generated files" @echo "gmake commit - commit changes to git" @@ -79,15 +79,17 @@ run: build setup: build dbclean bids @echo "Setting up the application database..." -swagger: build +swag: open http://localhost:8080/api/docs/#/ - $(PYTHON) app.py -test: +test: test_setup coverage run -m pytest -vv @echo "TEST COVERAGE REPORT" coverage report -m --omit="tests/*,dbconfig/*" +test_setup: test_requirements.txt + $(PIP) install -r test_requirements.txt + venv/bin/activate: requirements.txt python3 -m venv .venv $(PIP) install -r requirements.txt diff --git a/README.md b/README.md index 130c755..9a60a72 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # tdse-accessForce-bids-api # API Documentation -This API provides an endpoint to post a new bid document. +This API stores and serves information about Methods bids for client tenders. ## Prerequisites @@ -55,11 +55,27 @@ This API provides an endpoint to post a new bid document. 1. Run the following command to start the API: ```bash - python app/app.py + gmake run + ``` +2. In a new terminal run the following command to open the Swagger UI in your default web browser: + + ```bash + gmake swag ``` -2. The Swagger Specification will be available at http://localhost:8080/api/docs +-------------- + +## Testing the application +1. Run the following command to start the API: + ```bash + gmake run + ``` +2. In a new terminal enter the following command to run the test suites and generate a test coverage report: + + ```bash + gmake test + ``` -------------- ## Installing and running an instance of MongoDB on your local machine (MacOS) diff --git a/requirements.txt b/requirements.txt index 2411285..7772fce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,9 +6,7 @@ Jinja2 MarkupSafe Werkzeug pip == 23.2.0 -pytest flask_swagger_ui marshmallow pymongo -coverage python-dotenv diff --git a/test_requirements.txt b/test_requirements.txt new file mode 100644 index 0000000..e36ca7a --- /dev/null +++ b/test_requirements.txt @@ -0,0 +1,2 @@ +coverage +pytest \ No newline at end of file From 19e3bb3ad21516620867d7bde0327c6545590d19 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 21 Jul 2023 13:08:17 +0100 Subject: [PATCH 92/97] test: moved phase validation tests into test_BidRequestSchema; refactored dbConnection to db in controller and tests --- api/controllers/bid_controller.py | 8 +-- tests/test_BidRequestSchema.py | 95 +++++++++++++++++++++++++++++++ tests/test_PhaseSchema.py | 8 +-- tests/test_delete_bid.py | 20 +++---- tests/test_get_bid_by_id.py | 21 +++---- tests/test_get_bids.py | 15 +++-- tests/test_post_bid.py | 92 +++--------------------------- tests/test_update_bid_by_id.py | 24 ++++---- tests/test_update_bid_status.py | 24 ++++---- 9 files changed, 152 insertions(+), 155 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index b0eea17..20f5234 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -3,7 +3,7 @@ from marshmallow import ValidationError from werkzeug.exceptions import UnprocessableEntity from api.models.status_enum import Status -from dbconfig.mongo_setup import dbConnection +from dbconfig.mongo_setup import dbConnection as db from helpers.helpers import ( showInternalServerError, showNotFoundError, @@ -23,7 +23,6 @@ def get_bids(): # Get all bids from database collection try: - db = dbConnection() data = list(db["bids"].find({"status": {"$ne": Status.DELETED.value}})) hostname = request.headers.get("host") for resource in data: @@ -36,7 +35,6 @@ def get_bids(): @bid.route("/bids", methods=["POST"]) def post_bid(): try: - db = dbConnection() # Process input and create data model data = validate_and_create_bid_document(request.get_json()) # Insert document into database collection @@ -54,7 +52,6 @@ def post_bid(): def get_bid_by_id(bid_id): try: bid_id = validate_bid_id_path(bid_id) - db = dbConnection() data = db["bids"].find_one( {"_id": bid_id, "status": {"$ne": Status.DELETED.value}} ) @@ -80,7 +77,6 @@ def update_bid_by_id(bid_id): bid_id = validate_bid_id_path(bid_id) user_request = validate_bid_update(request.get_json()) # Updates document where id is equal to bid_id - db = dbConnection() data = db["bids"].find_one_and_update( {"_id": bid_id, "status": Status.IN_PROGRESS.value}, {"$set": user_request}, @@ -104,7 +100,6 @@ def update_bid_by_id(bid_id): def change_status_to_deleted(bid_id): try: bid_id = validate_bid_id_path(bid_id) - db = dbConnection() data = db["bids"].find_one_and_update( {"_id": bid_id, "status": {"$ne": Status.DELETED.value}}, { @@ -131,7 +126,6 @@ def update_bid_status(bid_id): bid_id = validate_bid_id_path(bid_id) data = validate_status_update(request.get_json()) # Updates document where id is equal to bid_id - db = dbConnection() response = db["bids"].find_one_and_update( {"_id": bid_id}, {"$set": data}, return_document=True ) diff --git a/tests/test_BidRequestSchema.py b/tests/test_BidRequestSchema.py index b8fa4b9..dc6d268 100644 --- a/tests/test_BidRequestSchema.py +++ b/tests/test_BidRequestSchema.py @@ -1,10 +1,12 @@ import pytest from marshmallow import ValidationError +from unittest.mock import patch from api.schemas.bid_schema import BidSchema from api.schemas.bid_request_schema import BidRequestSchema from helpers.helpers import is_valid_uuid, is_valid_isoformat +# Case 1: New instance of bid model class generates expected fields def test_bid_model(): data = { "tender": "Business Intelligence and Data Warehousing", @@ -46,6 +48,7 @@ def test_bid_model(): assert is_valid_isoformat("07-06-2023") is False +# Case 2: Field validation - tender def test_validate_tender(): data = { "tender": 42, @@ -56,6 +59,7 @@ def test_validate_tender(): BidRequestSchema().load(data) +# Case 3: Field validation - client def test_validate_client(): data = { "tender": "Business Intelligence and Data Warehousing", @@ -66,6 +70,7 @@ def test_validate_client(): BidRequestSchema().load(data) +# Case 4: Field validation - bid_date def test_validate_bid_date(): data = { "tender": "Business Intelligence and Data Warehousing", @@ -76,6 +81,7 @@ def test_validate_bid_date(): BidRequestSchema().load(data) +# Case 5: Field validation - bid_folder_url def test_validate_bid_folder_url(): data = { "tender": "Business Intelligence and Data Warehousing", @@ -88,6 +94,7 @@ def test_validate_bid_folder_url(): BidRequestSchema().load(data) +# Case 6: Field validation - feedback def test_validate_feedback(): data = { "tender": "Business Intelligence and Data Warehousing", @@ -100,3 +107,91 @@ def test_validate_feedback(): with pytest.raises(ValidationError): BidRequestSchema().load(data) + + +# Case 7: Neither success nor failed fields phase can be more than 2 +def test_phase_greater_than_2(): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", + }, + "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], + "failed": {"phase": 3, "has_score": True, "score": 20, "out_of": 36}, + } + + with pytest.raises(ValidationError, match="Must be one of: 1, 2."): + BidRequestSchema().load(data, partial=True) + + +# Case 8: Success cannot have the same phase in the list +def test_phase_already_exists_in_success(): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", + }, + "success": [ + {"phase": 1, "has_score": True, "out_of": 36, "score": 30}, + {"phase": 1, "has_score": True, "out_of": 50, "score": 60}, + ], + } + + with pytest.raises( + ValidationError, + match="Phase value already exists in 'success' list and cannot be repeated.", + ): + BidRequestSchema().load(data, partial=True) + + +# Case 9: Success cannot contain same phase value as failed +def test_phase_already_in_failed(): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", + }, + "failed": {"phase": 1, "has_score": True, "score": 20, "out_of": 36}, + "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], + } + + with pytest.raises( + ValidationError, + match="Phase value already exists in 'failed' section and cannot be repeated.", + ): + BidRequestSchema().load(data, partial=True) + + +# # Case 10: Failed cannot contain same phase value as success +# def test_phase_already_in_success(): +# data = { +# "tender": "Business Intelligence and Data Warehousing", +# "client": "Office for National Statistics", +# "bid_date": "21-06-2023", +# "alias": "ONS", +# "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", +# "feedback": { +# "description": "Feedback from client in detail", +# "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", +# }, +# "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], +# "failed": {"phase": 1, "has_score": True, "score": 20, "out_of": 36}, +# } + +# with pytest.raises(ValidationError, match="Phase value already exists in 'success' list and cannot be repeated."): +# BidRequestSchema().load(data, partial=True) diff --git a/tests/test_PhaseSchema.py b/tests/test_PhaseSchema.py index 7a654b8..cd88fae 100644 --- a/tests/test_PhaseSchema.py +++ b/tests/test_PhaseSchema.py @@ -2,8 +2,8 @@ # Case 1: score is mandatory when has_score is set to True -@patch("api.controllers.bid_controller.dbConnection") -def test_score_is_mandatory(mock_dbConnection, client): +@patch("api.controllers.bid_controller.db") +def test_score_is_mandatory(mock_db, client): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -20,8 +20,8 @@ def test_score_is_mandatory(mock_dbConnection, client): # Case 2: out_of is mandatory when has_score is set to True -@patch("api.controllers.bid_controller.dbConnection") -def test_score_is_mandatory(mock_dbConnection, client): +@patch("api.controllers.bid_controller.db") +def test_out_of_is_mandatory(mock_db, client): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py index c4f44de..b08dffa 100644 --- a/tests/test_delete_bid.py +++ b/tests/test_delete_bid.py @@ -2,9 +2,8 @@ # Case 1: Successful delete a bid by changing status to deleted -@patch("api.controllers.bid_controller.dbConnection") -def test_delete_bid_success(mock_dbConnection, client): - mock_db = mock_dbConnection.return_value +@patch("api.controllers.bid_controller.db") +def test_delete_bid_success(mock_db, client): mock_db["bids"].find_one_and_update.return_value = { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "status": "deleted", @@ -15,9 +14,8 @@ def test_delete_bid_success(mock_dbConnection, client): # Case 2: Failed to call database -@patch("api.controllers.bid_controller.dbConnection") -def test_delete_bid_connection_error(mock_dbConnection, client): - mock_db = mock_dbConnection.return_value +@patch("api.controllers.bid_controller.db") +def test_delete_bid_connection_error(mock_db, client): mock_db["bids"].find_one_and_update.side_effect = Exception response = client.delete("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") assert response.status_code == 500 @@ -25,22 +23,20 @@ def test_delete_bid_connection_error(mock_dbConnection, client): # Case 3: Validation error -@patch("api.controllers.bid_controller.dbConnection") -def test_delete_bid_validation_error(mock_dbConnection, client): +@patch("api.controllers.bid_controller.db") +def test_delete_bid_validation_error(mock_db, client): response = client.delete("/api/bids/invalid_bid_id") assert response.status_code == 400 assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} # Case 4: Bid not found -@patch("api.controllers.bid_controller.dbConnection") -def test_delete_bid_not_found(mock_dbConnection, client): - mock_db = mock_dbConnection.return_value +@patch("api.controllers.bid_controller.db") +def test_delete_bid_not_found(mock_db, client): mock_db["bids"].find_one_and_update.return_value = None response = client.delete("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") - mock_dbConnection.assert_called_once() mock_db["bids"].find_one_and_update.assert_called_once() assert response.status_code == 404 assert response.get_json() == {"Error": "Resource not found"} diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index b75a565..22bbc89 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -3,9 +3,8 @@ # Case 1: Successful get_bid_by_id -@patch("api.controllers.bid_controller.dbConnection") -def test_get_bid_by_id_success(mock_dbConnection, client): - mock_db = mock_dbConnection.return_value +@patch("api.controllers.bid_controller.db") +def test_get_bid_by_id_success(mock_db, client): mock_db["bids"].find_one.return_value = { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "alias": "ONS", @@ -26,7 +25,6 @@ def test_get_bid_by_id_success(mock_dbConnection, client): headers={"host": "localhost:8080"}, ) - mock_dbConnection.assert_called_once() mock_db["bids"].find_one.assert_called_once_with( {"_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "status": {"$ne": "deleted"}} ) @@ -48,22 +46,21 @@ def test_get_bid_by_id_success(mock_dbConnection, client): # Case 2: Connection error -@patch("api.controllers.bid_controller.dbConnection", side_effect=Exception) -def test_get_bid_by_id_connection_error(mock_dbConnection, client): +@patch("api.controllers.bid_controller.db", side_effect=Exception) +def test_get_bid_by_id_connection_error(mock_db, client): + mock_db["bids"].find_one.side_effect = Exception response = client.get("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} # Case 3: Bid not found -@patch("api.controllers.bid_controller.dbConnection") -def test_get_bid_by_id_not_found(mock_dbConnection, client): - mock_db = mock_dbConnection.return_value +@patch("api.controllers.bid_controller.db") +def test_get_bid_by_id_not_found(mock_db, client): mock_db["bids"].find_one.return_value = None response = client.get("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") - mock_dbConnection.assert_called_once() mock_db["bids"].find_one.assert_called_once_with( {"_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "status": {"$ne": "deleted"}} ) @@ -72,8 +69,8 @@ def test_get_bid_by_id_not_found(mock_dbConnection, client): # Case 4: Validation error -@patch("api.controllers.bid_controller.dbConnection") -def test_get_bid_by_id_validation_error(mock_dbConnection, client): +@patch("api.controllers.bid_controller.db") +def test_get_bid_by_id_validation_error(mock_db, client): response = client.get("/api/bids/invalid_bid_id") assert response.status_code == 400 assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} diff --git a/tests/test_get_bids.py b/tests/test_get_bids.py index f90427e..dabd495 100644 --- a/tests/test_get_bids.py +++ b/tests/test_get_bids.py @@ -2,9 +2,8 @@ # Case 1: Successful get -@patch("api.controllers.bid_controller.dbConnection") -def test_get_bids_success(mock_dbConnection, client): - mock_db = mock_dbConnection.return_value +@patch("api.controllers.bid_controller.db") +def test_get_bids_success(mock_db, client): mock_db["bids"].find.return_value = [] response = client.get("/api/bids") @@ -14,9 +13,8 @@ def test_get_bids_success(mock_dbConnection, client): # Case 2: Links prepended with hostname -@patch("api.controllers.bid_controller.dbConnection") -def test_links_with_host(mock_dbConnection, client): - mock_db = mock_dbConnection.return_value +@patch("api.controllers.bid_controller.db") +def test_links_with_host(mock_db, client): mock_db["bids"].find.return_value = [ { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", @@ -52,8 +50,9 @@ def test_links_with_host(mock_dbConnection, client): # Case 3: Connection error -@patch("api.controllers.bid_controller.dbConnection", side_effect=Exception) -def test_get_bids_connection_error(mock_dbConnection, client): +@patch("api.controllers.bid_controller.db") +def test_get_bids_connection_error(mock_db, client): + mock_db["bids"].find.side_effect = Exception response = client.get("/api/bids") assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} diff --git a/tests/test_post_bid.py b/tests/test_post_bid.py index 5137d83..4826989 100644 --- a/tests/test_post_bid.py +++ b/tests/test_post_bid.py @@ -2,8 +2,8 @@ # Case 1: Successful post -@patch("api.controllers.bid_controller.dbConnection") -def test_post_is_successful(mock_dbConnection, client): +@patch("api.controllers.bid_controller.db") +def test_post_is_successful(mock_db, client): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -18,8 +18,7 @@ def test_post_is_successful(mock_dbConnection, client): "failed": {"phase": 2, "has_score": True, "score": 20, "out_of": 36}, } - # Mock the behavior of dbConnection - mock_db = mock_dbConnection.return_value + # Mock the behavior of db mock_db["bids"].insert_one.return_value = data response = client.post("api/bids", json=data) @@ -45,8 +44,8 @@ def test_post_is_successful(mock_dbConnection, client): # Case 2: Missing mandatory fields -@patch("api.controllers.bid_controller.dbConnection") -def test_field_missing(mock_dbConnection, client): +@patch("api.controllers.bid_controller.db") +def test_field_missing(mock_db, client): data = {"client": "Sample Client", "bid_date": "20-06-2023"} response = client.post("api/bids", json=data) @@ -57,8 +56,8 @@ def test_field_missing(mock_dbConnection, client): # Case 3: Connection error -@patch("api.controllers.bid_controller.dbConnection") -def test_post_bid_connection_error(mock_dbConnection, client): +@patch("api.controllers.bid_controller.db") +def test_post_bid_connection_error(mock_db, client): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -72,84 +71,9 @@ def test_post_bid_connection_error(mock_dbConnection, client): "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], "failed": {"phase": 2, "has_score": True, "score": 20, "out_of": 36}, } - # Mock the behavior of dbConnection - mock_db = mock_dbConnection.return_value + # Mock the behavior of db mock_db["bids"].insert_one.side_effect = Exception response = client.post("/api/bids", json=data) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} - - -# Case 4: Neither success nor failed fields phase can be more than 2 -@patch("api.controllers.bid_controller.dbConnection") -def test_phase_greater_than_2(mock_dbConnection, client): - data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "21-06-2023", - "alias": "ONS", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "feedback": { - "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", - }, - "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], - "failed": {"phase": 3, "has_score": True, "score": 20, "out_of": 36}, - } - - response = client.post("api/bids", json=data) - assert response.status_code == 400 - assert response.get_json() == { - "Error": "{'failed': {'phase': ['Must be one of: 1, 2.']}}" - } - - -# Case 5: Neither success nor failed fields can have the same phase -@patch("api.controllers.bid_controller.dbConnection") -def test_same_phase(mock_dbConnection, client): - data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "21-06-2023", - "alias": "ONS", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "feedback": { - "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", - }, - "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], - "failed": {"phase": 1, "has_score": True, "score": 20, "out_of": 36}, - } - - response = client.post("api/bids", json=data) - assert response.status_code == 400 - assert response.get_json() == { - "Error": "{'success': [\"Phase value already exists in 'failed' section and cannot be repeated.\"]}" - } - - -# Case 6: Success cannot have the same phase in the list -@patch("api.controllers.bid_controller.dbConnection") -def test_success_same_phase(mock_dbConnection, client): - data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "21-06-2023", - "alias": "ONS", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "feedback": { - "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", - }, - "success": [ - {"phase": 1, "has_score": True, "out_of": 36, "score": 30}, - {"phase": 1, "has_score": True, "out_of": 50, "score": 60}, - ], - } - - response = client.post("api/bids", json=data) - assert response.status_code == 400 - assert response.get_json() == { - "Error": "{'success': [\"Phase value already exists in 'success' list and cannot be repeated.\"]}" - } diff --git a/tests/test_update_bid_by_id.py b/tests/test_update_bid_by_id.py index 0ca7621..f34c7d9 100644 --- a/tests/test_update_bid_by_id.py +++ b/tests/test_update_bid_by_id.py @@ -2,9 +2,8 @@ # Case 1: Successful update -@patch("api.controllers.bid_controller.dbConnection") -def test_update_bid_by_id_success(mock_dbConnection, client): - mock_db = mock_dbConnection.return_value +@patch("api.controllers.bid_controller.db") +def test_update_bid_by_id_success(mock_db, client): mock_db["bids"].find_one_and_update.return_value = { "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", "tender": "Business Intelligence and Data Warehousing", @@ -18,14 +17,13 @@ def test_update_bid_by_id_success(mock_dbConnection, client): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"tender": "UPDATED TENDER"} response = client.put(f"api/bids/{bid_id}", json=update) - mock_dbConnection.assert_called_once() mock_db["bids"].find_one_and_update.assert_called_once() assert response.status_code == 200 # Case 2: Invalid user input -@patch("api.controllers.bid_controller.dbConnection") -def test_input_validation(mock_dbConnection, client): +@patch("api.controllers.bid_controller.db") +def test_input_validation(mock_db, client): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"tender": 42} response = client.put(f"api/bids/{bid_id}", json=update) @@ -34,9 +32,8 @@ def test_input_validation(mock_dbConnection, client): # Case 3: Bid not found -@patch("api.controllers.bid_controller.dbConnection") -def test_bid_not_found(mock_dbConnection, client): - mock_db = mock_dbConnection.return_value +@patch("api.controllers.bid_controller.db") +def test_bid_not_found(mock_db, client): mock_db["bids"].find_one_and_update.return_value = None bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"tender": "Updated tender"} @@ -46,8 +43,8 @@ def test_bid_not_found(mock_dbConnection, client): # Case 4: Cannot update status -@patch("api.controllers.bid_controller.dbConnection") -def test_cannot_update_status(mock_dbConnection, client): +@patch("api.controllers.bid_controller.db") +def test_cannot_update_status(mock_db, client): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "deleted"} response = client.put(f"api/bids/{bid_id}", json=update) @@ -56,9 +53,8 @@ def test_cannot_update_status(mock_dbConnection, client): # Case 5: Failed to call database -@patch("api.controllers.bid_controller.dbConnection") -def test_update_by_id_find_error(mock_dbConnection, client): - mock_db = mock_dbConnection.return_value +@patch("api.controllers.bid_controller.db") +def test_update_by_id_find_error(mock_db, client): mock_db["bids"].find_one_and_update.side_effect = Exception bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"tender": "Updated tender"} diff --git a/tests/test_update_bid_status.py b/tests/test_update_bid_status.py index 722d886..d1f4491 100644 --- a/tests/test_update_bid_status.py +++ b/tests/test_update_bid_status.py @@ -2,9 +2,8 @@ # Case 1: Successful update -@patch("api.controllers.bid_controller.dbConnection") -def test_update_bid_status_success(mock_dbConnection, client): - mock_db = mock_dbConnection.return_value +@patch("api.controllers.bid_controller.db") +def test_update_bid_status_success(mock_db, client): mock_db["bids"].find_one_and_update.return_value = { "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", "tender": "Business Intelligence and Data Warehousing", @@ -18,14 +17,13 @@ def test_update_bid_status_success(mock_dbConnection, client): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "completed"} response = client.put(f"api/bids/{bid_id}/status", json=update) - mock_dbConnection.assert_called_once() mock_db["bids"].find_one_and_update.assert_called_once() assert response.status_code == 200 # Case 2: Invalid status -@patch("api.controllers.bid_controller.dbConnection") -def test_invalid_status(mock_dbConnection, client): +@patch("api.controllers.bid_controller.db") +def test_invalid_status(mock_db, client): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "invalid"} response = client.put(f"api/bids/{bid_id}/status", json=update) @@ -37,8 +35,8 @@ def test_invalid_status(mock_dbConnection, client): # Case 3: Empty request body -@patch("api.controllers.bid_controller.dbConnection") -def test_empty_request(mock_dbConnection, client): +@patch("api.controllers.bid_controller.db") +def test_empty_request(mock_db, client): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {} response = client.put(f"api/bids/{bid_id}/status", json=update) @@ -47,9 +45,8 @@ def test_empty_request(mock_dbConnection, client): # Case 4: Bid not found -@patch("api.controllers.bid_controller.dbConnection") -def test_bid_not_found(mock_dbConnection, client): - mock_db = mock_dbConnection.return_value +@patch("api.controllers.bid_controller.db") +def test_bid_not_found(mock_db, client): mock_db["bids"].find_one_and_update.return_value = None bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "completed"} @@ -59,9 +56,8 @@ def test_bid_not_found(mock_dbConnection, client): # Case 5: Failed to call database -@patch("api.controllers.bid_controller.dbConnection") -def test_update_status_find_error(mock_dbConnection, client): - mock_db = mock_dbConnection.return_value +@patch("api.controllers.bid_controller.db") +def test_update_status_find_error(mock_db, client): mock_db["bids"].find_one_and_update.side_effect = Exception bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "completed"} From 57a43492f04dc3d6b21845a045c0dda7afa5db8d Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 21 Jul 2023 16:55:50 +0100 Subject: [PATCH 93/97] refactor: separated logic for post_bid_schema and update_bid_schema; updated tests --- api/controllers/bid_controller.py | 4 +- api/schemas/bid_schema.py | 28 --- ...d_request_schema.py => post_bid_schema.py} | 10 +- api/schemas/update_bid_schema.py | 64 ++++++ dbconfig/mongo_setup.py | 6 +- helpers/helpers.py | 17 +- request_examples/all_fields.http | 4 +- request_examples/get_all.http | 2 +- request_examples/update_bid.http | 9 +- tests/test_BidRequestSchema.py | 197 ------------------ tests/test_get_bid_by_id.py | 1 - tests/test_helpers.py | 1 + tests/test_post_bid.py | 2 +- ...st_PhaseSchema.py => test_schema_phase.py} | 0 tests/test_schema_post_bid.py | 107 ++++++++++ tests/test_schema_update_bid.py | 91 ++++++++ tests/test_update_bid_by_id.py | 25 +++ 17 files changed, 322 insertions(+), 246 deletions(-) delete mode 100644 api/schemas/bid_schema.py rename api/schemas/{bid_request_schema.py => post_bid_schema.py} (90%) create mode 100644 api/schemas/update_bid_schema.py delete mode 100644 tests/test_BidRequestSchema.py rename tests/{test_PhaseSchema.py => test_schema_phase.py} (100%) create mode 100644 tests/test_schema_post_bid.py create mode 100644 tests/test_schema_update_bid.py diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 20f5234..f48cfaa 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -3,7 +3,7 @@ from marshmallow import ValidationError from werkzeug.exceptions import UnprocessableEntity from api.models.status_enum import Status -from dbconfig.mongo_setup import dbConnection as db +from dbconfig.mongo_setup import db from helpers.helpers import ( showInternalServerError, showNotFoundError, @@ -44,7 +44,7 @@ def post_bid(): except ValidationError as e: return showValidationError(e), 400 # Return 500 response in case of connection failure - except Exception: + except Exception as e: return showInternalServerError(), 500 diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py deleted file mode 100644 index 4bfc935..0000000 --- a/api/schemas/bid_schema.py +++ /dev/null @@ -1,28 +0,0 @@ -from marshmallow import Schema, fields, post_load -from datetime import datetime -from .links_schema import LinksSchema -from .phase_schema import PhaseSchema -from .feedback_schema import FeedbackSchema -from ..models.status_enum import Status - - -# Marshmallow schema -class BidSchema(Schema): - _id = fields.UUID(required=True) - tender = fields.Str(required=True) - client = fields.Str(required=True) - alias = fields.Str() - bid_date = fields.Date(required=True) - bid_folder_url = fields.Str() - status = fields.Enum(Status, by_value=True, required=True) - links = fields.Nested(LinksSchema, required=True) - was_successful = fields.Bool(required=True) - success = fields.List(fields.Nested(PhaseSchema)) - failed = fields.Nested(PhaseSchema) - feedback = fields.Nested(FeedbackSchema) - last_updated = fields.DateTime(required=True) - - @post_load - def set_last_updated(self, data, **kwargs): - data["last_updated"] = datetime.now().isoformat() - return data diff --git a/api/schemas/bid_request_schema.py b/api/schemas/post_bid_schema.py similarity index 90% rename from api/schemas/bid_request_schema.py rename to api/schemas/post_bid_schema.py index 19b098c..c8255f9 100644 --- a/api/schemas/bid_request_schema.py +++ b/api/schemas/post_bid_schema.py @@ -1,11 +1,14 @@ from marshmallow import Schema, fields, post_load, validates, ValidationError from api.models.bid_model import BidModel +from .links_schema import LinksSchema from .phase_schema import PhaseSchema from .feedback_schema import FeedbackSchema +from ..models.status_enum import Status # Marshmallow schema for request body -class BidRequestSchema(Schema): +class PostBidSchema(Schema): + _id = fields.UUID() tender = fields.Str( required=True, error_messages={"required": {"message": "Missing mandatory field"}}, @@ -14,17 +17,20 @@ class BidRequestSchema(Schema): required=True, error_messages={"required": {"message": "Missing mandatory field"}}, ) - alias = fields.Str() bid_date = fields.Date( format="%d-%m-%Y", required=True, error_messages={"required": {"message": "Missing mandatory field"}}, ) + alias = fields.Str() bid_folder_url = fields.URL() was_successful = fields.Boolean() success = fields.List(fields.Nested(PhaseSchema)) failed = fields.Nested(PhaseSchema) feedback = fields.Nested(FeedbackSchema) + status = fields.Enum(Status, by_value=True) + links = fields.Nested(LinksSchema) + last_updated = fields.DateTime() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/api/schemas/update_bid_schema.py b/api/schemas/update_bid_schema.py new file mode 100644 index 0000000..1096573 --- /dev/null +++ b/api/schemas/update_bid_schema.py @@ -0,0 +1,64 @@ +from marshmallow import Schema, fields, post_load, validates, ValidationError +from datetime import datetime +from .links_schema import LinksSchema +from .phase_schema import PhaseSchema +from .feedback_schema import FeedbackSchema +from ..models.status_enum import Status + + +# Marshmallow schema +class UpdateBidSchema(Schema): + _id = fields.UUID(required=True) + tender = fields.Str(required=True) + client = fields.Str(required=True) + bid_date = fields.Date(required=True) + alias = fields.Str() + bid_folder_url = fields.Str() + status = fields.Enum(Status, by_value=True, required=True) + links = fields.Nested(LinksSchema, required=True) + was_successful = fields.Bool(required=True) + success = fields.List(fields.Nested(PhaseSchema)) + failed = fields.Nested(PhaseSchema) + feedback = fields.Nested(FeedbackSchema) + last_updated = fields.DateTime(required=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.context["failed_phase_values"] = set() + + @validates("success") + def validate_success(self, value): + phase_values = set() + for phase in value: + phase_value = phase.get("phase") + if phase_value in phase_values: + raise ValidationError( + "Phase value already exists in 'success' list and cannot be repeated." + ) + phase_values.add(phase_value) + + @validates("failed") + def validate_failed(self, value): + phase_value = value.get("phase") + if phase_value in self.context.get("success_phase_values", set()): + raise ValidationError( + "Phase value already exists in 'success' list and cannot be repeated." + ) + self.context["failed_phase_values"].add(phase_value) + + @validates("success") + def validate_success_and_failed(self, value): + success_phase_values = set() + failed_phase_values = self.context.get("failed_phase_values", set()) + for phase in value: + phase_value = phase.get("phase") + if phase_value in failed_phase_values: + raise ValidationError( + "Phase value already exists in 'failed' section and cannot be repeated." + ) + success_phase_values.add(phase_value) + + @post_load + def set_last_updated(self, data, **kwargs): + data["last_updated"] = datetime.now().isoformat() + return data diff --git a/dbconfig/mongo_setup.py b/dbconfig/mongo_setup.py index d6bd278..b902276 100644 --- a/dbconfig/mongo_setup.py +++ b/dbconfig/mongo_setup.py @@ -8,7 +8,5 @@ # Create a new client and connect to the server -def dbConnection(): - client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=10000) - db = client["bidsAPI"] - return db +client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=10000) +db = client["bidsAPI"] diff --git a/helpers/helpers.py b/helpers/helpers.py index 0cf7c34..4ab13a3 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -2,8 +2,8 @@ import uuid from datetime import datetime from werkzeug.exceptions import UnprocessableEntity -from api.schemas.bid_schema import BidSchema -from api.schemas.bid_request_schema import BidRequestSchema +from api.schemas.update_bid_schema import UpdateBidSchema +from api.schemas.post_bid_schema import PostBidSchema from api.schemas.bid_id_schema import BidIdSchema @@ -41,9 +41,9 @@ def is_valid_isoformat(string): def validate_and_create_bid_document(request): # Process input and create data model - bid_document = BidRequestSchema().load(request) + bid_document = PostBidSchema().load(request) # Serialize to a JSON object - data = BidSchema().dump(bid_document) + data = PostBidSchema().dump(bid_document) return data @@ -56,14 +56,19 @@ def validate_bid_id_path(bid_id): def validate_bid_update(user_request): if "status" in user_request: raise UnprocessableEntity("Cannot update status") - data = BidSchema().load(user_request, partial=True) + data = UpdateBidSchema().load(user_request, partial=True) + if "failed" in data: + data["failed"]["phase"] = data["failed"]["phase"].value + if "success" in data: + for obj in data["success"]: + obj["phase"] = obj["phase"].value return data def validate_status_update(user_request): if user_request == {}: raise UnprocessableEntity("Request must not be empty") - data = BidSchema().load(user_request, partial=True) + data = UpdateBidSchema().load(user_request, partial=True) if data: data["status"] = data["status"].value return data diff --git a/request_examples/all_fields.http b/request_examples/all_fields.http index 7f0a2e3..42f5571 100644 --- a/request_examples/all_fields.http +++ b/request_examples/all_fields.http @@ -15,8 +15,8 @@ Content-Type: application/json { "phase": 1, "has_score": true, - "out_of": 36, - "score": 30 + "score": 28, + "out_of": 36 } ], "failed": { diff --git a/request_examples/get_all.http b/request_examples/get_all.http index 68f17b1..712676d 100644 --- a/request_examples/get_all.http +++ b/request_examples/get_all.http @@ -1 +1 @@ -GET http://localhost:8080/api/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301 HTTP/1.1 +GET http://localhost:8080/api/bids HTTP/1.1 diff --git a/request_examples/update_bid.http b/request_examples/update_bid.http index 0729337..25d51e1 100644 --- a/request_examples/update_bid.http +++ b/request_examples/update_bid.http @@ -1,6 +1,11 @@ -PUT http://localhost:8080/api/bids/7cea822f-fb27-4efd-87b3-3467eeb49d68 HTTP/1.1 +PUT http://localhost:8080/api/bids/0f05e112-6fd5-49b0-b7b0-e86aa376a61f HTTP/1.1 Content-Type: application/json { - "status": "completed" + "failed": { + "has_score": true, + "out_of": 36, + "phase": 1, + "score": 28 + } } diff --git a/tests/test_BidRequestSchema.py b/tests/test_BidRequestSchema.py deleted file mode 100644 index dc6d268..0000000 --- a/tests/test_BidRequestSchema.py +++ /dev/null @@ -1,197 +0,0 @@ -import pytest -from marshmallow import ValidationError -from unittest.mock import patch -from api.schemas.bid_schema import BidSchema -from api.schemas.bid_request_schema import BidRequestSchema -from helpers.helpers import is_valid_uuid, is_valid_isoformat - - -# Case 1: New instance of bid model class generates expected fields -def test_bid_model(): - data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "21-06-2023", - "alias": "ONS", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "feedback": { - "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", - }, - "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], - "failed": {"phase": 2, "has_score": True, "score": 20, "out_of": 36}, - } - bid_document = BidRequestSchema().load(data) - to_post = BidSchema().dump(bid_document) - - id = to_post["_id"] - # Test that UUID is generated and is valid UUID - assert to_post["_id"] is not None - assert is_valid_uuid(id) is True - # Test UUID validator - assert is_valid_uuid("99999") is False - - # Test that links object is generated and URLs are correct - assert to_post["links"] is not None - assert "self" in to_post["links"] - assert to_post["links"]["self"] == f"/bids/{id}" - assert "questions" in to_post["links"] - assert to_post["links"]["questions"] == f"/bids/{id}/questions" - - # Test that status is set to in_progress - assert to_post["status"] == "in_progress" - - # Test that last_updated field has been added and is valid - assert to_post["last_updated"] is not None - assert is_valid_isoformat(to_post["last_updated"]) is True - # Test ISOformat validator - assert is_valid_isoformat("07-06-2023") is False - - -# Case 2: Field validation - tender -def test_validate_tender(): - data = { - "tender": 42, - "client": "Office for National Statistics", - "bid_date": "21-06-2023", - } - with pytest.raises(ValidationError): - BidRequestSchema().load(data) - - -# Case 3: Field validation - client -def test_validate_client(): - data = { - "tender": "Business Intelligence and Data Warehousing", - "client": 42, - "bid_date": "21-06-2023", - } - with pytest.raises(ValidationError): - BidRequestSchema().load(data) - - -# Case 4: Field validation - bid_date -def test_validate_bid_date(): - data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "2023-12-25", - } - with pytest.raises(ValidationError): - BidRequestSchema().load(data) - - -# Case 5: Field validation - bid_folder_url -def test_validate_bid_folder_url(): - data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "21-06-2023", - "bid_folder_url": "Not a valid URL", - } - - with pytest.raises(ValidationError): - BidRequestSchema().load(data) - - -# Case 6: Field validation - feedback -def test_validate_feedback(): - data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "21-06-2023", - "alias": "ONS", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "feedback": {"description": 42, "url": "Invalid URL"}, - } - - with pytest.raises(ValidationError): - BidRequestSchema().load(data) - - -# Case 7: Neither success nor failed fields phase can be more than 2 -def test_phase_greater_than_2(): - data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "21-06-2023", - "alias": "ONS", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "feedback": { - "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", - }, - "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], - "failed": {"phase": 3, "has_score": True, "score": 20, "out_of": 36}, - } - - with pytest.raises(ValidationError, match="Must be one of: 1, 2."): - BidRequestSchema().load(data, partial=True) - - -# Case 8: Success cannot have the same phase in the list -def test_phase_already_exists_in_success(): - data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "21-06-2023", - "alias": "ONS", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "feedback": { - "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", - }, - "success": [ - {"phase": 1, "has_score": True, "out_of": 36, "score": 30}, - {"phase": 1, "has_score": True, "out_of": 50, "score": 60}, - ], - } - - with pytest.raises( - ValidationError, - match="Phase value already exists in 'success' list and cannot be repeated.", - ): - BidRequestSchema().load(data, partial=True) - - -# Case 9: Success cannot contain same phase value as failed -def test_phase_already_in_failed(): - data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "21-06-2023", - "alias": "ONS", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "feedback": { - "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", - }, - "failed": {"phase": 1, "has_score": True, "score": 20, "out_of": 36}, - "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], - } - - with pytest.raises( - ValidationError, - match="Phase value already exists in 'failed' section and cannot be repeated.", - ): - BidRequestSchema().load(data, partial=True) - - -# # Case 10: Failed cannot contain same phase value as success -# def test_phase_already_in_success(): -# data = { -# "tender": "Business Intelligence and Data Warehousing", -# "client": "Office for National Statistics", -# "bid_date": "21-06-2023", -# "alias": "ONS", -# "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", -# "feedback": { -# "description": "Feedback from client in detail", -# "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", -# }, -# "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], -# "failed": {"phase": 1, "has_score": True, "score": 20, "out_of": 36}, -# } - -# with pytest.raises(ValidationError, match="Phase value already exists in 'success' list and cannot be repeated."): -# BidRequestSchema().load(data, partial=True) diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index 22bbc89..f1398e9 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -1,5 +1,4 @@ from unittest.mock import patch -from pymongo.errors import ConnectionFailure # Case 1: Successful get_bid_by_id diff --git a/tests/test_helpers.py b/tests/test_helpers.py index e60f835..39d2106 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,6 +1,7 @@ from helpers.helpers import prepend_host_to_links +# Case 1: Host is prepended to values in links object def test_prepend_host(): resource = { "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", diff --git a/tests/test_post_bid.py b/tests/test_post_bid.py index 4826989..3270723 100644 --- a/tests/test_post_bid.py +++ b/tests/test_post_bid.py @@ -39,7 +39,7 @@ def test_post_is_successful(mock_db, client): ) assert ( "bid_date" in response.get_json() - and response.get_json()["bid_date"] == "2023-06-21" + and response.get_json()["bid_date"] == "21-06-2023" ) diff --git a/tests/test_PhaseSchema.py b/tests/test_schema_phase.py similarity index 100% rename from tests/test_PhaseSchema.py rename to tests/test_schema_phase.py diff --git a/tests/test_schema_post_bid.py b/tests/test_schema_post_bid.py new file mode 100644 index 0000000..df6ba7d --- /dev/null +++ b/tests/test_schema_post_bid.py @@ -0,0 +1,107 @@ +import pytest +from marshmallow import ValidationError +from api.schemas.post_bid_schema import PostBidSchema +from helpers.helpers import is_valid_uuid, is_valid_isoformat + + +# Case 1: New instance of bid model class generates expected fields +def test_bid_model(): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", + }, + "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], + "failed": {"phase": 2, "has_score": True, "score": 20, "out_of": 36}, + } + bid_document = PostBidSchema().load(data) + to_post = PostBidSchema().dump(bid_document) + + id = to_post["_id"] + # Test that UUID is generated and is valid UUID + assert to_post["_id"] is not None + assert is_valid_uuid(id) is True + # Test UUID validator + assert is_valid_uuid("99999") is False + + # Test that links object is generated and URLs are correct + assert to_post["links"] is not None + assert "self" in to_post["links"] + assert to_post["links"]["self"] == f"/bids/{id}" + assert "questions" in to_post["links"] + assert to_post["links"]["questions"] == f"/bids/{id}/questions" + + # Test that status is set to in_progress + assert to_post["status"] == "in_progress" + + # Test that last_updated field has been added and is valid + assert to_post["last_updated"] is not None + assert is_valid_isoformat(to_post["last_updated"]) is True + # Test ISOformat validator + assert is_valid_isoformat("07-06-2023") is False + + +# Case 2: Field validation - tender +def test_validate_tender(): + data = { + "tender": 42, + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + } + with pytest.raises(ValidationError): + PostBidSchema().load(data) + + +# Case 3: Field validation - client +def test_validate_client(): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": 42, + "bid_date": "21-06-2023", + } + with pytest.raises(ValidationError): + PostBidSchema().load(data) + + +# Case 4: Field validation - bid_date +def test_validate_bid_date(): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "2023-12-25", + } + with pytest.raises(ValidationError): + PostBidSchema().load(data) + + +# Case 5: Field validation - bid_folder_url +def test_validate_bid_folder_url(): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "bid_folder_url": "Not a valid URL", + } + + with pytest.raises(ValidationError): + PostBidSchema().load(data) + + +# Case 6: Field validation - feedback +def test_validate_feedback(): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": {"description": 42, "url": "Invalid URL"}, + } + + with pytest.raises(ValidationError): + PostBidSchema().load(data) diff --git a/tests/test_schema_update_bid.py b/tests/test_schema_update_bid.py new file mode 100644 index 0000000..360ca1c --- /dev/null +++ b/tests/test_schema_update_bid.py @@ -0,0 +1,91 @@ +import pytest +from marshmallow import ValidationError +from api.schemas.update_bid_schema import UpdateBidSchema + + +# Case 7: Neither success nor failed fields phase can be more than 2 +def test_phase_greater_than_2(): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", + }, + "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], + "failed": {"phase": 3, "has_score": True, "score": 20, "out_of": 36}, + } + + with pytest.raises(ValidationError, match="Must be one of: 1, 2."): + UpdateBidSchema().load(data, partial=True) + + +# Case 8: Success cannot have the same phase in the list +def test_phase_already_exists_in_success(): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", + }, + "success": [ + {"phase": 1, "has_score": True, "out_of": 36, "score": 30}, + {"phase": 1, "has_score": True, "out_of": 50, "score": 60}, + ], + } + + with pytest.raises( + ValidationError, + match="Phase value already exists in 'success' list and cannot be repeated.", + ): + UpdateBidSchema().load(data, partial=True) + + +# Case 9: Success cannot contain same phase value as failed +def test_phase_already_in_failed(): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "21-06-2023", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", + }, + "failed": {"phase": 1, "has_score": True, "score": 20, "out_of": 36}, + "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], + } + + with pytest.raises( + ValidationError, + match="Phase value already exists in 'failed' section and cannot be repeated.", + ): + UpdateBidSchema().load(data, partial=True) + + +# # Case 10: Failed cannot contain same phase value as success +# def test_phase_already_in_success(): +# data = { +# "tender": "Business Intelligence and Data Warehousing", +# "client": "Office for National Statistics", +# "bid_date": "21-06-2023", +# "alias": "ONS", +# "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", +# "feedback": { +# "description": "Feedback from client in detail", +# "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", +# }, +# "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], +# "failed": {"phase": 1, "has_score": True, "score": 20, "out_of": 36}, +# } + +# with pytest.raises(ValidationError, match="Phase value already exists in 'success' list and cannot be repeated."): +# UpdateBidSchema().load(data, partial=True) diff --git a/tests/test_update_bid_by_id.py b/tests/test_update_bid_by_id.py index f34c7d9..ffbb7f0 100644 --- a/tests/test_update_bid_by_id.py +++ b/tests/test_update_bid_by_id.py @@ -61,3 +61,28 @@ def test_update_by_id_find_error(mock_db, client): response = client.put(f"api/bids/{bid_id}", json=update) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} + + +# Case 6: Update failed field +@patch("api.controllers.bid_controller.db") +def test_update_failed(mock_db, client): + bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" + update = {"failed": {"phase": 2, "has_score": True, "score": 20, "out_of": 36}} + response = client.put(f"api/bids/{bid_id}", json=update) + mock_db["bids"].find_one_and_update.assert_called_once() + assert response.status_code == 200 + + +# Case 7: Update success field +@patch("api.controllers.bid_controller.db") +def test_update_success(mock_db, client): + bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" + update = { + "success": [ + {"phase": 1, "has_score": True, "score": 20, "out_of": 36}, + {"phase": 2, "has_score": True, "score": 20, "out_of": 36}, + ] + } + response = client.put(f"api/bids/{bid_id}", json=update) + mock_db["bids"].find_one_and_update.assert_called_once() + assert response.status_code == 200 From 4f67fdc150e04d3423a2eb6338cef3bbc140683e Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 21 Jul 2023 17:04:18 +0100 Subject: [PATCH 94/97] refactor: changed date input format to ensure same output, updated swagger --- api/schemas/post_bid_schema.py | 2 +- request_examples/all_fields.http | 2 +- static/swagger_config.yml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/schemas/post_bid_schema.py b/api/schemas/post_bid_schema.py index c8255f9..e0a9dc4 100644 --- a/api/schemas/post_bid_schema.py +++ b/api/schemas/post_bid_schema.py @@ -18,7 +18,7 @@ class PostBidSchema(Schema): error_messages={"required": {"message": "Missing mandatory field"}}, ) bid_date = fields.Date( - format="%d-%m-%Y", + format="%Y-%m-%d", required=True, error_messages={"required": {"message": "Missing mandatory field"}}, ) diff --git a/request_examples/all_fields.http b/request_examples/all_fields.http index 42f5571..6b33316 100644 --- a/request_examples/all_fields.http +++ b/request_examples/all_fields.http @@ -4,7 +4,7 @@ Content-Type: application/json { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", - "bid_date": "23-06-2023", + "bid_date": "2023-06-23", "alias": "ONS", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": { diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 35f5894..16dc336 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -394,7 +394,7 @@ components: description: 'Feedback from client in detail' client: 'Office for National Statistics' alias: 'ONS' - bid_date: '21-06-2023' + bid_date: '2023-06-21' success: [ { "phase": 1, @@ -419,7 +419,7 @@ components: description: 'Feedback from client in detail' client: 'Office for National Statistics' alias: 'ONS' - bid_date: '21-06-2023' + bid_date: '2023-06-21' UpdateBid: description: Bid object to replace bid by Id content: @@ -437,7 +437,7 @@ components: description: 'Feedback from client in detail' client: 'Office for National Statistics' alias: 'ONS' - bid_date: '21-06-2023' + bid_date: '2023-06-21' success: [ { "phase": 1, From 2b3fe8c121df8558180bb9f75e68ce4a67a4aad4 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 21 Jul 2023 17:21:36 +0100 Subject: [PATCH 95/97] test: updated tests for date format change --- tests/test_post_bid.py | 8 ++++---- tests/test_schema_phase.py | 4 ++-- tests/test_schema_post_bid.py | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/test_post_bid.py b/tests/test_post_bid.py index 3270723..6a3227f 100644 --- a/tests/test_post_bid.py +++ b/tests/test_post_bid.py @@ -7,7 +7,7 @@ def test_post_is_successful(mock_db, client): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", - "bid_date": "21-06-2023", + "bid_date": "2023-06-21", "alias": "ONS", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": { @@ -39,14 +39,14 @@ def test_post_is_successful(mock_db, client): ) assert ( "bid_date" in response.get_json() - and response.get_json()["bid_date"] == "21-06-2023" + and response.get_json()["bid_date"] == "2023-06-21" ) # Case 2: Missing mandatory fields @patch("api.controllers.bid_controller.db") def test_field_missing(mock_db, client): - data = {"client": "Sample Client", "bid_date": "20-06-2023"} + data = {"client": "Sample Client", "bid_date": "2023-06-20"} response = client.post("api/bids", json=data) assert response.status_code == 400 @@ -61,7 +61,7 @@ def test_post_bid_connection_error(mock_db, client): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", - "bid_date": "21-06-2023", + "bid_date": "2023-06-21", "alias": "ONS", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": { diff --git a/tests/test_schema_phase.py b/tests/test_schema_phase.py index cd88fae..085367e 100644 --- a/tests/test_schema_phase.py +++ b/tests/test_schema_phase.py @@ -7,7 +7,7 @@ def test_score_is_mandatory(mock_db, client): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", - "bid_date": "21-06-2023", + "bid_date": "2023-06-21", "success": [{"phase": 1, "has_score": True, "out_of": 36}], } @@ -25,7 +25,7 @@ def test_out_of_is_mandatory(mock_db, client): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", - "bid_date": "21-06-2023", + "bid_date": "2023-06-21", "failed": {"phase": 2, "has_score": True, "score": 20}, } diff --git a/tests/test_schema_post_bid.py b/tests/test_schema_post_bid.py index df6ba7d..bac514a 100644 --- a/tests/test_schema_post_bid.py +++ b/tests/test_schema_post_bid.py @@ -9,7 +9,7 @@ def test_bid_model(): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", - "bid_date": "21-06-2023", + "bid_date": "2023-06-21", "alias": "ONS", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": { @@ -51,7 +51,7 @@ def test_validate_tender(): data = { "tender": 42, "client": "Office for National Statistics", - "bid_date": "21-06-2023", + "bid_date": "2023-06-21", } with pytest.raises(ValidationError): PostBidSchema().load(data) @@ -62,7 +62,7 @@ def test_validate_client(): data = { "tender": "Business Intelligence and Data Warehousing", "client": 42, - "bid_date": "21-06-2023", + "bid_date": "2023-06-21", } with pytest.raises(ValidationError): PostBidSchema().load(data) @@ -73,7 +73,7 @@ def test_validate_bid_date(): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", - "bid_date": "2023-12-25", + "bid_date": "20-12-2023", } with pytest.raises(ValidationError): PostBidSchema().load(data) @@ -84,7 +84,7 @@ def test_validate_bid_folder_url(): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", - "bid_date": "21-06-2023", + "bid_date": "2023-06-21", "bid_folder_url": "Not a valid URL", } @@ -97,7 +97,7 @@ def test_validate_feedback(): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", - "bid_date": "21-06-2023", + "bid_date": "2023-06-21", "alias": "ONS", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": {"description": 42, "url": "Invalid URL"}, From 8b071e1311fc2c151ae4a5fff6fad1f972fe2e68 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 21 Jul 2023 18:56:23 +0100 Subject: [PATCH 96/97] refactor: added todo plan to incorporate validation of unique phases for update --- TODO.txt | 8 +++ api/schemas/post_bid_schema.py | 46 ++++---------- tests/test_schema_post_bid.py | 68 ++++++++++++++++++++ tests/test_schema_update_bid.py | 106 ++++++++++++++++---------------- 4 files changed, 141 insertions(+), 87 deletions(-) create mode 100644 TODO.txt diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..360424e --- /dev/null +++ b/TODO.txt @@ -0,0 +1,8 @@ +- Refactor PUT to retrieve bid with id +- Update bid with user_update +- Load and validate with post_bid_schema + - will validate unique phases +- Post_load into Bid Model + - will generate timestamp +- Dump insatnce back into post_bid_schema +- Replace bid in collection \ No newline at end of file diff --git a/api/schemas/post_bid_schema.py b/api/schemas/post_bid_schema.py index e0a9dc4..4d6db6d 100644 --- a/api/schemas/post_bid_schema.py +++ b/api/schemas/post_bid_schema.py @@ -1,4 +1,4 @@ -from marshmallow import Schema, fields, post_load, validates, ValidationError +from marshmallow import Schema, fields, post_load, validates_schema, ValidationError from api.models.bid_model import BidModel from .links_schema import LinksSchema from .phase_schema import PhaseSchema @@ -32,41 +32,21 @@ class PostBidSchema(Schema): links = fields.Nested(LinksSchema) last_updated = fields.DateTime() - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.context["failed_phase_values"] = set() + @validates_schema + def validate_unique_phases(self, data, **kwargs): + # Get the list of success phases and the failed phase (if available) + success_phases = data.get("success", []) + failed_phase = data.get("failed", None) - @validates("success") - def validate_success(self, value): - phase_values = set() - for phase in value: - phase_value = phase.get("phase") - if phase_value in phase_values: - raise ValidationError( - "Phase value already exists in 'success' list and cannot be repeated." - ) - phase_values.add(phase_value) + # Combine the success phases and the failed phase (if available) + all_phases = success_phases + ([failed_phase] if failed_phase else []) - @validates("failed") - def validate_failed(self, value): - phase_value = value.get("phase") - if phase_value in self.context.get("success_phase_values", set()): - raise ValidationError( - "Phase value already exists in 'success' list and cannot be repeated." - ) - self.context["failed_phase_values"].add(phase_value) + # Extract phase values and remove any None values + phase_values = [phase.get("phase") for phase in all_phases if phase] - @validates("success") - def validate_success_and_failed(self, value): - success_phase_values = set() - failed_phase_values = self.context.get("failed_phase_values", set()) - for phase in value: - phase_value = phase.get("phase") - if phase_value in failed_phase_values: - raise ValidationError( - "Phase value already exists in 'failed' section and cannot be repeated." - ) - success_phase_values.add(phase_value) + # Check if phase_values contain duplicates using sets + if len(phase_values) != len(set(phase_values)): + raise ValidationError("Phase values must be unique") # Creates a Bid instance after processing @post_load diff --git a/tests/test_schema_post_bid.py b/tests/test_schema_post_bid.py index bac514a..8542b43 100644 --- a/tests/test_schema_post_bid.py +++ b/tests/test_schema_post_bid.py @@ -105,3 +105,71 @@ def test_validate_feedback(): with pytest.raises(ValidationError): PostBidSchema().load(data) + + +# Case 7: Neither success nor failed fields phase can be more than 2 +def test_phase_greater_than_2(): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "2023-06-21", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", + }, + "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], + "failed": {"phase": 3, "has_score": True, "score": 20, "out_of": 36}, + } + + with pytest.raises(ValidationError, match="Must be one of: 1, 2."): + PostBidSchema().load(data, partial=True) + + +# Case 8: Success cannot have the same phase in the list +def test_phase_already_exists_in_success(): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "2023-06-21", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", + }, + "success": [ + {"phase": 1, "has_score": True, "out_of": 36, "score": 30}, + {"phase": 1, "has_score": True, "out_of": 50, "score": 60}, + ], + } + + with pytest.raises( + ValidationError, + match="Phase values must be unique", + ): + PostBidSchema().load(data, partial=True) + + +# Case 9: Success cannot contain same phase value as failed +def test_same_phase(): + data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "2023-06-21", + "alias": "ONS", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", + }, + "failed": {"phase": 1, "has_score": True, "score": 20, "out_of": 36}, + "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], + } + + with pytest.raises( + ValidationError, + match="Phase values must be unique", + ): + PostBidSchema().load(data, partial=True) diff --git a/tests/test_schema_update_bid.py b/tests/test_schema_update_bid.py index 360ca1c..94f4543 100644 --- a/tests/test_schema_update_bid.py +++ b/tests/test_schema_update_bid.py @@ -3,19 +3,9 @@ from api.schemas.update_bid_schema import UpdateBidSchema -# Case 7: Neither success nor failed fields phase can be more than 2 -def test_phase_greater_than_2(): +# Case 1: Failed phase cannot be more than 2 +def test_failed_phase_greater_than_2(): data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "21-06-2023", - "alias": "ONS", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "feedback": { - "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", - }, - "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], "failed": {"phase": 3, "has_score": True, "score": 20, "out_of": 36}, } @@ -23,55 +13,63 @@ def test_phase_greater_than_2(): UpdateBidSchema().load(data, partial=True) -# Case 8: Success cannot have the same phase in the list -def test_phase_already_exists_in_success(): - data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "21-06-2023", - "alias": "ONS", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "feedback": { - "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", - }, - "success": [ - {"phase": 1, "has_score": True, "out_of": 36, "score": 30}, - {"phase": 1, "has_score": True, "out_of": 50, "score": 60}, - ], - } +# Case 2: Success phase cannot be more than 2 +def test_success_phase_greater_than_2(): + data = {"success": [{"phase": 4, "has_score": True, "out_of": 36, "score": 30}]} - with pytest.raises( - ValidationError, - match="Phase value already exists in 'success' list and cannot be repeated.", - ): + with pytest.raises(ValidationError, match="Must be one of: 1, 2."): UpdateBidSchema().load(data, partial=True) -# Case 9: Success cannot contain same phase value as failed -def test_phase_already_in_failed(): - data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "21-06-2023", - "alias": "ONS", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "feedback": { - "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", - }, - "failed": {"phase": 1, "has_score": True, "score": 20, "out_of": 36}, - "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], - } +# # Case 2: Success cannot have the same phase in the list +# def test_phase_already_exists_in_success(): +# data = { +# "tender": "Business Intelligence and Data Warehousing", +# "client": "Office for National Statistics", +# "bid_date": "21-06-2023", +# "alias": "ONS", +# "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", +# "feedback": { +# "description": "Feedback from client in detail", +# "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", +# }, +# "success": [ +# {"phase": 1, "has_score": True, "out_of": 36, "score": 30}, +# {"phase": 1, "has_score": True, "out_of": 50, "score": 60}, +# ], +# } - with pytest.raises( - ValidationError, - match="Phase value already exists in 'failed' section and cannot be repeated.", - ): - UpdateBidSchema().load(data, partial=True) +# with pytest.raises( +# ValidationError, +# match="Phase value already exists in 'success' list and cannot be repeated.", +# ): +# UpdateBidSchema().load(data, partial=True) + + +# # Case 3: Success cannot contain same phase value as failed +# def test_phase_already_in_failed(): +# data = { +# "tender": "Business Intelligence and Data Warehousing", +# "client": "Office for National Statistics", +# "bid_date": "21-06-2023", +# "alias": "ONS", +# "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", +# "feedback": { +# "description": "Feedback from client in detail", +# "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", +# }, +# "failed": {"phase": 1, "has_score": True, "score": 20, "out_of": 36}, +# "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], +# } + +# with pytest.raises( +# ValidationError, +# match="Phase value already exists in 'failed' section and cannot be repeated.", +# ): +# UpdateBidSchema().load(data, partial=True) -# # Case 10: Failed cannot contain same phase value as success +# # Case 4: Failed cannot contain same phase value as success # def test_phase_already_in_success(): # data = { # "tender": "Business Intelligence and Data Warehousing", From ee1ea7668cefe62b5fbe6ba07f4d565a283d3cac Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 24 Jul 2023 14:41:23 +0100 Subject: [PATCH 97/97] refactor: unique phase validation works on put; put and post use same schema; removed and renamed bid schema --- TODO.txt | 4 +- api/controllers/bid_controller.py | 36 ++++---- api/models/bid_model.py | 15 +++- .../{post_bid_schema.py => bid_schema.py} | 14 +-- api/schemas/update_bid_schema.py | 64 ------------- helpers/helpers.py | 30 +++---- request_examples/all_fields.http | 21 +---- request_examples/update_bid.http | 9 +- ..._schema_post_bid.py => test_bid_schema.py} | 48 +++++----- ...t_schema_phase.py => test_phase_schema.py} | 0 tests/test_schema_update_bid.py | 89 ------------------ tests/test_update_bid_by_id.py | 90 +++++++++++++++++-- tests/test_update_bid_status.py | 42 +++++++-- 13 files changed, 196 insertions(+), 266 deletions(-) rename api/schemas/{post_bid_schema.py => bid_schema.py} (83%) delete mode 100644 api/schemas/update_bid_schema.py rename tests/{test_schema_post_bid.py => test_bid_schema.py} (80%) rename tests/{test_schema_phase.py => test_phase_schema.py} (100%) delete mode 100644 tests/test_schema_update_bid.py diff --git a/TODO.txt b/TODO.txt index 360424e..1b278d2 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,8 +1,8 @@ - Refactor PUT to retrieve bid with id - Update bid with user_update -- Load and validate with post_bid_schema +- Load and validate with bid_schema - will validate unique phases - Post_load into Bid Model - will generate timestamp -- Dump insatnce back into post_bid_schema +- Dump insatnce back into bid_schema - Replace bid in collection \ No newline at end of file diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index f48cfaa..38b87bd 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -75,24 +75,26 @@ def get_bid_by_id(bid_id): def update_bid_by_id(bid_id): try: bid_id = validate_bid_id_path(bid_id) - user_request = validate_bid_update(request.get_json()) - # Updates document where id is equal to bid_id - data = db["bids"].find_one_and_update( - {"_id": bid_id, "status": Status.IN_PROGRESS.value}, - {"$set": user_request}, - return_document=True, + # Retrieve resource where id is equal to bid_id + current_bid = db["bids"].find_one( + {"_id": bid_id, "status": Status.IN_PROGRESS.value} ) # Return 404 response if not found / returns None - if data is None: + if current_bid is None: return showNotFoundError(), 404 - return data, 200 + updated_bid = validate_bid_update(request.get_json(), current_bid) + db["bids"].replace_one( + {"_id": bid_id}, + updated_bid, + ) + return updated_bid, 200 # Return 400 response if input validation fails except ValidationError as e: return showValidationError(e), 400 except UnprocessableEntity as e: return showUnprocessableEntityError(e), 422 # Return 500 response in case of connection failure - except Exception: + except Exception as e: return showInternalServerError(), 500 @@ -124,15 +126,17 @@ def change_status_to_deleted(bid_id): def update_bid_status(bid_id): try: bid_id = validate_bid_id_path(bid_id) - data = validate_status_update(request.get_json()) - # Updates document where id is equal to bid_id - response = db["bids"].find_one_and_update( - {"_id": bid_id}, {"$set": data}, return_document=True - ) + # Retrieve resource where id is equal to bid_id + current_bid = db["bids"].find_one({"_id": bid_id}) # Return 404 response if not found / returns None - if response is None: + if current_bid is None: return showNotFoundError(), 404 - return response, 200 + updated_bid = validate_status_update(request.get_json(), current_bid) + db["bids"].replace_one( + {"_id": bid_id}, + updated_bid, + ) + return updated_bid, 200 # Return 400 response if input validation fails except ValidationError as e: return showValidationError(e), 400 diff --git a/api/models/bid_model.py b/api/models/bid_model.py index 3e13bbf..442b7a8 100644 --- a/api/models/bid_model.py +++ b/api/models/bid_model.py @@ -17,15 +17,24 @@ def __init__( failed=None, was_successful=False, success=[], - status=Status.IN_PROGRESS, + status=None, + _id=None, + links=None, + last_updated=None, ): - self._id = uuid4() + if _id is None: + self._id = uuid4() + else: + self._id = _id + if status is None: + self.status = Status.IN_PROGRESS + else: + self.status = status self.tender = tender self.client = client self.alias = alias self.bid_date = bid_date self.bid_folder_url = bid_folder_url - self.status = status # enum: "deleted", "in_progress" or "completed" self.links = LinksModel(self._id) self.was_successful = was_successful self.success = success diff --git a/api/schemas/post_bid_schema.py b/api/schemas/bid_schema.py similarity index 83% rename from api/schemas/post_bid_schema.py rename to api/schemas/bid_schema.py index 4d6db6d..67a7f1e 100644 --- a/api/schemas/post_bid_schema.py +++ b/api/schemas/bid_schema.py @@ -7,7 +7,7 @@ # Marshmallow schema for request body -class PostBidSchema(Schema): +class BidSchema(Schema): _id = fields.UUID() tender = fields.Str( required=True, @@ -22,12 +22,12 @@ class PostBidSchema(Schema): required=True, error_messages={"required": {"message": "Missing mandatory field"}}, ) - alias = fields.Str() - bid_folder_url = fields.URL() - was_successful = fields.Boolean() - success = fields.List(fields.Nested(PhaseSchema)) - failed = fields.Nested(PhaseSchema) - feedback = fields.Nested(FeedbackSchema) + alias = fields.Str(allow_none=True) + bid_folder_url = fields.URL(allow_none=True) + was_successful = fields.Boolean(allow_none=True) + success = fields.List(fields.Nested(PhaseSchema), allow_none=True) + failed = fields.Nested(PhaseSchema, allow_none=True) + feedback = fields.Nested(FeedbackSchema, allow_none=True) status = fields.Enum(Status, by_value=True) links = fields.Nested(LinksSchema) last_updated = fields.DateTime() diff --git a/api/schemas/update_bid_schema.py b/api/schemas/update_bid_schema.py deleted file mode 100644 index 1096573..0000000 --- a/api/schemas/update_bid_schema.py +++ /dev/null @@ -1,64 +0,0 @@ -from marshmallow import Schema, fields, post_load, validates, ValidationError -from datetime import datetime -from .links_schema import LinksSchema -from .phase_schema import PhaseSchema -from .feedback_schema import FeedbackSchema -from ..models.status_enum import Status - - -# Marshmallow schema -class UpdateBidSchema(Schema): - _id = fields.UUID(required=True) - tender = fields.Str(required=True) - client = fields.Str(required=True) - bid_date = fields.Date(required=True) - alias = fields.Str() - bid_folder_url = fields.Str() - status = fields.Enum(Status, by_value=True, required=True) - links = fields.Nested(LinksSchema, required=True) - was_successful = fields.Bool(required=True) - success = fields.List(fields.Nested(PhaseSchema)) - failed = fields.Nested(PhaseSchema) - feedback = fields.Nested(FeedbackSchema) - last_updated = fields.DateTime(required=True) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.context["failed_phase_values"] = set() - - @validates("success") - def validate_success(self, value): - phase_values = set() - for phase in value: - phase_value = phase.get("phase") - if phase_value in phase_values: - raise ValidationError( - "Phase value already exists in 'success' list and cannot be repeated." - ) - phase_values.add(phase_value) - - @validates("failed") - def validate_failed(self, value): - phase_value = value.get("phase") - if phase_value in self.context.get("success_phase_values", set()): - raise ValidationError( - "Phase value already exists in 'success' list and cannot be repeated." - ) - self.context["failed_phase_values"].add(phase_value) - - @validates("success") - def validate_success_and_failed(self, value): - success_phase_values = set() - failed_phase_values = self.context.get("failed_phase_values", set()) - for phase in value: - phase_value = phase.get("phase") - if phase_value in failed_phase_values: - raise ValidationError( - "Phase value already exists in 'failed' section and cannot be repeated." - ) - success_phase_values.add(phase_value) - - @post_load - def set_last_updated(self, data, **kwargs): - data["last_updated"] = datetime.now().isoformat() - return data diff --git a/helpers/helpers.py b/helpers/helpers.py index 4ab13a3..436feec 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -2,8 +2,7 @@ import uuid from datetime import datetime from werkzeug.exceptions import UnprocessableEntity -from api.schemas.update_bid_schema import UpdateBidSchema -from api.schemas.post_bid_schema import PostBidSchema +from api.schemas.bid_schema import BidSchema from api.schemas.bid_id_schema import BidIdSchema @@ -41,9 +40,9 @@ def is_valid_isoformat(string): def validate_and_create_bid_document(request): # Process input and create data model - bid_document = PostBidSchema().load(request) + bid_document = BidSchema().load(request) # Serialize to a JSON object - data = PostBidSchema().dump(bid_document) + data = BidSchema().dump(bid_document) return data @@ -53,24 +52,21 @@ def validate_bid_id_path(bid_id): return data -def validate_bid_update(user_request): - if "status" in user_request: +def validate_bid_update(request, resource): + if "status" in request: raise UnprocessableEntity("Cannot update status") - data = UpdateBidSchema().load(user_request, partial=True) - if "failed" in data: - data["failed"]["phase"] = data["failed"]["phase"].value - if "success" in data: - for obj in data["success"]: - obj["phase"] = obj["phase"].value + resource.update(request) + bid = BidSchema().load(resource, partial=True) + data = BidSchema().dump(bid) return data -def validate_status_update(user_request): - if user_request == {}: +def validate_status_update(request, resource): + if request == {}: raise UnprocessableEntity("Request must not be empty") - data = UpdateBidSchema().load(user_request, partial=True) - if data: - data["status"] = data["status"].value + resource.update(request) + bid = BidSchema().load(resource, partial=True) + data = BidSchema().dump(bid) return data diff --git a/request_examples/all_fields.http b/request_examples/all_fields.http index 6b33316..aa610a7 100644 --- a/request_examples/all_fields.http +++ b/request_examples/all_fields.http @@ -5,24 +5,5 @@ Content-Type: application/json "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", "bid_date": "2023-06-23", - "alias": "ONS", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "feedback": { - "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" - }, - "success": [ - { - "phase": 1, - "has_score": true, - "score": 28, - "out_of": 36 - } - ], - "failed": { - "phase": 2, - "has_score": true, - "score": 20, - "out_of": 36 - } + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder" } diff --git a/request_examples/update_bid.http b/request_examples/update_bid.http index 25d51e1..47e08bd 100644 --- a/request_examples/update_bid.http +++ b/request_examples/update_bid.http @@ -1,11 +1,6 @@ -PUT http://localhost:8080/api/bids/0f05e112-6fd5-49b0-b7b0-e86aa376a61f HTTP/1.1 +PUT http://localhost:8080/api/bids/34c765e7-ed75-4108-80ac-2c902bcf38f1/status HTTP/1.1 Content-Type: application/json { - "failed": { - "has_score": true, - "out_of": 36, - "phase": 1, - "score": 28 - } + "status": "completed" } diff --git a/tests/test_schema_post_bid.py b/tests/test_bid_schema.py similarity index 80% rename from tests/test_schema_post_bid.py rename to tests/test_bid_schema.py index 8542b43..01d686b 100644 --- a/tests/test_schema_post_bid.py +++ b/tests/test_bid_schema.py @@ -1,6 +1,6 @@ import pytest from marshmallow import ValidationError -from api.schemas.post_bid_schema import PostBidSchema +from api.schemas.bid_schema import BidSchema from helpers.helpers import is_valid_uuid, is_valid_isoformat @@ -19,8 +19,8 @@ def test_bid_model(): "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], "failed": {"phase": 2, "has_score": True, "score": 20, "out_of": 36}, } - bid_document = PostBidSchema().load(data) - to_post = PostBidSchema().dump(bid_document) + bid_document = BidSchema().load(data) + to_post = BidSchema().dump(bid_document) id = to_post["_id"] # Test that UUID is generated and is valid UUID @@ -54,7 +54,7 @@ def test_validate_tender(): "bid_date": "2023-06-21", } with pytest.raises(ValidationError): - PostBidSchema().load(data) + BidSchema().load(data) # Case 3: Field validation - client @@ -65,7 +65,7 @@ def test_validate_client(): "bid_date": "2023-06-21", } with pytest.raises(ValidationError): - PostBidSchema().load(data) + BidSchema().load(data) # Case 4: Field validation - bid_date @@ -76,7 +76,7 @@ def test_validate_bid_date(): "bid_date": "20-12-2023", } with pytest.raises(ValidationError): - PostBidSchema().load(data) + BidSchema().load(data) # Case 5: Field validation - bid_folder_url @@ -89,7 +89,7 @@ def test_validate_bid_folder_url(): } with pytest.raises(ValidationError): - PostBidSchema().load(data) + BidSchema().load(data) # Case 6: Field validation - feedback @@ -104,30 +104,28 @@ def test_validate_feedback(): } with pytest.raises(ValidationError): - PostBidSchema().load(data) + BidSchema().load(data) -# Case 7: Neither success nor failed fields phase can be more than 2 -def test_phase_greater_than_2(): +# Case 7: Failed phase cannot be more than 2 +def test_failed_phase_greater_than_2(): data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "2023-06-21", - "alias": "ONS", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "feedback": { - "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", - }, - "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], "failed": {"phase": 3, "has_score": True, "score": 20, "out_of": 36}, } with pytest.raises(ValidationError, match="Must be one of: 1, 2."): - PostBidSchema().load(data, partial=True) + BidSchema().load(data, partial=True) + + +# Case 8: Success phase cannot be more than 2 +def test_success_phase_greater_than_2(): + data = {"success": [{"phase": 4, "has_score": True, "out_of": 36, "score": 30}]} + + with pytest.raises(ValidationError, match="Must be one of: 1, 2."): + BidSchema().load(data, partial=True) -# Case 8: Success cannot have the same phase in the list +# Case 9: Success cannot have the same phase in the list def test_phase_already_exists_in_success(): data = { "tender": "Business Intelligence and Data Warehousing", @@ -149,10 +147,10 @@ def test_phase_already_exists_in_success(): ValidationError, match="Phase values must be unique", ): - PostBidSchema().load(data, partial=True) + BidSchema().load(data, partial=True) -# Case 9: Success cannot contain same phase value as failed +# Case 10: Success cannot contain same phase value as failed def test_same_phase(): data = { "tender": "Business Intelligence and Data Warehousing", @@ -172,4 +170,4 @@ def test_same_phase(): ValidationError, match="Phase values must be unique", ): - PostBidSchema().load(data, partial=True) + BidSchema().load(data, partial=True) diff --git a/tests/test_schema_phase.py b/tests/test_phase_schema.py similarity index 100% rename from tests/test_schema_phase.py rename to tests/test_phase_schema.py diff --git a/tests/test_schema_update_bid.py b/tests/test_schema_update_bid.py deleted file mode 100644 index 94f4543..0000000 --- a/tests/test_schema_update_bid.py +++ /dev/null @@ -1,89 +0,0 @@ -import pytest -from marshmallow import ValidationError -from api.schemas.update_bid_schema import UpdateBidSchema - - -# Case 1: Failed phase cannot be more than 2 -def test_failed_phase_greater_than_2(): - data = { - "failed": {"phase": 3, "has_score": True, "score": 20, "out_of": 36}, - } - - with pytest.raises(ValidationError, match="Must be one of: 1, 2."): - UpdateBidSchema().load(data, partial=True) - - -# Case 2: Success phase cannot be more than 2 -def test_success_phase_greater_than_2(): - data = {"success": [{"phase": 4, "has_score": True, "out_of": 36, "score": 30}]} - - with pytest.raises(ValidationError, match="Must be one of: 1, 2."): - UpdateBidSchema().load(data, partial=True) - - -# # Case 2: Success cannot have the same phase in the list -# def test_phase_already_exists_in_success(): -# data = { -# "tender": "Business Intelligence and Data Warehousing", -# "client": "Office for National Statistics", -# "bid_date": "21-06-2023", -# "alias": "ONS", -# "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", -# "feedback": { -# "description": "Feedback from client in detail", -# "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", -# }, -# "success": [ -# {"phase": 1, "has_score": True, "out_of": 36, "score": 30}, -# {"phase": 1, "has_score": True, "out_of": 50, "score": 60}, -# ], -# } - -# with pytest.raises( -# ValidationError, -# match="Phase value already exists in 'success' list and cannot be repeated.", -# ): -# UpdateBidSchema().load(data, partial=True) - - -# # Case 3: Success cannot contain same phase value as failed -# def test_phase_already_in_failed(): -# data = { -# "tender": "Business Intelligence and Data Warehousing", -# "client": "Office for National Statistics", -# "bid_date": "21-06-2023", -# "alias": "ONS", -# "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", -# "feedback": { -# "description": "Feedback from client in detail", -# "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", -# }, -# "failed": {"phase": 1, "has_score": True, "score": 20, "out_of": 36}, -# "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], -# } - -# with pytest.raises( -# ValidationError, -# match="Phase value already exists in 'failed' section and cannot be repeated.", -# ): -# UpdateBidSchema().load(data, partial=True) - - -# # Case 4: Failed cannot contain same phase value as success -# def test_phase_already_in_success(): -# data = { -# "tender": "Business Intelligence and Data Warehousing", -# "client": "Office for National Statistics", -# "bid_date": "21-06-2023", -# "alias": "ONS", -# "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", -# "feedback": { -# "description": "Feedback from client in detail", -# "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", -# }, -# "success": [{"phase": 1, "has_score": True, "out_of": 36, "score": 30}], -# "failed": {"phase": 1, "has_score": True, "score": 20, "out_of": 36}, -# } - -# with pytest.raises(ValidationError, match="Phase value already exists in 'success' list and cannot be repeated."): -# UpdateBidSchema().load(data, partial=True) diff --git a/tests/test_update_bid_by_id.py b/tests/test_update_bid_by_id.py index ffbb7f0..130c3e6 100644 --- a/tests/test_update_bid_by_id.py +++ b/tests/test_update_bid_by_id.py @@ -4,7 +4,7 @@ # Case 1: Successful update @patch("api.controllers.bid_controller.db") def test_update_bid_by_id_success(mock_db, client): - mock_db["bids"].find_one_and_update.return_value = { + mock_db["bids"].find_one.return_value = { "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", "tender": "Business Intelligence and Data Warehousing", "alias": "ONS", @@ -17,14 +17,35 @@ def test_update_bid_by_id_success(mock_db, client): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"tender": "UPDATED TENDER"} response = client.put(f"api/bids/{bid_id}", json=update) - mock_db["bids"].find_one_and_update.assert_called_once() + mock_db["bids"].find_one.assert_called_once_with( + {"_id": bid_id, "status": "in_progress"} + ) + mock_db["bids"].replace_one.assert_called_once() assert response.status_code == 200 # Case 2: Invalid user input @patch("api.controllers.bid_controller.db") def test_input_validation(mock_db, client): - bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" + mock_db["bids"].find_one.return_value = { + "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", + "alias": "ONS", + "bid_date": "2023-06-23", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "Office for National Statistics", + "failed": None, + "feedback": None, + "last_updated": "2023-07-24T11:38:20.388019", + "links": { + "questions": "http://localhost:8080/bids/4141fac8-8879-4169-a46d-2effb1f515f6/questions", + "self": "http://localhost:8080/bids/4141fac8-8879-4169-a46d-2effb1f515f6", + }, + "status": "in_progress", + "success": [], + "tender": "Business Intelligence and Data Warehousing", + "was_successful": False, + } + bid_id = "4141fac8-8879-4169-a46d-2effb1f515f6" update = {"tender": 42} response = client.put(f"api/bids/{bid_id}", json=update) assert response.status_code == 400 @@ -34,7 +55,7 @@ def test_input_validation(mock_db, client): # Case 3: Bid not found @patch("api.controllers.bid_controller.db") def test_bid_not_found(mock_db, client): - mock_db["bids"].find_one_and_update.return_value = None + mock_db["bids"].find_one.return_value = None bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"tender": "Updated tender"} response = client.put(f"api/bids/{bid_id}", json=update) @@ -55,7 +76,16 @@ def test_cannot_update_status(mock_db, client): # Case 5: Failed to call database @patch("api.controllers.bid_controller.db") def test_update_by_id_find_error(mock_db, client): - mock_db["bids"].find_one_and_update.side_effect = Exception + mock_db["bids"].find_one.return_value = { + "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", + "tender": "Business Intelligence and Data Warehousing", + "alias": "ONS", + "bid_date": "2023-06-23", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "Office for National Statistics", + "was_successful": False, + } + mock_db["bids"].find_one.side_effect = Exception bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"tender": "Updated tender"} response = client.put(f"api/bids/{bid_id}", json=update) @@ -66,17 +96,56 @@ def test_update_by_id_find_error(mock_db, client): # Case 6: Update failed field @patch("api.controllers.bid_controller.db") def test_update_failed(mock_db, client): - bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" + mock_db["bids"].find_one.return_value = { + "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", + "alias": "ONS", + "bid_date": "2023-06-23", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "Office for National Statistics", + "failed": None, + "feedback": None, + "last_updated": "2023-07-24T11:38:20.388019", + "links": { + "questions": "http://localhost:8080/bids/4141fac8-8879-4169-a46d-2effb1f515f6/questions", + "self": "http://localhost:8080/bids/4141fac8-8879-4169-a46d-2effb1f515f6", + }, + "status": "in_progress", + "success": [], + "tender": "Business Intelligence and Data Warehousing", + "was_successful": False, + } + bid_id = "4141fac8-8879-4169-a46d-2effb1f515f6" update = {"failed": {"phase": 2, "has_score": True, "score": 20, "out_of": 36}} response = client.put(f"api/bids/{bid_id}", json=update) - mock_db["bids"].find_one_and_update.assert_called_once() + mock_db["bids"].find_one.assert_called_once_with( + {"_id": bid_id, "status": "in_progress"} + ) + mock_db["bids"].replace_one.assert_called_once() assert response.status_code == 200 # Case 7: Update success field @patch("api.controllers.bid_controller.db") def test_update_success(mock_db, client): - bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" + mock_db["bids"].find_one.return_value = { + "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", + "alias": "ONS", + "bid_date": "2023-06-23", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "Office for National Statistics", + "failed": None, + "feedback": None, + "last_updated": "2023-07-24T11:38:20.388019", + "links": { + "questions": "http://localhost:8080/bids/4141fac8-8879-4169-a46d-2effb1f515f6/questions", + "self": "http://localhost:8080/bids/4141fac8-8879-4169-a46d-2effb1f515f6", + }, + "status": "in_progress", + "success": [], + "tender": "Business Intelligence and Data Warehousing", + "was_successful": False, + } + bid_id = "4141fac8-8879-4169-a46d-2effb1f515f6" update = { "success": [ {"phase": 1, "has_score": True, "score": 20, "out_of": 36}, @@ -84,5 +153,8 @@ def test_update_success(mock_db, client): ] } response = client.put(f"api/bids/{bid_id}", json=update) - mock_db["bids"].find_one_and_update.assert_called_once() + mock_db["bids"].find_one.assert_called_once_with( + {"_id": bid_id, "status": "in_progress"} + ) + mock_db["bids"].replace_one.assert_called_once() assert response.status_code == 200 diff --git a/tests/test_update_bid_status.py b/tests/test_update_bid_status.py index d1f4491..3ee6513 100644 --- a/tests/test_update_bid_status.py +++ b/tests/test_update_bid_status.py @@ -4,26 +4,54 @@ # Case 1: Successful update @patch("api.controllers.bid_controller.db") def test_update_bid_status_success(mock_db, client): - mock_db["bids"].find_one_and_update.return_value = { - "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", - "tender": "Business Intelligence and Data Warehousing", + mock_db["bids"].find_one.return_value = { + "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", "alias": "ONS", "bid_date": "2023-06-23", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "client": "Office for National Statistics", + "failed": None, + "feedback": None, + "last_updated": "2023-07-24T11:38:20.388019", + "links": { + "questions": "http://localhost:8080/bids/4141fac8-8879-4169-a46d-2effb1f515f6/questions", + "self": "http://localhost:8080/bids/4141fac8-8879-4169-a46d-2effb1f515f6", + }, + "status": "in_progress", + "success": [], + "tender": "Business Intelligence and Data Warehousing", "was_successful": False, } - bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" + bid_id = "4141fac8-8879-4169-a46d-2effb1f515f6" update = {"status": "completed"} response = client.put(f"api/bids/{bid_id}/status", json=update) - mock_db["bids"].find_one_and_update.assert_called_once() + mock_db["bids"].find_one.assert_called_once_with({"_id": bid_id}) + # mock_db["bids"].replace_one.assert_called_once() assert response.status_code == 200 # Case 2: Invalid status @patch("api.controllers.bid_controller.db") def test_invalid_status(mock_db, client): + mock_db["bids"].find_one.return_value = { + "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", + "alias": "ONS", + "bid_date": "2023-06-23", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "Office for National Statistics", + "failed": None, + "feedback": None, + "last_updated": "2023-07-24T11:38:20.388019", + "links": { + "questions": "http://localhost:8080/bids/4141fac8-8879-4169-a46d-2effb1f515f6/questions", + "self": "http://localhost:8080/bids/4141fac8-8879-4169-a46d-2effb1f515f6", + }, + "status": "in_progress", + "success": [], + "tender": "Business Intelligence and Data Warehousing", + "was_successful": False, + } bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "invalid"} response = client.put(f"api/bids/{bid_id}/status", json=update) @@ -47,7 +75,7 @@ def test_empty_request(mock_db, client): # Case 4: Bid not found @patch("api.controllers.bid_controller.db") def test_bid_not_found(mock_db, client): - mock_db["bids"].find_one_and_update.return_value = None + mock_db["bids"].find_one.return_value = None bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "completed"} response = client.put(f"api/bids/{bid_id}/status", json=update) @@ -58,7 +86,7 @@ def test_bid_not_found(mock_db, client): # Case 5: Failed to call database @patch("api.controllers.bid_controller.db") def test_update_status_find_error(mock_db, client): - mock_db["bids"].find_one_and_update.side_effect = Exception + mock_db["bids"].find_one.side_effect = Exception bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "completed"} response = client.put(f"api/bids/{bid_id}/status", json=update)